diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000000..bd3c077fc32 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "mixpanel-iphone"] + path = mixpanel-iphone + url = git@github.com:mixpanel/mixpanel-iphone.git +[submodule "ios/mixpanel-iphone"] + path = ios/mixpanel-iphone + url = git@github.com:mixpanel/mixpanel-iphone.git diff --git a/android/app/build.gradle b/android/app/build.gradle index f2030dee4ae..ee1b2f320c3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -174,14 +174,14 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 8 - versionName "0.1.7" + versionCode 9 + versionName "0.1.8" multiDexEnabled true testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy "minReactNative", "minReactNative46" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ndk { - abiFilters "armeabi-v7a", "x86" + abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64" } dexOptions { javaMaxHeapSize "2048M" @@ -189,7 +189,8 @@ android { manifestPlaceholders = [ MM_BRANCH_KEY_TEST: "$System.env.MM_BRANCH_KEY_TEST", - MM_BRANCH_KEY_LIVE: "$System.env.MM_BRANCH_KEY_LIVE" + MM_BRANCH_KEY_LIVE: "$System.env.MM_BRANCH_KEY_LIVE", + MM_MIXPANEL_TOKEN: "$System.env.MM_MIXPANEL_TOKEN" ] missingDimensionStrategy 'react-native-camera', 'general' @@ -213,6 +214,12 @@ android { include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" } } + + packagingOptions { + pickFirst 'lib/x86_64/libjsc.so' + pickFirst 'lib/arm64-v8a/libjsc.so' + } + buildTypes { debug { manifestPlaceholders = [isDebug:true] @@ -248,6 +255,8 @@ android { } dependencies { + implementation project(':react-native-fabric') + implementation project(':@react-native-community_netinfo') implementation project(':react-native-view-shot') implementation project(':lottie-react-native') implementation project(':@react-native-community_async-storage') @@ -257,9 +266,9 @@ dependencies { implementation project(':react-native-svg') implementation project(':react-native-gesture-handler') implementation project(':react-native-screens') - implementation 'com.android.support:multidex:1.0.1' - implementation "com.android.support:support-annotations:27.1.1" - implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" + implementation 'androidx.multidex:multidex:2.0.0' + implementation 'androidx.annotation:annotation:1.0.0' + implementation 'androidx.appcompat:appcompat:1.0.0' implementation "com.facebook.react:react-native:+" // From node_modules implementation project(':react-native-branch') @@ -269,13 +278,13 @@ dependencies { implementation project(':react-native-camera') implementation project(':react-native-share') implementation project(':react-native-i18n') - implementation project(':react-native-fabric') implementation project(':react-native-aes-crypto') implementation project(':react-native-keychain') implementation project(':react-native-os') implementation project(':react-native-randombytes') implementation project(':react-native-fs') implementation project(':react-native-vector-icons') + implementation 'com.mixpanel.android:mixpanel-android:5.+' implementation('com.crashlytics.sdk.android:crashlytics:2.9.4@aar') { transitive = true; diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 01328b29fa4..b24e883ff64 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ > + @@ -43,12 +44,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -70,6 +110,8 @@ android:value="io.metamask"/> + + @@ -80,7 +122,7 @@ diff --git a/android/app/src/main/java/io/metamask/MainActivity.java b/android/app/src/main/java/io/metamask/MainActivity.java index 49c8c479bef..3af0f56e99e 100644 --- a/android/app/src/main/java/io/metamask/MainActivity.java +++ b/android/app/src/main/java/io/metamask/MainActivity.java @@ -3,12 +3,18 @@ import com.facebook.react.ReactActivityDelegate; import com.facebook.react.ReactFragmentActivity; import com.facebook.react.ReactRootView; +import com.mixpanel.android.mpmetrics.MixpanelAPI; import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; import io.branch.rnbranch.*; + import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.Bundle; -import android.support.annotation.Nullable; +import android.util.Log; + +import androidx.annotation.NonNull; public class MainActivity extends ReactFragmentActivity { @@ -25,9 +31,15 @@ protected String getMainComponentName() { @Override protected void onStart() { super.onStart(); - if(!BuildConfig.DEBUG){ - RNBranchModule.initSession(getIntent().getData(), this); + RNBranchModule.initSession(getIntent().getData(), this); + try{ + ApplicationInfo ai = this.getPackageManager().getApplicationInfo(this.getPackageName(), PackageManager.GET_META_DATA); + String mixpanelToken = (String)ai.metaData.get("com.mixpanel.android.mpmetrics.MixpanelAPI.token"); + MixpanelAPI.getInstance(this, mixpanelToken); + }catch (PackageManager.NameNotFoundException e){ + Log.d("RCTAnalytics","init:token missing"); } + } @Override @@ -43,7 +55,7 @@ public void onNewIntent(Intent intent) { @Override protected ReactActivityDelegate createReactActivityDelegate() { return new ReactActivityDelegate(this, getMainComponentName()) { - @Nullable + @NonNull @Override protected Bundle getLaunchOptions() { Bundle bundle = new Bundle(); diff --git a/android/app/src/main/java/io/metamask/MainApplication.java b/android/app/src/main/java/io/metamask/MainApplication.java index 19f7c6daf61..986909cb69d 100644 --- a/android/app/src/main/java/io/metamask/MainApplication.java +++ b/android/app/src/main/java/io/metamask/MainApplication.java @@ -4,6 +4,8 @@ import com.crashlytics.android.Crashlytics; import com.facebook.react.ReactApplication; +import com.smixx.fabric.FabricPackage; +import com.reactnativecommunity.netinfo.NetInfoPackage; import fr.greweb.reactnativeviewshot.RNViewShotPackage; import com.airbnb.android.react.lottie.LottiePackage; import com.reactnativecommunity.asyncstorage.AsyncStoragePackage; @@ -13,8 +15,9 @@ import com.horcrux.svg.SvgPackage; import com.swmansion.gesturehandler.react.RNGestureHandlerPackage; import io.branch.rnbranch.RNBranchPackage; -import io.branch.referral.Branch; +import io.branch.rnbranch.RNBranchModule; import com.web3webview.Web3WebviewPackage; +import io.metamask.nativeModules.RCTAnalyticsPackage; import com.oblador.vectoricons.VectorIconsPackage; import cl.json.RNSharePackage; import com.bitgo.randombytes.RandomBytesPackage; @@ -22,7 +25,6 @@ import com.oblador.keychain.KeychainPackage; import com.AlexanderZaytsev.RNI18n.RNI18nPackage; import com.rnfs.RNFSPackage; -import com.smixx.fabric.FabricPackage; import org.reactnative.camera.RNCameraPackage; import com.tectiv3.aes.RCTAesPackage; import com.swmansion.rnscreens.RNScreensPackage; @@ -35,7 +37,7 @@ import java.util.Arrays; import java.util.List; -import android.support.multidex.MultiDexApplication; +import androidx.multidex.MultiDexApplication; public class MainApplication extends MultiDexApplication implements ShareApplication, ReactApplication { @@ -50,8 +52,10 @@ public boolean getUseDeveloperSupport() { protected List getPackages() { return Arrays.asList( new MainReactPackage(), - new RNViewShotPackage(), - new LottiePackage(), + new FabricPackage(), + new NetInfoPackage(), + new RNViewShotPackage(), + new LottiePackage(), new AsyncStoragePackage(), new ReactNativePushNotificationPackage(), new BackgroundTimerPackage(), @@ -60,7 +64,6 @@ protected List getPackages() { new RNGestureHandlerPackage(), new RNScreensPackage(), new RNBranchPackage(), - new FabricPackage(), new KeychainPackage(), new RandomBytesPackage(), new RCTAesPackage(), @@ -70,7 +73,8 @@ protected List getPackages() { new RNOSModule(), new RNSharePackage(), new VectorIconsPackage(), - new Web3WebviewPackage() + new Web3WebviewPackage(), + new RCTAnalyticsPackage() ); } @@ -90,8 +94,8 @@ public void onCreate() { super.onCreate(); if (!BuildConfig.DEBUG){ Fabric.with(this, new Crashlytics()); - Branch.getAutoInstance(this); } + RNBranchModule.getAutoInstance(this); SoLoader.init(this, /* native exopackage */ false); } diff --git a/android/app/src/main/java/io/metamask/SplashActivity.java b/android/app/src/main/java/io/metamask/SplashActivity.java index 13708cf4751..f8d2552c096 100644 --- a/android/app/src/main/java/io/metamask/SplashActivity.java +++ b/android/app/src/main/java/io/metamask/SplashActivity.java @@ -2,7 +2,7 @@ import android.content.Intent; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatActivity; public class SplashActivity extends AppCompatActivity { @Override diff --git a/android/app/src/main/java/io/metamask/nativeModules/RCTAnalytics.java b/android/app/src/main/java/io/metamask/nativeModules/RCTAnalytics.java new file mode 100644 index 00000000000..3987115ad96 --- /dev/null +++ b/android/app/src/main/java/io/metamask/nativeModules/RCTAnalytics.java @@ -0,0 +1,178 @@ +package io.metamask.nativeModules; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.util.Log; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.Promise; + +import com.mixpanel.android.mpmetrics.MixpanelAPI; +import com.mixpanel.android.mpmetrics.Tweak; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; + +public class RCTAnalytics extends ReactContextBaseJavaModule { + + MixpanelAPI mixpanel; + private static Tweak remoteVariables = MixpanelAPI.stringTweak("remoteVariables","{}"); + + + public RCTAnalytics(ReactApplicationContext reactContext) { + super(reactContext); + try{ + ApplicationInfo ai = reactContext.getPackageManager().getApplicationInfo(reactContext.getPackageName(), PackageManager.GET_META_DATA); + String mixpanelToken = (String)ai.metaData.get("com.mixpanel.android.mpmetrics.MixpanelAPI.token"); + this.mixpanel = + MixpanelAPI.getInstance(reactContext, mixpanelToken); + }catch (PackageManager.NameNotFoundException e){ + Log.d("RCTAnalytics","init:token missing"); + } + } + + @Override + public String getName() { + return "Analytics"; + } + + @ReactMethod + public void trackEvent(ReadableMap e) { + String eventCategory = e.getString("category"); + JSONObject props = toJSONObject(e); + props.remove("category"); + this.mixpanel.track(eventCategory, props); + } + + @ReactMethod + public void optIn(boolean val) { + if(val){ + this.mixpanel.optInTracking(); + }else{ + this.mixpanel.optOutTracking(); + } + } + + @ReactMethod + public void getRemoteVariables(Promise promise) { + try{ + String vars = remoteVariables.get(); + promise.resolve(vars); + } catch (Error e){ + promise.reject(e); + } + + } + + private JSONObject toJSONObject(ReadableMap readableMap){ + + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + JSONObject map = new JSONObject(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + try{ + switch (readableMap.getType(key)) { + case Null: + map.put(key, null); + break; + case Boolean: + map.put(key, readableMap.getBoolean(key)); + break; + case Number: + map.put(key, readableMap.getDouble(key)); + break; + case String: + map.put(key, readableMap.getString(key)); + break; + case Map: + map.put(key, toHashMap(readableMap.getMap(key))); + break; + case Array: + map.put(key, toArrayList(readableMap.getArray(key))); + break; + default: + throw new IllegalArgumentException("Could not convert object with key: " + key + "."); + } + }catch(JSONException e){ + Log.d("RCTAnalytics","JSON parse error"); + } + } + return map; + + } + + private HashMap toHashMap(ReadableMap readableMap){ + + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + HashMap hashMap = new HashMap<>(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + switch (readableMap.getType(key)) { + case Null: + hashMap.put(key, null); + break; + case Boolean: + hashMap.put(key, readableMap.getBoolean(key)); + break; + case Number: + hashMap.put(key, readableMap.getDouble(key)); + break; + case String: + hashMap.put(key, readableMap.getString(key)); + break; + case Map: + hashMap.put(key, toHashMap(readableMap.getMap(key))); + break; + case Array: + hashMap.put(key, toArrayList(readableMap.getArray(key))); + break; + default: + throw new IllegalArgumentException("Could not convert object with key: " + key + "."); + } + } + return hashMap; + + } + + + private ArrayList toArrayList(ReadableArray readableArray) { + + + ArrayList arrayList = new ArrayList<>(); + + for (int i = 0; i < readableArray.size(); i++) { + switch (readableArray.getType(i)) { + case Null: + arrayList.add(null); + break; + case Boolean: + arrayList.add(readableArray.getBoolean(i)); + break; + case Number: + arrayList.add(readableArray.getDouble(i)); + break; + case String: + arrayList.add(readableArray.getString(i)); + break; + case Map: + arrayList.add(toHashMap(readableArray.getMap(i))); + break; + case Array: + arrayList.add(toArrayList(readableArray.getArray(i))); + break; + default: + throw new IllegalArgumentException("Could not convert object at index: " + i + "."); + } + } + return arrayList; + + } +} diff --git a/android/app/src/main/java/io/metamask/nativeModules/RCTAnalyticsPackage.java b/android/app/src/main/java/io/metamask/nativeModules/RCTAnalyticsPackage.java new file mode 100644 index 00000000000..d16fa85c53f --- /dev/null +++ b/android/app/src/main/java/io/metamask/nativeModules/RCTAnalyticsPackage.java @@ -0,0 +1,29 @@ +package io.metamask.nativeModules; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RCTAnalyticsPackage implements ReactPackage { + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new RCTAnalytics(reactContext)); + + return modules; + } + +} diff --git a/android/app/src/main/res/drawable-hdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png b/android/app/src/main/res/drawable-hdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png new file mode 100644 index 00000000000..ad03a63bf3c Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_astronaut.png b/android/app/src/main/res/drawable-mdpi/app_images_astronaut.png new file mode 100644 index 00000000000..b9870ef26df Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_astronaut.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_bg.png b/android/app/src/main/res/drawable-mdpi/app_images_bg.png new file mode 100644 index 00000000000..0ddd46b28b9 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_bg.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_ethlogo.png b/android/app/src/main/res/drawable-mdpi/app_images_ethlogo.png new file mode 100644 index 00000000000..9fc721f6433 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_ethlogo.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_fox.png b/android/app/src/main/res/drawable-mdpi/app_images_fox.png new file mode 100644 index 00000000000..63f9006eb93 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_fox.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_foxbadge.png b/android/app/src/main/res/drawable-mdpi/app_images_foxbadge.png new file mode 100644 index 00000000000..923694a20df Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_foxbadge.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_frame.png b/android/app/src/main/res/drawable-mdpi/app_images_frame.png new file mode 100644 index 00000000000..b612cccfb88 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_frame.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_lock.png b/android/app/src/main/res/drawable-mdpi/app_images_lock.png new file mode 100644 index 00000000000..2763fee33a1 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_lock.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_metamaskname.png b/android/app/src/main/res/drawable-mdpi/app_images_metamaskname.png new file mode 100644 index 00000000000..cba4a0706ff Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_metamaskname.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_mminstapay.png b/android/app/src/main/res/drawable-mdpi/app_images_mminstapay.png new file mode 100644 index 00000000000..eb2e5bebe7e Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_mminstapay.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_mminstapayselected.png b/android/app/src/main/res/drawable-mdpi/app_images_mminstapayselected.png new file mode 100644 index 00000000000..f3170fcbd76 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_mminstapayselected.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_opensealogoflatcoloredblue.png b/android/app/src/main/res/drawable-mdpi/app_images_opensealogoflatcoloredblue.png new file mode 100644 index 00000000000..1a8a294b6b8 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_opensealogoflatcoloredblue.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_paymentchannelwatermark.png b/android/app/src/main/res/drawable-mdpi/app_images_paymentchannelwatermark.png new file mode 100644 index 00000000000..36e8fb7a648 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_paymentchannelwatermark.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_paymentchannelwelcome.png b/android/app/src/main/res/drawable-mdpi/app_images_paymentchannelwelcome.png new file mode 100644 index 00000000000..d7095430d32 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_paymentchannelwelcome.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_selectedwalleticon.png b/android/app/src/main/res/drawable-mdpi/app_images_selectedwalleticon.png new file mode 100644 index 00000000000..1b0e1044e80 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_selectedwalleticon.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_syncicon.png b/android/app/src/main/res/drawable-mdpi/app_images_syncicon.png new file mode 100644 index 00000000000..68ed6f36e9d Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_syncicon.png differ diff --git a/android/app/src/main/res/drawable-mdpi/app_images_walleticon.png b/android/app/src/main/res/drawable-mdpi/app_images_walleticon.png new file mode 100644 index 00000000000..7b582eb6262 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/app_images_walleticon.png differ diff --git a/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png b/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png new file mode 100644 index 00000000000..4637f04abf9 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png differ diff --git a/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png b/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png new file mode 100644 index 00000000000..b8caaa17d82 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png differ diff --git a/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png b/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png new file mode 100644 index 00000000000..55afac478f4 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png differ diff --git a/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png b/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png new file mode 100644 index 00000000000..1c7da1dcd90 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png differ diff --git a/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png b/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png new file mode 100644 index 00000000000..083db295f47 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png differ diff --git a/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backiconmask.png b/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backiconmask.png new file mode 100644 index 00000000000..dbddbdff603 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigationstack_src_views_assets_backiconmask.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png b/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png new file mode 100644 index 00000000000..de61f137831 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png b/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png new file mode 100644 index 00000000000..d97610d44ba Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png b/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png new file mode 100644 index 00000000000..54e0d5fb969 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png b/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png new file mode 100644 index 00000000000..85a3771096a Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png b/android/app/src/main/res/drawable-xhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png new file mode 100644 index 00000000000..6de0a1cbb36 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png b/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png new file mode 100644 index 00000000000..a1fabf69cf0 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_danger.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png b/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png new file mode 100644 index 00000000000..7686d9c3281 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_info.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png b/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png new file mode 100644 index 00000000000..6472f9d45f3 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_success.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png b/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png new file mode 100644 index 00000000000..8b4a90a16b6 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnativeflashmessage_src_icons_fm_icon_warning.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png b/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png new file mode 100644 index 00000000000..15a983a67d9 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png b/android/app/src/main/res/drawable-xxxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png new file mode 100644 index 00000000000..17e52e8550e Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/node_modules_reactnavigationstack_src_views_assets_backicon.png differ diff --git a/android/gradle.properties b/android/gradle.properties index e586e7e3e98..821cee49276 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -17,3 +17,5 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true android.disableResourceValidation=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/settings.gradle b/android/settings.gradle index 0a21451e91a..628cfb75a8e 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,8 @@ rootProject.name = 'MetaMask' +include ':react-native-fabric' +project(':react-native-fabric').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fabric/android') +include ':@react-native-community_netinfo' +project(':@react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android') include ':react-native-view-shot' project(':react-native-view-shot').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-view-shot/android') include ':lottie-react-native' @@ -26,8 +30,6 @@ include ':react-native-aes-crypto' project(':react-native-aes-crypto').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-aes-crypto/android') include ':react-native-camera' project(':react-native-camera').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-camera/android') -include ':react-native-fabric' -project(':react-native-fabric').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fabric/android') include ':react-native-fs' project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fs/android') include ':react-native-i18n' diff --git a/app/actions/settings/index.js b/app/actions/settings/index.js index e731a61b7e0..779ff8056ae 100644 --- a/app/actions/settings/index.js +++ b/app/actions/settings/index.js @@ -25,3 +25,10 @@ export function setPrimaryCurrency(primaryCurrency) { primaryCurrency }; } + +export function setEnablePaymentChannels(paymentChannelsEnabled) { + return { + type: 'SET_ENABLE_PAYMENT_CHANNELS', + paymentChannelsEnabled + }; +} diff --git a/app/actions/transaction/index.js b/app/actions/transaction/index.js index d3b83af2f56..dc897049fbb 100644 --- a/app/actions/transaction/index.js +++ b/app/actions/transaction/index.js @@ -7,6 +7,18 @@ export function newTransaction() { }; } +/** + * Sets any attribute in transaction object + * + * @param {object} transaction - New transaction object + */ +export function setPaymentChannelTransaction(asset) { + return { + type: 'SET_PAYMENT_CHANNEL_TRANSACTION', + asset + }; +} + /** * Sets any attribute in transaction object * diff --git a/app/animations/bounce.json b/app/animations/bounce.json index e04c6ea807a..726c7eac351 100644 --- a/app/animations/bounce.json +++ b/app/animations/bounce.json @@ -28,18 +28,18 @@ "o": { "x": 0.167, "y": 0.167 }, "t": 0, "s": [562.688, 850.992, 0], - "e": [562.688, 730.992, 0], - "to": [0, -20, 0], + "e": [562.688, 626.992, 0], + "to": [0, -37.333, 0], "ti": [0, 0, 0] }, { "i": { "x": 0.833, "y": 0.833 }, "o": { "x": 0.167, "y": 0.167 }, "t": 12, - "s": [562.688, 730.992, 0], + "s": [562.688, 626.992, 0], "e": [562.688, 850.992, 0], "to": [0, 0, 0], - "ti": [0, -20, 0] + "ti": [0, -37.333, 0] }, { "t": 22 } ], @@ -94,7 +94,7 @@ { "i": { "x": 0.833, "y": 1 }, "o": { "x": 0.167, "y": 0 }, - "t": 10, + "t": 7, "s": [ { "i": [[0, 0], [0, 0], [0, 0], [0, 0]], @@ -183,18 +183,18 @@ "o": { "x": 0.167, "y": 0.167 }, "t": 0, "s": [562.688, 850.992, 0], - "e": [562.688, 730.992, 0], - "to": [0, -20, 0], + "e": [562.688, 626.992, 0], + "to": [0, -37.333, 0], "ti": [0, 0, 0] }, { "i": { "x": 0.833, "y": 0.833 }, "o": { "x": 0.167, "y": 0.167 }, "t": 12, - "s": [562.688, 730.992, 0], + "s": [562.688, 626.992, 0], "e": [562.688, 850.992, 0], "to": [0, 0, 0], - "ti": [0, -20, 0] + "ti": [0, -37.333, 0] }, { "t": 22 } ], @@ -317,8 +317,8 @@ "tm": { "a": 1, "k": [ - { "i": { "x": [0], "y": [1] }, "o": { "x": [0.064], "y": [0] }, "t": 0, "s": [0], "e": [0.4] }, - { "i": { "x": [1], "y": [1] }, "o": { "x": [1], "y": [0] }, "t": 8, "s": [0.4], "e": [0.733] }, + { "i": { "x": [0], "y": [1] }, "o": { "x": [0.135], "y": [0] }, "t": 0, "s": [0], "e": [0.4] }, + { "i": { "x": [0.928], "y": [1] }, "o": { "x": [1], "y": [0] }, "t": 8, "s": [0.4], "e": [0.733] }, { "i": { "x": [0.858], "y": [1] }, "o": { "x": [0.158], "y": [0] }, diff --git a/app/animations/fox-in.json b/app/animations/fox-in.json index 3b7cabc99cb..72e186e80b4 100644 --- a/app/animations/fox-in.json +++ b/app/animations/fox-in.json @@ -2,7 +2,7 @@ "v": "5.5.1", "fr": 30, "ip": 0, - "op": 41, + "op": 34, "w": 1120, "h": 930, "nm": "Fox_simpler", @@ -189,7 +189,7 @@ } ], "ip": 0, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -355,7 +355,7 @@ } ], "ip": 0, - "op": 51, + "op": 56, "st": 0, "bm": 0 }, @@ -544,7 +544,7 @@ } ], "ip": 0, - "op": 51, + "op": 56, "st": 0, "bm": 0 }, @@ -733,7 +733,7 @@ } ], "ip": 0, - "op": 51, + "op": 56, "st": 0, "bm": 0 }, @@ -922,7 +922,7 @@ } ], "ip": 0, - "op": 51, + "op": 56, "st": 0, "bm": 0 }, @@ -1061,7 +1061,7 @@ } ], "ip": 5, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -1200,7 +1200,7 @@ } ], "ip": 5, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -1339,7 +1339,7 @@ } ], "ip": 11, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -1478,7 +1478,7 @@ } ], "ip": 11, - "op": 51, + "op": 56, "st": -15, "bm": 0 }, @@ -1617,7 +1617,7 @@ } ], "ip": 11, - "op": 51, + "op": 56, "st": -15, "bm": 0 }, @@ -1754,7 +1754,7 @@ } ], "ip": 11, - "op": 51, + "op": 56, "st": -15, "bm": 0 }, @@ -1891,7 +1891,7 @@ } ], "ip": 11, - "op": 51, + "op": 56, "st": -15, "bm": 0 }, @@ -2121,7 +2121,7 @@ } ], "ip": 16, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -2259,7 +2259,7 @@ } ], "ip": 16, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -2395,7 +2395,7 @@ } ], "ip": 22, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -2535,7 +2535,7 @@ } ], "ip": 27, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -2671,7 +2671,7 @@ } ], "ip": 22, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -2811,7 +2811,7 @@ } ], "ip": 27, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -2950,7 +2950,7 @@ } ], "ip": 22, - "op": 51, + "op": 56, "st": -21, "bm": 0 }, @@ -3089,7 +3089,7 @@ } ], "ip": 27, - "op": 51, + "op": 56, "st": -13, "bm": 0 }, @@ -3228,7 +3228,7 @@ } ], "ip": 22, - "op": 51, + "op": 56, "st": -21, "bm": 0 }, @@ -3367,7 +3367,7 @@ } ], "ip": 27, - "op": 51, + "op": 56, "st": -13, "bm": 0 }, @@ -3503,7 +3503,7 @@ } ], "ip": 33, - "op": 51, + "op": 56, "st": -31, "bm": 0 }, @@ -3668,7 +3668,7 @@ } ], "ip": 33, - "op": 51, + "op": 56, "st": -31, "bm": 0 }, @@ -3804,7 +3804,7 @@ } ], "ip": 33, - "op": 51, + "op": 56, "st": -31, "bm": 0 }, @@ -3940,7 +3940,7 @@ } ], "ip": 33, - "op": 51, + "op": 56, "st": -31, "bm": 0 }, @@ -4078,7 +4078,7 @@ } ], "ip": 16, - "op": 51, + "op": 56, "st": -23, "bm": 0 }, @@ -4216,7 +4216,7 @@ } ], "ip": 27, - "op": 51, + "op": 56, "st": -7, "bm": 0 }, @@ -4354,7 +4354,7 @@ } ], "ip": 31, - "op": 51, + "op": 56, "st": -2, "bm": 0 }, @@ -4490,7 +4490,7 @@ } ], "ip": 41, - "op": 51, + "op": 56, "st": 13, "bm": 0 }, @@ -4628,7 +4628,7 @@ } ], "ip": 17, - "op": 51, + "op": 56, "st": -23, "bm": 0 }, @@ -4766,7 +4766,7 @@ } ], "ip": 28, - "op": 51, + "op": 56, "st": -7, "bm": 0 }, @@ -4904,7 +4904,7 @@ } ], "ip": 32, - "op": 51, + "op": 56, "st": -1, "bm": 0 }, @@ -5040,7 +5040,7 @@ } ], "ip": 41, - "op": 51, + "op": 56, "st": 13, "bm": 0 }, @@ -5179,7 +5179,7 @@ } ], "ip": 12, - "op": 51, + "op": 56, "st": -35, "bm": 0 }, @@ -5318,7 +5318,7 @@ } ], "ip": 17, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -5457,7 +5457,7 @@ } ], "ip": 12, - "op": 51, + "op": 56, "st": -35, "bm": 0 }, @@ -5596,7 +5596,7 @@ } ], "ip": 17, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -5732,7 +5732,7 @@ } ], "ip": 45, - "op": 51, + "op": 56, "st": 3, "bm": 0 }, @@ -5868,7 +5868,7 @@ } ], "ip": 39, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -6006,7 +6006,7 @@ } ], "ip": 39, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -6144,7 +6144,7 @@ } ], "ip": 34, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -6282,7 +6282,7 @@ } ], "ip": 28, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -6418,7 +6418,7 @@ } ], "ip": 45, - "op": 51, + "op": 56, "st": 3, "bm": 0 }, @@ -6554,7 +6554,7 @@ } ], "ip": 39, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -6692,7 +6692,7 @@ } ], "ip": 39, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -6830,7 +6830,7 @@ } ], "ip": 34, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -6968,7 +6968,7 @@ } ], "ip": 28, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -7104,7 +7104,7 @@ } ], "ip": 28, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -7240,7 +7240,7 @@ } ], "ip": 28, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -7376,7 +7376,7 @@ } ], "ip": 32, - "op": 51, + "op": 56, "st": -24, "bm": 0 }, @@ -7512,7 +7512,7 @@ } ], "ip": 32, - "op": 51, + "op": 56, "st": -24, "bm": 0 }, @@ -7635,7 +7635,7 @@ } ], "ip": 17, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -7758,7 +7758,7 @@ } ], "ip": 23, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -7881,7 +7881,7 @@ } ], "ip": 23, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -8006,7 +8006,7 @@ } ], "ip": 28, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -8131,7 +8131,7 @@ } ], "ip": 28, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -8268,7 +8268,7 @@ } ], "ip": 45, - "op": 51, + "op": 56, "st": -15, "bm": 0 }, @@ -8405,7 +8405,7 @@ } ], "ip": 45, - "op": 51, + "op": 56, "st": -15, "bm": 0 }, @@ -8542,7 +8542,7 @@ } ], "ip": 41, - "op": 51, + "op": 56, "st": -21, "bm": 0 }, @@ -8679,7 +8679,7 @@ } ], "ip": 37, - "op": 51, + "op": 56, "st": -28, "bm": 0 }, @@ -8816,7 +8816,7 @@ } ], "ip": 45, - "op": 51, + "op": 56, "st": -15, "bm": 0 }, @@ -8953,7 +8953,7 @@ } ], "ip": 45, - "op": 51, + "op": 56, "st": -15, "bm": 0 }, @@ -9119,7 +9119,7 @@ } ], "ip": 41, - "op": 51, + "op": 56, "st": -21, "bm": 0 }, @@ -9256,7 +9256,7 @@ } ], "ip": 37, - "op": 51, + "op": 56, "st": -28, "bm": 0 }, @@ -9393,7 +9393,7 @@ } ], "ip": 23, - "op": 51, + "op": 56, "st": -37, "bm": 0 }, @@ -9530,7 +9530,7 @@ } ], "ip": 28, - "op": 51, + "op": 56, "st": -29, "bm": 0 }, @@ -9667,7 +9667,7 @@ } ], "ip": 34, - "op": 51, + "op": 56, "st": -21, "bm": 0 }, @@ -9804,7 +9804,7 @@ } ], "ip": 34, - "op": 51, + "op": 56, "st": -21, "bm": 0 }, @@ -9943,7 +9943,7 @@ } ], "ip": 38, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -10080,7 +10080,7 @@ } ], "ip": 39, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -10217,7 +10217,7 @@ } ], "ip": 44, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -10354,7 +10354,7 @@ } ], "ip": 44, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -10491,7 +10491,7 @@ } ], "ip": 44, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -10628,7 +10628,7 @@ } ], "ip": 45, - "op": 51, + "op": 56, "st": -4, "bm": 0 }, @@ -10765,7 +10765,7 @@ } ], "ip": 47, - "op": 51, + "op": 56, "st": -2, "bm": 0 }, @@ -10902,7 +10902,7 @@ } ], "ip": 23, - "op": 51, + "op": 56, "st": -37, "bm": 0 }, @@ -11039,7 +11039,7 @@ } ], "ip": 28, - "op": 51, + "op": 56, "st": -29, "bm": 0 }, @@ -11176,7 +11176,7 @@ } ], "ip": 34, - "op": 51, + "op": 56, "st": -21, "bm": 0 }, @@ -11313,7 +11313,7 @@ } ], "ip": 34, - "op": 51, + "op": 56, "st": -21, "bm": 0 }, @@ -11452,7 +11452,7 @@ } ], "ip": 38, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -11589,7 +11589,7 @@ } ], "ip": 38, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -11726,7 +11726,7 @@ } ], "ip": 44, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -11863,7 +11863,7 @@ } ], "ip": 44, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -12000,7 +12000,7 @@ } ], "ip": 44, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -12137,7 +12137,7 @@ } ], "ip": 45, - "op": 51, + "op": 56, "st": -4, "bm": 0 }, @@ -12274,7 +12274,7 @@ } ], "ip": 47, - "op": 51, + "op": 56, "st": -2, "bm": 0 }, @@ -12416,7 +12416,7 @@ } ], "ip": 34, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -12556,7 +12556,7 @@ } ], "ip": 39, - "op": 51, + "op": 56, "st": -14, "bm": 0 }, @@ -12684,7 +12684,7 @@ } ], "ip": 45, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -12812,7 +12812,7 @@ } ], "ip": 45, - "op": 51, + "op": 56, "st": -6, "bm": 0 }, @@ -12950,7 +12950,7 @@ } ], "ip": 34, - "op": 51, + "op": 56, "st": -22, "bm": 0 }, @@ -13088,7 +13088,7 @@ } ], "ip": 34, - "op": 51, + "op": 56, "st": -22, "bm": 0 } @@ -13123,8 +13123,8 @@ "k": [ { "i": { "x": 0, "y": 1 }, - "o": { "x": 0, "y": 0 }, - "t": 10.255, + "o": { "x": 0.049, "y": 0 }, + "t": 10, "s": [ { "i": [[0, 0], [0, 0], [0, 0]], @@ -13145,7 +13145,7 @@ { "i": { "x": 0, "y": 1 }, "o": { "x": 0.333, "y": 0 }, - "t": 19, + "t": 19.54, "s": [ { "i": [[0, 0], [0, 0], [0, 0]], @@ -13166,7 +13166,7 @@ { "i": { "x": 0.667, "y": 1 }, "o": { "x": 0.333, "y": 0 }, - "t": 33, + "t": 26, "s": [ { "i": [[0, 0], [0, 0], [0, 0]], @@ -13187,7 +13187,7 @@ { "i": { "x": 0.667, "y": 1 }, "o": { "x": 0.167, "y": 0 }, - "t": 34.568, + "t": 27.999, "s": [ { "i": [[0, 0], [0, 0], [0, 0]], @@ -13208,7 +13208,7 @@ { "i": { "x": 0, "y": 1 }, "o": { "x": 0.167, "y": 0 }, - "t": 35.353, + "t": 28.999, "s": [ { "i": [[0, 0], [0, 0], [0, 0]], @@ -13226,7 +13226,7 @@ } ] }, - { "t": 36.921875 } + { "t": 31 } ], "ix": 2 }, @@ -13309,8 +13309,8 @@ "k": [ { "i": { "x": 0, "y": 1 }, - "o": { "x": 0, "y": 0 }, - "t": 10.255, + "o": { "x": 0.049, "y": 0 }, + "t": 10, "s": [ { "i": [[0, 0], [0, 0], [0, 0]], @@ -13331,7 +13331,7 @@ { "i": { "x": 0, "y": 1 }, "o": { "x": 0.333, "y": 0 }, - "t": 19, + "t": 19.54, "s": [ { "i": [[0, 0], [0, 0], [0, 0]], @@ -13352,7 +13352,7 @@ { "i": { "x": 0.667, "y": 1 }, "o": { "x": 0.333, "y": 0 }, - "t": 33, + "t": 26, "s": [ { "i": [[0, 0], [0, 0], [0, 0]], @@ -13373,7 +13373,7 @@ { "i": { "x": 0.667, "y": 1 }, "o": { "x": 0.167, "y": 0 }, - "t": 34.568, + "t": 27.999, "s": [ { "i": [[0, 0], [0, 0], [0, 0]], @@ -13394,7 +13394,7 @@ { "i": { "x": 0, "y": 1 }, "o": { "x": 0.167, "y": 0 }, - "t": 35.353, + "t": 28.999, "s": [ { "i": [[0, 0], [0, 0], [0, 0]], @@ -13412,7 +13412,7 @@ } ] }, - { "t": 36.921875 } + { "t": 31 } ], "ix": 2 }, @@ -13487,7 +13487,7 @@ "a": 1, "k": [ { "i": { "x": [0.015], "y": [1] }, "o": { "x": [0.154], "y": [0] }, "t": 0, "s": [0], "e": [1.7] }, - { "t": 40 } + { "t": 33 } ], "ix": 2 }, diff --git a/app/components/Nav/App/__snapshots__/index.test.js.snap b/app/components/Nav/App/__snapshots__/index.test.js.snap index f500d6b7f8f..cd304232e36 100644 --- a/app/components/Nav/App/__snapshots__/index.test.js.snap +++ b/app/components/Nav/App/__snapshots__/index.test.js.snap @@ -111,6 +111,32 @@ exports[`App should render correctly 1`] = ` "getStateForAction": [Function], }, "LockScreen": null, + "OfflineModeView": Object { + "childRouters": Object { + "OfflineMode": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, + "PaymentChannelView": Object { + "childRouters": Object { + "PaymentChannel": null, + "PaymentChannelDeposit": null, + "PaymentChannelSend": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "PaymentRequestView": Object { "childRouters": Object { "PaymentRequest": null, @@ -489,6 +515,32 @@ exports[`App should render correctly 1`] = ` "getStateForAction": [Function], }, "LockScreen": null, + "OfflineModeView": Object { + "childRouters": Object { + "OfflineMode": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, + "PaymentChannelView": Object { + "childRouters": Object { + "PaymentChannel": null, + "PaymentChannelDeposit": null, + "PaymentChannelSend": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "PaymentRequestView": Object { "childRouters": Object { "PaymentRequest": null, diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index f3ce5921a0e..c7fc66c365c 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -18,6 +18,7 @@ import Main from '../Main'; import DrawerView from '../../UI/DrawerView'; import OptinMetrics from '../../UI/OptinMetrics'; import SimpleWebview from '../../Views/SimpleWebview'; +import DrawerStatusTracker from '../../../core/DrawerStatusTracker'; /** * Stack navigator responsible for the onboarding process @@ -99,6 +100,23 @@ const HomeNav = createDrawerNavigator( } ); +/** + * Drawer status tracking + */ +const defaultGetStateForAction = HomeNav.router.getStateForAction; +DrawerStatusTracker.init(); +HomeNav.router.getStateForAction = (action, state) => { + if (action) { + if (action.type === 'Navigation/MARK_DRAWER_SETTLING' && action.willShow) { + DrawerStatusTracker.setStatus('open'); + } else if (action.type === 'Navigation/MARK_DRAWER_SETTLING' && !action.willShow) { + DrawerStatusTracker.setStatus('closed'); + } + } + + return defaultGetStateForAction(action, state); +}; + /** * Top level switch navigator which decides * which top level view to show diff --git a/app/components/Nav/Main/__snapshots__/index.test.js.snap b/app/components/Nav/Main/__snapshots__/index.test.js.snap index 213a43d57fa..edf21cd2ecc 100644 --- a/app/components/Nav/Main/__snapshots__/index.test.js.snap +++ b/app/components/Nav/Main/__snapshots__/index.test.js.snap @@ -119,6 +119,32 @@ exports[`Main should render correctly 1`] = ` "getStateForAction": [Function], }, "LockScreen": null, + "OfflineModeView": Object { + "childRouters": Object { + "OfflineMode": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, + "PaymentChannelView": Object { + "childRouters": Object { + "PaymentChannel": null, + "PaymentChannelDeposit": null, + "PaymentChannelSend": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "PaymentRequestView": Object { "childRouters": Object { "PaymentRequest": null, @@ -383,6 +409,32 @@ exports[`Main should render correctly 1`] = ` "getStateForAction": [Function], }, "LockScreen": null, + "OfflineModeView": Object { + "childRouters": Object { + "OfflineMode": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, + "PaymentChannelView": Object { + "childRouters": Object { + "PaymentChannel": null, + "PaymentChannelDeposit": null, + "PaymentChannelSend": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "PaymentRequestView": Object { "childRouters": Object { "PaymentRequest": null, diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index fb1b02db9a8..d07d638d60c 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -1,6 +1,15 @@ import React, { Component } from 'react'; -// eslint-disable-next-line react-native/split-platform-components -import { ActivityIndicator, AppState, StyleSheet, View, PushNotificationIOS, Platform } from 'react-native'; +import { + InteractionManager, + ActivityIndicator, + AppState, + StyleSheet, + View, + PushNotificationIOS, // eslint-disable-line react-native/split-platform-components + Platform, + Alert +} from 'react-native'; +import NetInfo from '@react-native-community/netinfo'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { createStackNavigator, createBottomTabNavigator } from 'react-navigation'; @@ -29,6 +38,7 @@ import CollectibleView from '../../Views/CollectibleView'; import Send from '../../Views/Send'; import RevealPrivateCredential from '../../Views/RevealPrivateCredential'; import WalletConnectSessions from '../../Views/WalletConnectSessions'; +import OfflineMode from '../../Views/OfflineMode'; import QrScanner from '../../Views/QRScanner'; import LockScreen from '../../Views/LockScreen'; import ProtectYourAccount from '../../Views/ProtectYourAccount'; @@ -40,6 +50,7 @@ import AccountBackupStep4 from '../../Views/AccountBackupStep4'; import AccountBackupStep5 from '../../Views/AccountBackupStep5'; import AccountBackupStep6 from '../../Views/AccountBackupStep6'; import ImportPrivateKey from '../../Views/ImportPrivateKey'; +import PaymentChannel from '../../Views/PaymentChannel'; import ImportPrivateKeySuccess from '../../Views/ImportPrivateKeySuccess'; import PaymentRequest from '../../UI/PaymentRequest'; import PaymentRequestSuccess from '../../UI/PaymentRequestSuccess'; @@ -51,7 +62,6 @@ import PushNotification from 'react-native-push-notification'; import I18n from '../../../../locales/i18n'; import { colors } from '../../../styles/common'; import LockManager from '../../../core/LockManager'; -import OnboardingWizard from '../../UI/OnboardingWizard'; import FadeOutOverlay from '../../UI/FadeOutOverlay'; import { hexToBN, fromWei } from '../../../util/number'; import { setTransactionObject } from '../../../actions/transaction'; @@ -59,7 +69,14 @@ import PersonalSign from '../../UI/PersonalSign'; import TypedSign from '../../UI/TypedSign'; import Modal from 'react-native-modal'; import WalletConnect from '../../../core/WalletConnect'; +import PaymentChannelsClient from '../../../core/PaymentChannelsClient'; import WalletConnectSessionApproval from '../../UI/WalletConnectSessionApproval'; +import PaymentChannelApproval from '../../UI/PaymentChannelApproval'; +import PaymentChannelDeposit from '../../Views/PaymentChannel/PaymentChannelDeposit'; +import PaymentChannelSend from '../../Views/PaymentChannel/PaymentChannelSend'; +import Networks from '../../../util/networks'; +import { CONNEXT_DEPOSIT } from '../../../util/transactions'; +import { toChecksumAddress } from 'ethereumjs-util'; const styles = StyleSheet.create({ flex: { @@ -139,6 +156,24 @@ const MainNavigator = createStackNavigator( } ) }, + PaymentChannelView: { + screen: createStackNavigator( + { + PaymentChannel: { + screen: PaymentChannel + }, + PaymentChannelDeposit: { + screen: PaymentChannelDeposit + }, + PaymentChannelSend: { + screen: PaymentChannelSend + } + }, + { + mode: 'modal' + } + ) + }, SettingsView: { screen: createStackNavigator({ Settings: { @@ -212,6 +247,13 @@ const MainNavigator = createStackNavigator( } }) }, + OfflineModeView: { + screen: createStackNavigator({ + OfflineMode: { + screen: OfflineMode + } + }) + }, /** ALL FULL SCREEN MODALS SHOULD GO HERE */ QRScanner: { screen: QrScanner @@ -219,6 +261,7 @@ const MainNavigator = createStackNavigator( LockScreen: { screen: LockScreen }, + PaymentRequestView: { screen: createStackNavigator( { @@ -291,9 +334,9 @@ class Main extends Component { */ lockTime: PropTypes.number, /** - * Current onboarding wizard step + * Flag that determines if payment channels are enabled */ - wizardStep: PropTypes.number, + paymentChannelsEnabled: PropTypes.bool, /** * Action that sets a transaction */ @@ -301,16 +344,33 @@ class Main extends Component { /** * Object containing the information for the current transaction */ - transaction: PropTypes.object + transaction: PropTypes.object, + /** + * Selected address + */ + selectedAddress: PropTypes.string, + /** + * List of transactions + */ + transactions: PropTypes.array, + /** + * A string representing the network name + */ + providerType: PropTypes.string }; state = { + connected: true, forceReload: false, signMessage: false, signMessageParams: { data: '' }, signType: '', walletConnectRequest: false, - walletConnectRequestInfo: {} + walletConnectRequestInfo: {}, + paymentChannelRequest: false, + paymentChannelRequestLoading: false, + paymentChannelRequestCompleted: false, + paymentChannelRequestInfo: {} }; backgroundMode = false; @@ -327,86 +387,176 @@ class Main extends Component { }; componentDidMount = async () => { - TransactionsNotificationManager.init(this.props.navigation); - this.pollForIncomingTransactions(); - AppState.addEventListener('change', this.handleAppStateChange); - this.lockManager = new LockManager(this.props.navigation, this.props.lockTime); - PushNotification.configure({ - requestPermissions: false, - onNotification: notification => { - let data = null; - if (Platform.OS === 'android') { - if (notification.tag) { - data = JSON.parse(notification.tag); + InteractionManager.runAfterInteractions(() => { + AppState.addEventListener('change', this.handleAppStateChange); + this.lockManager = new LockManager(this.props.navigation, this.props.lockTime); + PushNotification.configure({ + requestPermissions: false, + onNotification: notification => { + let data = null; + if (Platform.OS === 'android') { + if (notification.tag) { + data = JSON.parse(notification.tag); + } + } else if (notification.data) { + data = notification.data; + } + if (data && data.action === 'tx') { + TransactionsNotificationManager.setTransactionToView(data.id); + this.props.navigation.navigate('TransactionsHome'); } - } else if (notification.data) { - data = notification.data; - } - if (data && data.action === 'tx') { - TransactionsNotificationManager.setTransactionToView(data.id); - this.props.navigation.navigate('TransactionsHome'); - } - if (Platform.OS === 'ios') { - notification.finish(PushNotificationIOS.FetchResult.NoData); + if (Platform.OS === 'ios') { + notification.finish(PushNotificationIOS.FetchResult.NoData); + } } - } - }); + }); - Engine.context.TransactionController.hub.on('unapprovedTransaction', this.onUnapprovedTransaction); - - Engine.context.PersonalMessageManager.hub.on('unapprovedMessage', messageParams => { - const { title: currentPageTitle, url: currentPageUrl } = messageParams.meta; - delete messageParams.meta; - this.setState({ - signMessage: true, - signMessageParams: messageParams, - signType: 'personal', - currentPageTitle, - currentPageUrl + Engine.context.TransactionController.hub.on('unapprovedTransaction', this.onUnapprovedTransaction); + + Engine.context.PersonalMessageManager.hub.on('unapprovedMessage', messageParams => { + const { title: currentPageTitle, url: currentPageUrl } = messageParams.meta; + delete messageParams.meta; + this.setState({ + signMessage: true, + signMessageParams: messageParams, + signType: 'personal', + currentPageTitle, + currentPageUrl + }); }); - }); - Engine.context.TypedMessageManager.hub.on('unapprovedMessage', messageParams => { - const { title: currentPageTitle, url: currentPageUrl } = messageParams.meta; - delete messageParams.meta; - this.setState({ - signMessage: true, - signMessageParams: messageParams, - signType: 'typed', - currentPageTitle, - currentPageUrl + Engine.context.TypedMessageManager.hub.on('unapprovedMessage', messageParams => { + const { title: currentPageTitle, url: currentPageUrl } = messageParams.meta; + delete messageParams.meta; + this.setState({ + signMessage: true, + signMessageParams: messageParams, + signType: 'typed', + currentPageTitle, + currentPageUrl + }); }); + + setTimeout(() => { + TransactionsNotificationManager.init(this.props.navigation); + this.pollForIncomingTransactions(); + + this.initializeWalletConnect(); + + // Only if enabled under settings + if (this.props.paymentChannelsEnabled) { + this.initializePaymentChannels(); + } + + this.removeConnectionStatusListener = NetInfo.addEventListener(this.connectionChangeHandler); + }, 1000); }); + }; + connectionChangeHandler = state => { + // Show the modal once the status changes to offline + if (this.state.connected && !state.isConnected) { + this.props.navigation.navigate('OfflineModeView'); + } + + this.setState({ connected: state.isConnected }); + }; + + initializeWalletConnect = () => { WalletConnect.hub.on('walletconnectSessionRequest', peerInfo => { this.setState({ walletConnectRequest: true, walletConnectRequestInfo: peerInfo }); }); WalletConnect.init(); }; - onUnapprovedTransaction = transactionMeta => { + initializePaymentChannels = () => { + PaymentChannelsClient.init(this.props.selectedAddress); + PaymentChannelsClient.hub.on('payment::request', request => { + this.setState({ paymentChannelRequest: true, paymentChannelRequestInfo: request }); + }); + + PaymentChannelsClient.hub.on('payment::complete', () => { + // show the success screen + this.setState({ paymentChannelRequestCompleted: true }); + // hide the modal and reset state + setTimeout(() => { + setTimeout(() => { + this.setState({ + paymentChannelRequest: false, + paymentChannelRequestLoading: false, + paymentChannelRequestInfo: {} + }); + setTimeout(() => { + this.setState({ + paymentChannelRequestCompleted: false + }); + }); + }, 800); + }, 800); + }); + }; + + paymentChannelDepositSign = async transactionMeta => { + const { TransactionController } = Engine.context; + const { transactions } = this.props; + try { + TransactionController.hub.once(`${transactionMeta.id}:finished`, transactionMeta => { + if (transactionMeta.status === 'submitted') { + this.setState({ transactionHandled: true }); + this.props.navigation.pop(); + TransactionsNotificationManager.watchSubmittedTransaction({ + ...transactionMeta, + assetType: transactionMeta.transaction.assetType + }); + } else { + throw transactionMeta.error; + } + }); + + const fullTx = transactions.find(({ id }) => id === transactionMeta.id); + const updatedTx = { ...fullTx, transaction: transactionMeta.transaction }; + await TransactionController.updateTransaction(updatedTx); + await TransactionController.approveTransaction(transactionMeta.id); + } catch (error) { + Alert.alert('Transaction error', error && error.message, [{ text: 'OK' }]); + this.setState({ transactionHandled: false }); + } + }; + + onUnapprovedTransaction = async transactionMeta => { if (this.props.transaction.value || this.props.transaction.to) { return; } - const { - transaction: { value, gas, gasPrice } - } = transactionMeta; - transactionMeta.transaction.value = hexToBN(value); - transactionMeta.transaction.readableValue = fromWei(transactionMeta.transaction.value); - transactionMeta.transaction.gas = hexToBN(gas); - transactionMeta.transaction.gasPrice = hexToBN(gasPrice); - this.props.setTransactionObject({ - ...{ - symbol: 'ETH', - selectedAsset: { isETH: true, symbol: 'ETH' }, - type: 'ETHER_TRANSACTION', - assetType: 'ETH', - id: transactionMeta.id - }, - ...transactionMeta.transaction - }); - this.props.navigation.push('ApprovalView'); + // Check if it's a payment channel deposit transaction to sign + const networkId = Networks[this.props.providerType].networkId.toString(); + if ( + this.props.paymentChannelsEnabled && + AppConstants.CONNEXT.SUPPORTED_NETWORKS.includes(this.props.providerType) && + transactionMeta.transaction.data.substr(0, 10) === CONNEXT_DEPOSIT && + toChecksumAddress(transactionMeta.transaction.to) === AppConstants.CONNEXT.CONTRACTS[networkId] + ) { + await this.paymentChannelDepositSign(transactionMeta); + } else { + const { + transaction: { value, gas, gasPrice } + } = transactionMeta; + transactionMeta.transaction.value = hexToBN(value); + transactionMeta.transaction.readableValue = fromWei(transactionMeta.transaction.value); + transactionMeta.transaction.gas = hexToBN(gas); + transactionMeta.transaction.gasPrice = hexToBN(gasPrice); + this.props.setTransactionObject({ + ...{ + symbol: 'ETH', + type: 'ETHER_TRANSACTION', + assetType: 'ETH', + selectedAsset: { isETH: true, symbol: 'ETH' }, + id: transactionMeta.id + }, + ...transactionMeta.transaction + }); + this.props.navigation.push('ApprovalView'); + } }; handleAppStateChange = appState => { @@ -438,6 +588,13 @@ class Main extends Component { if (this.props.lockTime !== prevProps.lockTime) { this.lockManager.updateLockTime(this.props.lockTime); } + if (this.props.paymentChannelsEnabled !== prevProps.paymentChannelsEnabled) { + if (this.props.paymentChannelsEnabled) { + this.initializePaymentChannels(); + } else { + PaymentChannelsClient.stop(); + } + } } forceReload() { @@ -461,16 +618,12 @@ class Main extends Component { Engine.context.PersonalMessageManager.hub.removeAllListeners(); Engine.context.TypedMessageManager.hub.removeAllListeners(); Engine.context.TransactionController.hub.removeListener('unapprovedTransaction', this.onUnapprovedTransaction); + WalletConnect.hub.removeAllListeners(); + PaymentChannelsClient.hub.removeAllListeners(); + PaymentChannelsClient.stop(); + this.removeConnectionStatusListener && this.removeConnectionStatusListener(); } - /** - * Return current step of onboarding wizard if not step 5 nor 0 - */ - renderOnboardingWizard = () => { - const { wizardStep } = this.props; - return wizardStep !== 5 && wizardStep > 0 && ; - }; - onSignAction = () => { this.setState({ signMessage: false }); }; @@ -529,9 +682,22 @@ class Main extends Component { WalletConnect.hub.emit('walletconnectSessionRequest::rejected', peerId); }; + onPaymentChannelRequestApproval = () => { + PaymentChannelsClient.hub.emit('payment::confirm', this.state.paymentChannelRequestInfo); + this.setState({ + paymentChannelRequestLoading: true + }); + }; + + onPaymentChannelRequestRejected = () => { + this.setState({ + paymentChannelRequest: false + }); + setTimeout(() => this.setState({ paymentChannelRequestInfo: {} }), 1000); + }; + renderWalletConnectSessionRequestModal = () => { const { walletConnectRequest, walletConnectRequestInfo } = this.state; - const meta = walletConnectRequestInfo.peerMeta || null; return ( @@ -553,6 +719,38 @@ class Main extends Component { title: meta && meta.name, url: meta && meta.url }} + autosign={false} + /> + + ); + }; + + renderPaymentChannelRequestApproval = () => { + const { + paymentChannelRequest, + paymentChannelRequestInfo, + paymentChannelRequestLoading, + paymentChannelRequestCompleted + } = this.state; + + return ( + + ); @@ -565,7 +763,6 @@ class Main extends Component { {!forceReload ? : this.renderLoader()} - {this.renderOnboardingWizard()} {this.renderSigningModal()} {this.renderWalletConnectSessionRequestModal()} + {this.renderPaymentChannelRequestApproval()} ); } @@ -583,8 +781,11 @@ class Main extends Component { const mapStateToProps = state => ({ lockTime: state.settings.lockTime, - wizardStep: state.wizard.step, - transaction: state.transaction + transaction: state.transaction, + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + transactions: state.engine.backgroundState.TransactionController.transactions, + paymentChannelsEnabled: state.settings.paymentChannelsEnabled, + providerType: state.engine.backgroundState.NetworkController.provider.type }); const mapDispatchToProps = dispatch => ({ diff --git a/app/components/UI/AccountApproval/__snapshots__/index.test.js.snap b/app/components/UI/AccountApproval/__snapshots__/index.test.js.snap index 1c618c51874..a44bc0cb01e 100644 --- a/app/components/UI/AccountApproval/__snapshots__/index.test.js.snap +++ b/app/components/UI/AccountApproval/__snapshots__/index.test.js.snap @@ -182,6 +182,7 @@ exports[`AccountApproval should render correctly 1`] = ` diameter={54} /> - {title} + {title || getHost(url)} {url} @@ -275,7 +276,7 @@ class AccountApproval extends Component { - + {renderAccountName(selectedAddress, identities)} diff --git a/app/components/UI/AccountInput/index.js b/app/components/UI/AccountInput/index.js index db6c46c309f..9aab0dcdc91 100644 --- a/app/components/UI/AccountInput/index.js +++ b/app/components/UI/AccountInput/index.js @@ -35,7 +35,9 @@ const styles = StyleSheet.create({ input: { ...fontStyles.bold, backgroundColor: colors.white, - marginRight: 24 + marginRight: 24, + paddingLeft: 0, + minWidth: 120 }, qrCodeButton: { minHeight: Platform.OS === 'android' ? 50 : 50, @@ -46,7 +48,6 @@ const styles = StyleSheet.create({ alignItems: 'center' }, accountContainer: { - flex: 1, flexDirection: 'row', backgroundColor: colors.white, borderColor: colors.grey100, @@ -178,12 +179,12 @@ class AccountInput extends Component { state = { address: undefined, ensRecipient: undefined, - value: undefined + value: undefined, + inputEnabled: Platform.OS === 'ios' }; componentDidMount = async () => { const { provider } = Engine.context.NetworkController; - const { address, network, ensRecipient } = this.props; const networkHasEnsSupport = this.getNetworkEnsSupport(); @@ -191,10 +192,20 @@ class AccountInput extends Component { this.ens = new ENS({ provider, network }); } if (ensRecipient) { - this.setState({ value: ensRecipient, ensRecipient, address }); + this.setState({ value: ensRecipient, ensRecipient, address }, () => { + // If we have an ENS name predefined + // We need to resolve it + this.onBlur(); + }); } else if (address) { this.setState({ value: address, address }); } + + // Workaround https://github.com/facebook/react-native/issues/9958 + !this.state.inputEnabled && + setTimeout(() => { + this.setState({ inputEnabled: true }); + }, 100); }; isEnsName = recipient => { @@ -318,7 +329,7 @@ class AccountInput extends Component { renderOptionList() { const visibleOptions = this.getVisibleOptions(this.state.value); return ( - + {Object.keys(visibleOptions).map(address => @@ -379,6 +390,7 @@ class AccountInput extends Component { onChangeText={this.onChange} placeholder={placeholder} spellCheck={false} + editable={this.state.inputEnabled} style={styles.input} value={value} onBlur={this.onBlur} diff --git a/app/components/UI/AccountList/index.js b/app/components/UI/AccountList/index.js index 3dd23c002a6..6ec8a5d942c 100644 --- a/app/components/UI/AccountList/index.js +++ b/app/components/UI/AccountList/index.js @@ -4,6 +4,7 @@ import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import Identicon from '../Identicon'; import PropTypes from 'prop-types'; import { + Alert, ActivityIndicator, InteractionManager, FlatList, @@ -258,6 +259,30 @@ export default class AccountList extends Component { return ret; } + onLongPress = (address, imported, index) => { + if (!imported) return; + Alert.alert( + strings('accounts.remove_account_title'), + strings('accounts.remove_account_message'), + [ + { + text: strings('accounts.no'), + onPress: () => false, + style: 'cancel' + }, + { + text: strings('accounts.yes_remove_it'), + onPress: async () => { + await Engine.context.KeyringController.removeAccount(address); + // Default to the previous account in the list + this.onAccountChange(index - 1); + } + } + ], + { cancelable: false } + ); + }; + renderItem = ({ item }) => { const { ticker } = this.props; const { index, name, address, balance, isSelected, isImported } = item; @@ -276,6 +301,7 @@ export default class AccountList extends Component { style={styles.account} key={`account-${address}`} onPress={() => this.onAccountChange(index)} // eslint-disable-line + onLongPress={() => this.onLongPress(address, imported, index)} // eslint-disable-line > diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js index b98107a6bbd..17edc1323ee 100644 --- a/app/components/UI/AccountOverview/index.js +++ b/app/components/UI/AccountOverview/index.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Clipboard, Platform, ScrollView, TextInput, StyleSheet, Text, View, TouchableOpacity } from 'react-native'; -import { colors, fontStyles } from '../../../styles/common'; +import { colors, fontStyles, baseStyles } from '../../../styles/common'; import Identicon from '../Identicon'; import Engine from '../../../core/Engine'; import { setTokensTransaction } from '../../../actions/transaction'; @@ -14,7 +14,8 @@ import { toggleAccountsModal } from '../../../actions/modals'; const styles = StyleSheet.create({ scrollView: { - maxHeight: Platform.OS === 'ios' ? 210 : 220 + maxHeight: Platform.OS === 'ios' ? 210 : 220, + backgroundColor: colors.white }, wrapper: { maxHeight: Platform.OS === 'ios' ? 210 : 220, @@ -69,7 +70,6 @@ const styles = StyleSheet.create({ onboardingWizardLabel: { borderWidth: 2, borderRadius: 4, - borderColor: colors.blue, paddingVertical: Platform.OS === 'ios' ? 2 : -4, paddingHorizontal: Platform.OS === 'ios' ? 5 : 5, top: Platform.OS === 'ios' ? 0 : -2 @@ -109,7 +109,11 @@ class AccountOverview extends Component { /** * whether component is being rendered from onboarding wizard */ - onboardingWizard: PropTypes.bool + onboardingWizard: PropTypes.bool, + /** + * Used to get child ref + */ + onRef: PropTypes.func }; state = { @@ -118,6 +122,10 @@ class AccountOverview extends Component { originalAccountLabel: '' }; + editableLabelRef = React.createRef(); + scrollViewContainer = React.createRef(); + mainView = React.createRef(); + animatingAccountsModal = false; toggleAccountsModal = () => { @@ -134,9 +142,10 @@ class AccountOverview extends Component { input = React.createRef(); componentDidMount = () => { - const { identities, selectedAddress } = this.props; + const { identities, selectedAddress, onRef } = this.props; const accountLabel = renderAccountName(selectedAddress, identities); this.setState({ accountLabel }); + onRef && onRef(this); }; setAccountLabel = () => { @@ -190,59 +199,68 @@ class AccountOverview extends Component { const { accountLabelEditable, accountLabel } = this.state; return ( - - - - - - - {accountLabelEditable ? ( - - ) : ( - - + + + + + + + {accountLabelEditable ? ( + - {name} - - - )} + /> + ) : ( + + + {name} + + + )} + + {fiatBalance} + + {renderShortAddress(address)} + - {fiatBalance} - - {renderShortAddress(address)} - - - + + ); } } diff --git a/app/components/UI/AccountRightButton/index.js b/app/components/UI/AccountRightButton/index.js index fe9792a13c4..81c3339eb0a 100644 --- a/app/components/UI/AccountRightButton/index.js +++ b/app/components/UI/AccountRightButton/index.js @@ -8,8 +8,11 @@ import { toggleAccountsModal } from '../../../actions/modals'; const styles = StyleSheet.create({ leftButton: { marginTop: 12, - marginRight: Platform.OS === 'android' ? 22 : 18, - marginBottom: 12 + marginRight: Platform.OS === 'android' ? 7 : 18, + marginLeft: Platform.OS === 'android' ? 7 : 0, + marginBottom: 12, + alignItems: 'center', + justifyContent: 'center' } }); diff --git a/app/components/UI/AccountSelect/index.js b/app/components/UI/AccountSelect/index.js index 9af9bfddd66..2cabce78c9b 100644 --- a/app/components/UI/AccountSelect/index.js +++ b/app/components/UI/AccountSelect/index.js @@ -9,6 +9,8 @@ import { hexToBN } from 'gaba/util'; import { toChecksumAddress } from 'ethereumjs-util'; import { weiToFiat, renderFromWei } from '../../../util/number'; import { getTicker } from '../../../util/transactions'; +import PaymentChannelsClient from '../../../core/PaymentChannelsClient'; +import { strings } from '../../../../locales/i18n'; const styles = StyleSheet.create({ root: { @@ -129,7 +131,11 @@ class AccountSelect extends Component { /** * Current provider ticker */ - ticker: PropTypes.string + ticker: PropTypes.string, + /** + * Transaction object associated with this transaction + */ + transaction: PropTypes.object }; static defaultProps = { @@ -156,12 +162,21 @@ class AccountSelect extends Component { } renderOption(account, onPress) { - const { conversionRate, currentCurrency, primaryCurrency, ticker } = this.props; + const { + conversionRate, + currentCurrency, + primaryCurrency, + ticker, + transaction: { paymentChannelTransaction } + } = this.props; const balance = hexToBN(account.balance); // render balances according to selected 'primaryCurrency' let mainBalance, secondaryBalance; - if (primaryCurrency === 'ETH') { + if (paymentChannelTransaction) { + const state = PaymentChannelsClient.getState(); + mainBalance = `${state.balance} ${strings('unit.dai')}`; + } else if (primaryCurrency === 'ETH') { mainBalance = renderFromWei(balance) + ' ' + getTicker(ticker); secondaryBalance = weiToFiat(balance, conversionRate, currentCurrency.toUpperCase()); } else { @@ -184,7 +199,7 @@ class AccountSelect extends Component { {account.name} {mainBalance} - {secondaryBalance} + {secondaryBalance && {secondaryBalance}} ); @@ -222,7 +237,8 @@ const mapStateToProps = state => ({ currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, primaryCurrency: state.settings.primaryCurrency, - ticker: state.engine.backgroundState.NetworkController.provider.ticker + ticker: state.engine.backgroundState.NetworkController.provider.ticker, + transaction: state.transaction }); export default connect(mapStateToProps)(AccountSelect); diff --git a/app/components/UI/AddCustomCollectible/__snapshots__/index.test.js.snap b/app/components/UI/AddCustomCollectible/__snapshots__/index.test.js.snap index 44006d67073..fe4cb0f6f67 100644 --- a/app/components/UI/AddCustomCollectible/__snapshots__/index.test.js.snap +++ b/app/components/UI/AddCustomCollectible/__snapshots__/index.test.js.snap @@ -14,6 +14,7 @@ exports[`AddCustomCollectible should render correctly 1`] = ` cancelTestID="add-custom-asset-cancel-button" cancelText="CANCEL" confirmButtonMode="normal" + confirmDisabled={true} confirmTestID="add-custom-asset-confirm-button" confirmText="ADD" confirmed={false} diff --git a/app/components/UI/AddCustomCollectible/index.js b/app/components/UI/AddCustomCollectible/index.js index 8edde8c71eb..792890eae50 100644 --- a/app/components/UI/AddCustomCollectible/index.js +++ b/app/components/UI/AddCustomCollectible/index.js @@ -157,50 +157,61 @@ class AddCustomCollectible extends Component { } }; - render = () => ( - - - - - {strings('collectible.collectible_address')} - - {this.state.warningAddress} - - - {strings('collectible.collectible_token_id')} - - {this.state.warningTokenId} + render = () => { + const { address, tokenId } = this.state; + + return ( + + + + + {strings('collectible.collectible_address')} + + {this.state.warningAddress} + + + {strings('collectible.collectible_token_id')} + + {this.state.warningTokenId} + - - - - ); + + + ); + }; } const mapStateToProps = state => ({ diff --git a/app/components/UI/AddCustomToken/__snapshots__/index.test.js.snap b/app/components/UI/AddCustomToken/__snapshots__/index.test.js.snap index 245ba9583de..f941c3fbe3e 100644 --- a/app/components/UI/AddCustomToken/__snapshots__/index.test.js.snap +++ b/app/components/UI/AddCustomToken/__snapshots__/index.test.js.snap @@ -14,6 +14,7 @@ exports[`AddCustomToken should render correctly 1`] = ` cancelTestID="add-custom-asset-cancel-button" cancelText="CANCEL" confirmButtonMode="normal" + confirmDisabled={true} confirmTestID="add-custom-asset-confirm-button" confirmText="ADD TOKEN" confirmed={false} diff --git a/app/components/UI/AddCustomToken/index.js b/app/components/UI/AddCustomToken/index.js index 4b717c61504..bbb1071881c 100644 --- a/app/components/UI/AddCustomToken/index.js +++ b/app/components/UI/AddCustomToken/index.js @@ -149,65 +149,69 @@ export default class AddCustomToken extends Component { current && current.focus(); }; - render = () => ( - - - - - {strings('token.token_address')} - - {this.state.warningAddress} - - - {strings('token.token_symbol')} - - {this.state.warningSymbol} - - - {strings('token.token_precision')} - - {this.state.warningDecimals} + render = () => { + const { address, symbol, decimals } = this.state; + return ( + + + + + {strings('token.token_address')} + + {this.state.warningAddress} + + + {strings('token.token_symbol')} + + {this.state.warningSymbol} + + + {strings('token.token_precision')} + + {this.state.warningDecimals} + - - - - ); + + + ); + }; } diff --git a/app/components/UI/AssetIcon/__snapshots__/index.test.js.snap b/app/components/UI/AssetIcon/__snapshots__/index.test.js.snap index 68ea1a428aa..af7b1ff88d4 100644 --- a/app/components/UI/AssetIcon/__snapshots__/index.test.js.snap +++ b/app/components/UI/AssetIcon/__snapshots__/index.test.js.snap @@ -10,7 +10,7 @@ exports[`AssetIcon should render correctly 1`] = ` } source={ Object { - "uri": "https://raw.githubusercontent.com/MetaMask/eth-contract-metadata/master/images/metamark.svg", + "uri": "https://raw.githubusercontent.com/metamask/eth-contract-metadata/master/images/metamark.svg", } } style={ diff --git a/app/components/UI/BackupAlert/__snapshots__/index.test.js.snap b/app/components/UI/BackupAlert/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..0fae597c298 --- /dev/null +++ b/app/components/UI/BackupAlert/__snapshots__/index.test.js.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BackupAlert should render correctly 1`] = ` + + + + + + + + Protect your funds + + + Tap to save your seed phrase + + + + +`; diff --git a/app/components/UI/BackupAlert/index.js b/app/components/UI/BackupAlert/index.js new file mode 100644 index 00000000000..0d13b90c700 --- /dev/null +++ b/app/components/UI/BackupAlert/index.js @@ -0,0 +1,72 @@ +import React, { Component } from 'react'; +import { Text, StyleSheet, View, TouchableOpacity } from 'react-native'; +import PropTypes from 'prop-types'; +import Icon from 'react-native-vector-icons/MaterialIcons'; +import ElevatedView from 'react-native-elevated-view'; +import { strings } from '../../../../locales/i18n'; +import { colors, fontStyles } from '../../../styles/common'; + +const styles = StyleSheet.create({ + backupAlertWrapper: { + padding: 9, + flexDirection: 'row', + backgroundColor: colors.orange000, + borderWidth: 1, + borderColor: colors.yellow200, + borderRadius: 8 + }, + backupAlertIconWrapper: { + marginRight: 13 + }, + backupAlertIcon: { + fontSize: 22, + color: colors.yellow700 + }, + backupAlertTitle: { + fontSize: 12, + lineHeight: 17, + color: colors.yellow700, + ...fontStyles.bold + }, + backupAlertMessage: { + fontSize: 10, + lineHeight: 14, + color: colors.yellow700, + ...fontStyles.normal + } +}); + +/** + * Component that renders an alert shown when the + * seed phrase hasn't been backed up + */ +export default class BackupAlert extends Component { + static propTypes = { + /** + * The action that will be triggered onPress + */ + onPress: PropTypes.any, + /** + * Styles for the alert + */ + style: PropTypes.any + }; + + render() { + const { onPress, style } = this.props; + + return ( + + + + + + + {strings('browser.backup_alert_title')} + {strings('browser.backup_alert_message')} + + + + ); + } +} diff --git a/app/components/UI/BackupAlert/index.test.js b/app/components/UI/BackupAlert/index.test.js new file mode 100644 index 00000000000..abbe4d0cb0d --- /dev/null +++ b/app/components/UI/BackupAlert/index.test.js @@ -0,0 +1,13 @@ +/* eslint-disable react/jsx-no-bind */ +import React from 'react'; +import { shallow } from 'enzyme'; +import BackupAlert from './'; + +describe('BackupAlert', () => { + it('should render correctly', () => { + const fn = () => null; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/BrowserBottomBar/__snapshots__/index.test.js.snap b/app/components/UI/BrowserBottomBar/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..04251625c7f --- /dev/null +++ b/app/components/UI/BrowserBottomBar/__snapshots__/index.test.js.snap @@ -0,0 +1,188 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BrowserBottomBar should render correctly 1`] = ` + + + + + + + + + + + + + + + + + + + + +`; diff --git a/app/components/UI/BrowserBottomBar/index.js b/app/components/UI/BrowserBottomBar/index.js new file mode 100644 index 00000000000..9ce2783ae49 --- /dev/null +++ b/app/components/UI/BrowserBottomBar/index.js @@ -0,0 +1,136 @@ +import React, { PureComponent } from 'react'; +import { TouchableOpacity, Platform, StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; +import ElevatedView from 'react-native-elevated-view'; +import TabCountIcon from '../Tabs/TabCountIcon'; +import Icon from 'react-native-vector-icons/FontAwesome'; +import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; +import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons'; +import FeatherIcons from 'react-native-vector-icons/Feather'; +import DeviceSize from '../../../util/DeviceSize'; +import { colors } from '../../../styles/common'; + +const HOME_INDICATOR_HEIGHT = 18; + +const styles = StyleSheet.create({ + bottomBar: { + backgroundColor: Platform.OS === 'android' ? colors.white : colors.grey000, + flexDirection: 'row', + paddingHorizontal: 14, + paddingVertical: 18, + paddingBottom: DeviceSize.isIphoneX() && Platform.OS === 'ios' ? 18 + HOME_INDICATOR_HEIGHT : 18, + flex: 0, + borderTopWidth: Platform.OS === 'android' ? 0 : StyleSheet.hairlineWidth, + borderColor: colors.grey200, + justifyContent: 'space-between' + }, + iconButton: { + height: 24, + width: 24, + justifyContent: 'center', + alignItems: 'center', + textAlign: 'center' + }, + tabIcon: { + marginTop: 0, + width: 24, + height: 24 + }, + disabledIcon: { + color: colors.grey100 + }, + icon: { + width: 24, + height: 24, + color: colors.grey500, + textAlign: 'center' + } +}); + +/** + * Browser bottom bar that contains icons for navigatio + * tab management, url change and other options + */ +export default class BrowserBottomBar extends PureComponent { + static propTypes = { + /** + * Boolean that determines if you can navigate back + */ + canGoBack: PropTypes.bool, + /** + * Boolean that determines if you can navigate forward + */ + canGoForward: PropTypes.bool, + /** + * Function that allows you to navigate back + */ + goBack: PropTypes.func, + /** + * Function that allows you to navigate forward + */ + goForward: PropTypes.func, + /** + * Function that triggers the tabs view + */ + showTabs: PropTypes.func, + /** + * Function that triggers the change url modal view + */ + showUrlModal: PropTypes.func, + /** + * Function that redirects to the home screen + */ + goHome: PropTypes.func, + /** + * Function that toggles the options menu + */ + toggleOptions: PropTypes.func + }; + + render() { + const { + canGoBack, + goBack, + canGoForward, + goForward, + showTabs, + goHome, + showUrlModal, + toggleOptions + } = this.props; + + return ( + + + + + + + + + + + + + + + + + + + + + + + ); + } +} diff --git a/app/components/UI/BrowserBottomBar/index.test.js b/app/components/UI/BrowserBottomBar/index.test.js new file mode 100644 index 00000000000..6d8c64a79e9 --- /dev/null +++ b/app/components/UI/BrowserBottomBar/index.test.js @@ -0,0 +1,23 @@ +/* eslint-disable react/jsx-no-bind */ +import React from 'react'; +import { shallow } from 'enzyme'; +import BrowserBottomBar from './'; + +describe('BrowserBottomBar', () => { + it('should render correctly', () => { + const fn = () => null; + + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/CustomGas/index.js b/app/components/UI/CustomGas/index.js index 7ec83d24d64..1350889e707 100644 --- a/app/components/UI/CustomGas/index.js +++ b/app/components/UI/CustomGas/index.js @@ -12,9 +12,10 @@ import { convertApiValueToGWEI } from '../../../util/custom-gas'; import { BN } from 'ethereumjs-util'; -import { fromWei } from '../../../util/number'; +import { fromWei, renderWei } from '../../../util/number'; import Logger from '../../../util/Logger'; import { getTicker } from '../../../util/transactions'; +import { hexToBN } from 'gaba/util'; const AVERAGE_GAS = 20; const LOW_GAS = 10; @@ -208,13 +209,22 @@ class CustomGas extends Component { componentDidMount = async () => { await this.handleFetchBasicEstimates(); - this.onPressGasAverage(); const { ticker } = this.props; if (ticker && ticker !== 'ETH') { this.setState({ advancedCustomGas: true }); } }; + componentDidUpdate = prevProps => { + if (this.state.advancedCustomGas) { + // Handles gas recalculation for custom gas input + const actualGasLimitWei = renderWei(hexToBN(this.props.gas)); + if (renderWei(hexToBN(prevProps.gas)) !== actualGasLimitWei) { + this.setState({ customGasLimit: actualGasLimitWei }); + } + } + }; + handleFetchBasicEstimates = async () => { this.setState({ ready: false }); let basicGasEstimates; @@ -224,7 +234,15 @@ class CustomGas extends Component { Logger.log('Error while trying to get gas limit estimates', error); basicGasEstimates = { average: AVERAGE_GAS, safeLow: LOW_GAS, fast: FAST_GAS }; } - const { average, fast, safeLow } = basicGasEstimates; + + // Handle api failure returning same gas prices + let { average, fast, safeLow } = basicGasEstimates; + if (average === fast && average === safeLow) { + average = AVERAGE_GAS; + safeLow = LOW_GAS; + fast = FAST_GAS; + } + this.setState({ averageGwei: convertApiValueToGWEI(average), fastGwei: convertApiValueToGWEI(fast), diff --git a/app/components/UI/DrawerView/index.js b/app/components/UI/DrawerView/index.js index 599bc8dfc45..f34d811b918 100644 --- a/app/components/UI/DrawerView/index.js +++ b/app/components/UI/DrawerView/index.js @@ -11,6 +11,7 @@ import { ScrollView, InteractionManager } from 'react-native'; +import SvgImage from 'react-native-remote-svg'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import Share from 'react-native-share'; // eslint-disable-line import/default @@ -44,8 +45,10 @@ import DeviceSize from '../../../util/DeviceSize'; import OnboardingWizard from '../OnboardingWizard'; import ReceiveRequest from '../ReceiveRequest'; import Analytics from '../../../core/Analytics'; +import AppConstants from '../../../core/AppConstants'; import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; import URL from 'url-parse'; +import { generateUniversalLinkAddress } from '../../../util/payment-link-generator'; const ANDROID_OFFSET = 30; const styles = StyleSheet.create({ @@ -265,12 +268,9 @@ const styles = StyleSheet.create({ fontSize: 10, ...fontStyles.bold }, - onboardingContainer: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 315 - DeviceSize.getDeviceWidth() + instapayLogo: { + width: 24, + height: 24 } }); @@ -281,6 +281,8 @@ const ICON_IMAGES = { 'selected-wallet': require('../../../images/selected-wallet-icon.png') }; const drawerBg = require('../../../images/drawer-bg.png'); // eslint-disable-line +const instapay_logo_selected = require('../../../images/mm-instapay-selected.png'); // eslint-disable-line +const instapay_logo = require('../../../images/mm-instapay.png'); // eslint-disable-line /** * View component that displays the MetaMask fox @@ -363,7 +365,15 @@ class DrawerView extends Component { /** * Frequent RPC list from PreferencesController */ - frequentRpcList: PropTypes.array + frequentRpcList: PropTypes.array, + /** + /* flag that determines the state of payment channels + */ + paymentChannelsEnabled: PropTypes.bool, + /** + * Current provider type + */ + providerType: PropTypes.string }; state = { @@ -371,6 +381,8 @@ class DrawerView extends Component { showSecureWalletModal: false }; + browserSectionRef = React.createRef(); + currentBalance = null; previousBalance = null; processedNewBalance = false; @@ -483,6 +495,19 @@ class DrawerView extends Component { this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_BROWSER); }; + goToPaymentChannel = () => { + const { providerType } = this.props; + if (AppConstants.CONNEXT.SUPPORTED_NETWORKS.indexOf(providerType) !== -1) { + this.props.navigation.navigate('PaymentChannelView'); + } else { + Alert.alert( + strings('experimental_settings.network_not_supported'), + strings('experimental_settings.switch_network') + ); + } + this.hideDrawer(); + }; + showWallet = () => { this.props.navigation.navigate('WalletTabHome'); this.hideDrawer(); @@ -669,7 +694,8 @@ class DrawerView extends Component { network: { provider: { type, rpcTarget } }, - frequentRpcList + frequentRpcList, + paymentChannelsEnabled } = this.props; let blockExplorer, blockExplorerName; if (type === 'rpc') { @@ -692,6 +718,12 @@ class DrawerView extends Component { action: this.showWallet, routeNames: ['WalletView', 'Asset', 'AddAsset', 'Collectible', 'CollectibleView'] }, + paymentChannelsEnabled && { + name: strings('drawer.insta_pay'), + icon: , + selectedIcon: , + action: this.goToPaymentChannel + }, { name: strings('drawer.transaction_history'), icon: this.getFeatherIcon('list'), @@ -748,7 +780,7 @@ class DrawerView extends Component { onShare = () => { const { selectedAddress } = this.props; Share.open({ - message: `ethereum:${selectedAddress}` + message: generateUniversalLinkAddress(selectedAddress) }).catch(err => { Logger.log('Error while trying to share address', err); }); @@ -776,11 +808,7 @@ class DrawerView extends Component { wizard: { step } } = this.props; return ( - step === 5 && ( - - - - ) + step === 5 && ); }; @@ -879,6 +907,7 @@ class DrawerView extends Component { > {section .filter(item => { + if (!item) return undefined; if (item.name.toLowerCase().indexOf('etherscan') !== -1) { return this.hasBlockExplorer(network.provider.type); } @@ -893,6 +922,7 @@ class DrawerView extends Component { ? styles.selectedRoute : null ]} + ref={item.name === strings('drawer.browser') && this.browserSectionRef} onPress={() => item.action()} // eslint-disable-line > {item.icon @@ -1010,7 +1040,9 @@ const mapStateToProps = state => ({ receiveModalVisible: state.modals.receiveModalVisible, passwordSet: state.user.passwordSet, wizard: state.wizard, - ticker: state.engine.backgroundState.NetworkController.provider.ticker + ticker: state.engine.backgroundState.NetworkController.provider.ticker, + providerType: state.engine.backgroundState.NetworkController.provider.type, + paymentChannelsEnabled: state.settings.paymentChannelsEnabled }); const mapDispatchToProps = dispatch => ({ diff --git a/app/components/UI/EthInput/index.js b/app/components/UI/EthInput/index.js index 8e2629c7e96..d7c1da000af 100644 --- a/app/components/UI/EthInput/index.js +++ b/app/components/UI/EthInput/index.js @@ -4,8 +4,6 @@ import { Keyboard, ScrollView, Platform, StyleSheet, Text, TextInput, View, Imag import { colors, fontStyles } from '../../../styles/common'; import { connect } from 'react-redux'; import { - weiToFiat, - balanceToFiat, fromTokenMinimalUnit, renderFromTokenMinimalUnit, renderFromWei, @@ -14,7 +12,8 @@ import { toWei, fiatNumberToWei, isDecimal, - weiToFiatNumber + weiToFiatNumber, + balanceToFiatNumber } from '../../../util/number'; import { strings } from '../../../../locales/i18n'; import TokenImage from '../TokenImage'; @@ -23,6 +22,7 @@ import ElevatedView from 'react-native-elevated-view'; import CollectibleImage from '../CollectibleImage'; import SelectableAsset from './SelectableAsset'; import { getTicker } from '../../../util/transactions'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; const styles = StyleSheet.create({ root: { @@ -35,11 +35,13 @@ const styles = StyleSheet.create({ paddingVertical: 10, paddingLeft: 14, position: 'relative', - backgroundColor: colors.white, borderColor: colors.grey100, borderRadius: 4, borderWidth: 1 }, + wrapper: { + flexDirection: 'row' + }, input: { ...fontStyles.bold, backgroundColor: colors.white, @@ -53,57 +55,72 @@ const styles = StyleSheet.create({ }, eth: { ...fontStyles.bold, - marginRight: 30, + marginRight: 0, fontSize: 16, - paddingTop: Platform.OS === 'android' ? 3 : 0, + paddingTop: Platform.OS === 'android' ? 1 : 0, paddingLeft: 10, alignSelf: 'center' }, - fiatValue: { + secondaryValue: { + ...fontStyles.normal, + fontSize: 12 + }, + secondaryCurrency: { ...fontStyles.normal, fontSize: 12 }, + secondaryValues: { + flexDirection: 'row', + maxWidth: '70%' + }, split: { - flex: 1, flexDirection: 'row' }, + splitNoSecondaryAmount: { + top: Platform.OS === 'android' ? 5 : 8 + }, ethContainer: { flex: 1, - paddingLeft: 6, - paddingRight: 10 + marginLeft: 6, + marginRight: 10, + maxWidth: '65%' }, icon: { - paddingBottom: 4, - paddingRight: 10, - paddingTop: 6 + paddingVertical: Platform.OS === 'android' ? 8 : 6, + marginRight: 10 }, logo: { width: 22, height: 22, borderRadius: 11 }, - arrow: { - color: colors.grey100, + actions: { position: 'absolute', right: 10, - top: Platform.OS === 'android' ? 20 : 13 + flexDirection: 'row', + top: Platform.OS === 'android' ? 18 : 15 + }, + switch: { + transform: [{ rotate: '270deg' }], + marginVertical: 3, + marginHorizontal: 3 }, - componentContainer: { + scrollContainer: { position: 'relative', - maxHeight: 200, - borderRadius: 4 + maxHeight: 200 }, optionList: { backgroundColor: colors.white, borderColor: colors.grey100, borderRadius: 4, borderWidth: 1, - paddingLeft: 14, - paddingBottom: 12, - width: '100%' + paddingHorizontal: 14, + paddingVertical: 6, + flexGrow: 1 }, selectableAsset: { - paddingTop: 12 + flex: 1, + paddingVertical: 6 } }); @@ -134,10 +151,6 @@ class EthInput extends Component { * Callback triggered when this input value */ onChange: PropTypes.func, - /** - * Makes this input readonly - */ - readonly: PropTypes.bool, /** * Object containing token balances in the format address => balance */ @@ -192,7 +205,13 @@ class EthInput extends Component { ticker: PropTypes.string }; - state = { readableValue: undefined, assets: undefined }; + state = { + readableValue: undefined, + assets: undefined, + secondaryAmount: undefined, + internalPrimaryCurrency: this.props.primaryCurrency, + inputEnabled: Platform.OS === 'ios' + }; /** * Used to 'fillMax' feature. Will process value coming from parent to render corresponding values on input @@ -204,6 +223,12 @@ class EthInput extends Component { this.setState({ readableValue: processedReadableValue }); } this.props.updateFillMax(false); + + // Workaround https://github.com/facebook/react-native/issues/9958 + !this.state.inputEnabled && + setTimeout(() => { + this.setState({ inputEnabled: true }); + }, 100); }; /** @@ -211,14 +236,15 @@ class EthInput extends Component { */ componentDidMount = () => { const { transaction, collectibles } = this.props; - const { processedReadableValue } = this.processValue(transaction.readableValue); + const processedReadableValue = this.processFromValue(transaction.value); switch (transaction.type) { case 'TOKENS_TRANSACTION': this.setState({ assets: [ { name: 'Ether', - symbol: 'ETH' + symbol: 'ETH', + isETH: true }, ...this.props.tokens ], @@ -344,14 +370,11 @@ class EthInput extends Component { }; const assetsList = assetsLists[assetType](); return ( - - + + - {assetsList.map(asset => ( - + {assetsList.map((asset, i) => ( + {this.renderAsset(asset, async () => { await this.selectAsset(asset); })} @@ -366,43 +389,50 @@ class EthInput extends Component { /** * Handle value from eth input according to app 'primaryCurrency', transforming either Token or Fiat value to corresponding transaction object value. * + * @param {String} readableValue - String containing the tx value in readable format + * @param {String} internalPrimaryCurrency - If provided, represents internal primary currency * @returns {Object} - Object containing BN instance of the value for the transaction and a string containing readable value + * @returns {String} - String containing internalPrimaryCurrency, if not provided will take it from state */ - processValue = value => { + processValue = (readableValue, internalPrimaryCurrency) => { const { transaction: { selectedAsset, assetType }, conversionRate, - primaryCurrency, contractExchangeRates } = this.props; + internalPrimaryCurrency = internalPrimaryCurrency || this.state.internalPrimaryCurrency; let processedValue, processedReadableValue; - const decimal = isDecimal(value); + const decimal = isDecimal(readableValue); if (decimal) { // Only for ETH or ERC20, depending on 'primaryCurrency' selected switch (assetType) { case 'ETH': - if (primaryCurrency === 'ETH') { - processedValue = toWei(value); - processedReadableValue = value; + if (internalPrimaryCurrency === 'ETH') { + processedValue = toWei(readableValue); + processedReadableValue = readableValue; } else { - processedValue = fiatNumberToWei(value, conversionRate); - processedReadableValue = weiToFiatNumber(toWei(value), conversionRate).toString(); + processedValue = fiatNumberToWei(readableValue, conversionRate); + processedReadableValue = weiToFiatNumber(toWei(readableValue), conversionRate).toString(); } break; case 'ERC20': { const exchangeRate = selectedAsset && selectedAsset.address && contractExchangeRates[selectedAsset.address]; - if (primaryCurrency !== 'ETH' && (exchangeRate && exchangeRate !== 0)) { + if (internalPrimaryCurrency !== 'ETH' && (exchangeRate && exchangeRate !== 0)) { processedValue = fiatNumberToTokenMinimalUnit( - value, + readableValue, conversionRate, exchangeRate, selectedAsset.decimals ); - processedReadableValue = balanceToFiat(value, conversionRate, exchangeRate, ''); + processedReadableValue = balanceToFiatNumber( + readableValue, + conversionRate, + exchangeRate + ).toString(); } else { - processedValue = toTokenMinimalUnit(value, selectedAsset.decimals); - processedReadableValue = value; + processedValue = toTokenMinimalUnit(readableValue, selectedAsset.decimals); + processedReadableValue = readableValue; } } } @@ -410,6 +440,43 @@ class EthInput extends Component { return { processedValue, processedReadableValue }; }; + /** + * Handle generation of readable information for the component from a transaction value + * + * @param {Object} value - Transaction value + * @returns {String} - Readable transaction value depending on primaryCurrency + */ + processFromValue = value => { + if (!value) return undefined; + const { + transaction: { selectedAsset, assetType }, + conversionRate, + contractExchangeRates + } = this.props; + const { internalPrimaryCurrency } = this.state; + let processedReadableValue; + // Only for ETH or ERC20, depending on 'primaryCurrency' selected + switch (assetType) { + case 'ETH': + if (internalPrimaryCurrency === 'ETH') { + processedReadableValue = renderFromWei(value); + } else { + processedReadableValue = weiToFiatNumber(value, conversionRate).toString(); + } + break; + case 'ERC20': { + const exchangeRate = + selectedAsset && selectedAsset.address && contractExchangeRates[selectedAsset.address]; + if (internalPrimaryCurrency !== 'ETH' && (exchangeRate && exchangeRate !== 0)) { + processedReadableValue = balanceToFiatNumber(value, conversionRate, exchangeRate).toString(); + } else { + processedReadableValue = renderFromTokenMinimalUnit(value, selectedAsset.decimals); + } + } + } + return processedReadableValue; + }; + /** * On value change, callback to props 'onChange' and update 'readableValue' */ @@ -425,21 +492,25 @@ class EthInput extends Component { * * @param {object} image - Image object of the asset * @param {tsring} currency - String containing currency code - * @param {string} conversionRate - String containing amount depending on primary currency + * @param {string} secondaryAmount - String containing amount depending on primary currency + * @param {string} secondaryCurrency - String containing currency code * @returns {object} - View object to render as input field */ - renderTokenInput = (image, currency, convertedAmount) => { - const { readonly } = this.props; - const { readableValue } = this.state; + renderTokenInput = (image, currency, secondaryAmount, secondaryCurrency) => { + const { + transaction: { paymentChannelTransaction } + } = this.props; + const { readableValue, assets } = this.state; + const selectAssets = assets && assets.length > 1; return ( {image} - + - {convertedAmount && ( - - {convertedAmount} - + {secondaryAmount !== undefined && ( + + + {secondaryAmount} + + + {' '} + {secondaryCurrency} + + + )} + + + {!paymentChannelTransaction && secondaryCurrency && ( + this.switchInternalPrimaryCurrency(secondaryAmount)} // eslint-disable-line react/jsx-no-bind + name="exchange" + size={18} + color={colors.grey100} + style={styles.switch} + /> + )} + {!paymentChannelTransaction && selectAssets && ( + )} @@ -473,73 +569,108 @@ class EthInput extends Component { contractExchangeRates, conversionRate, transaction: { assetType, selectedAsset, value }, - primaryCurrency, ticker } = this.props; - // Depending on 'assetType' return object with corresponding 'convertedAmount', 'currency' and 'image' + const { internalPrimaryCurrency } = this.state; + + // Depending on 'assetType' return object with corresponding 'secondaryAmount', 'currency' and 'image' const inputs = { ETH: () => { - let convertedAmount, currency; - if (primaryCurrency === 'ETH') { - convertedAmount = weiToFiat(value, conversionRate, currentCurrency.toUpperCase()); + let secondaryAmount, currency, secondaryCurrency; + if (internalPrimaryCurrency === 'ETH') { + secondaryAmount = weiToFiatNumber(value, conversionRate).toString(); + secondaryCurrency = currentCurrency.toUpperCase(); currency = getTicker(ticker); } else { - convertedAmount = renderFromWei(value) + ' ' + getTicker(ticker); + secondaryAmount = renderFromWei(value); + secondaryCurrency = getTicker(ticker); currency = currentCurrency.toUpperCase(); } const image = ; - return this.renderTokenInput(image, currency, convertedAmount); + return this.renderTokenInput(image, currency, secondaryAmount, secondaryCurrency); }, ERC20: () => { const exchangeRate = selectedAsset && selectedAsset.address && contractExchangeRates[selectedAsset.address]; - let convertedAmount, currency; + let secondaryAmount, currency, secondaryCurrency; if (exchangeRate && exchangeRate !== 0) { - if (primaryCurrency === 'ETH') { + if (internalPrimaryCurrency === 'ETH') { const finalValue = (value && fromTokenMinimalUnit(value, selectedAsset.decimals)) || 0; - convertedAmount = balanceToFiat(finalValue, conversionRate, exchangeRate, currentCurrency); + secondaryAmount = balanceToFiatNumber(finalValue, conversionRate, exchangeRate).toString(); currency = selectedAsset.symbol; + secondaryCurrency = currentCurrency.toUpperCase(); } else { const finalValue = (value && renderFromTokenMinimalUnit(value, selectedAsset.decimals)) || 0; - convertedAmount = finalValue + ' ' + selectedAsset.symbol; + secondaryAmount = finalValue.toString(); currency = currentCurrency.toUpperCase(); + secondaryCurrency = selectedAsset.symbol; } } else { - convertedAmount = strings('transaction.conversion_not_available'); + currency = + internalPrimaryCurrency === 'ETH' + ? selectedAsset.symbol + : currentCurrency && currentCurrency.toUpperCase(); } + const image = ; - return this.renderTokenInput(image, currency, convertedAmount); + return this.renderTokenInput(image, currency, secondaryAmount, secondaryCurrency); }, - ERC721: () => ( - - - } - asset={selectedAsset} - /> - - ) + ERC721: () => { + const { assets } = this.state; + const selectAssets = assets && assets.length > 1; + return ( + + + } + asset={selectedAsset} + /> + + {selectAssets && ( + + )} + + + ); + } }; return assetType && inputs[assetType](); }; + /** + * Handle change of primary currency + */ + switchInternalPrimaryCurrency = secondaryAmount => { + const { internalPrimaryCurrency } = this.state; + const primarycurrencies = { + ETH: 'Fiat', + Fiat: 'ETH' + }; + this.setState({ + readableValue: secondaryAmount, + internalPrimaryCurrency: primarycurrencies[internalPrimaryCurrency] + }); + }; + render = () => { const { assets } = this.state; const { isOpen } = this.props; const selectAssets = assets && assets.length > 1; return ( - {this.renderInput()} - {selectAssets && ( - - )} + {this.renderInput()} {selectAssets && isOpen && this.renderAssetsList()} ); diff --git a/app/components/UI/HomePage/__snapshots__/index.test.js.snap b/app/components/UI/HomePage/__snapshots__/index.test.js.snap deleted file mode 100644 index ce0585f079d..00000000000 --- a/app/components/UI/HomePage/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,258 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`HomePage should render correctly 1`] = ` - - - - - - - - - - - - - - - - - | - - - DAPP BROWSER - - - - - - Welcome! - - - MetaMask is your wallet and browser for the decentralized web. Have a look around! - - - - - - - - - - - -`; diff --git a/app/components/UI/HomePage/index.js b/app/components/UI/HomePage/index.js deleted file mode 100644 index fa6861702cd..00000000000 --- a/app/components/UI/HomePage/index.js +++ /dev/null @@ -1,451 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { - Keyboard, - TouchableWithoutFeedback, - Image, - TouchableOpacity, - Text, - Platform, - StyleSheet, - TextInput, - View, - ScrollView -} from 'react-native'; -import { colors, fontStyles } from '../../../styles/common'; -import { strings } from '../../../../locales/i18n'; -import ElevatedView from 'react-native-elevated-view'; -import Icon from 'react-native-vector-icons/MaterialIcons'; -import FeatherIcon from 'react-native-vector-icons/Feather'; -import DeviceSize from '../../../util/DeviceSize'; -import ScrollableTabView from 'react-native-scrollable-tab-view'; -import DefaultTabBar from 'react-native-scrollable-tab-view/DefaultTabBar'; -import BrowserFeatured from '../BrowserFeatured'; -import BrowserFavorites from '../BrowserFavorites'; -import UrlAutocomplete from '../UrlAutocomplete'; -import onUrlSubmit from '../../../util/browser'; -import { removeBookmark } from '../../../actions/bookmarks'; -import Analytics from '../../../core/Analytics'; -import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; - -const foxImage = require('../../../images/fox.png'); // eslint-disable-line import/no-commonjs -const NAVBAR_HEIGHT = 50; - -const styles = StyleSheet.create({ - flex: { - flex: 1, - backgroundColor: colors.beige - }, - - homePageContent: { - marginBottom: 43, - paddingHorizontal: 18 - }, - paddingBottom: { - paddingBottom: Platform.OS === 'ios' ? 75 : 0 - }, - foxWrapper: { - height: 20 - }, - topBarWrapper: { - flexDirection: 'row' - }, - titleWrapper: { - flex: 1, - flexDirection: 'row', - marginLeft: 8, - marginTop: 2 - }, - image: { - width: 22, - height: 22 - }, - startPageContent: { - alignItems: 'flex-start' - }, - startPageTitle: { - fontSize: Platform.OS === 'android' ? 30 : 35, - marginTop: 20, - marginBottom: 8, - color: colors.fontPrimary, - justifyContent: 'center', - textAlign: 'center', - ...fontStyles.bold - }, - title: { - fontSize: 12, - top: 1, - ...fontStyles.light - }, - separator: { - fontSize: 16, - top: -2, - ...fontStyles.normal - }, - startPageSubtitle: { - fontSize: Platform.OS === 'android' ? 14 : 16, - color: colors.fontPrimary, - ...fontStyles.normal - }, - searchWrapper: { - marginTop: 12, - marginBottom: 24, - marginHorizontal: 16 - }, - searchInput: { - flex: 1, - marginHorizontal: 0, - paddingTop: Platform.OS === 'android' ? 12 : 2, - borderRadius: 20, - paddingHorizontal: 38, - fontSize: 16, - backgroundColor: colors.grey000, - height: 40, - color: colors.grey400, - ...fontStyles.normal - }, - searchIcon: { - position: 'absolute', - textAlignVertical: 'center', - marginTop: Platform.OS === 'android' ? 9 : 10, - marginLeft: 12 - }, - backupAlert: { - position: 'absolute', - bottom: DeviceSize.isIphoneX() ? 30 : 20, - left: 16, - right: 16 - }, - backupAlertWrapper: { - padding: 9, - flexDirection: 'row', - backgroundColor: colors.orange000, - borderWidth: 1, - borderColor: colors.yellow200, - borderRadius: 8 - }, - backupAlertIconWrapper: { - marginRight: 13 - }, - backupAlertIcon: { - fontSize: 22, - color: colors.yellow700 - }, - backupAlertTitle: { - fontSize: 12, - lineHeight: 17, - color: colors.yellow700, - ...fontStyles.bold - }, - backupAlertMessage: { - fontSize: 10, - lineHeight: 14, - color: colors.yellow700, - ...fontStyles.normal - }, - tabUnderlineStyle: { - height: 2, - backgroundColor: colors.blue - }, - tabStyle: { - paddingHorizontal: 0 - }, - textStyle: { - fontSize: 12, - letterSpacing: 0.5, - ...fontStyles.bold - }, - metamaskName: { - width: 90, - height: 16 - }, - urlAutocomplete: { - position: 'absolute', - marginTop: 62, - backgroundColor: colors.white, - width: '100%', - height: '100%' - } -}); - -const metamask_name = require('../../../images/metamask-name.png'); // eslint-disable-line - -/** - * Main view component for the Lock screen - */ -class HomePage extends Component { - static propTypes = { - /** - * react-navigation object used to switch between screens - */ - navigation: PropTypes.object, - /** - * Function to be called when tapping on a bookmark item - */ - goTo: PropTypes.any, - /** - * function to be called when submitting the text input field - */ - onInitialUrlSubmit: PropTypes.any, - /** - * redux flag that indicates if the user set a password - */ - passwordSet: PropTypes.bool, - /** - * redux flag that indicates if the user - * completed the seed phrase backup flow - */ - seedphraseBackedUp: PropTypes.bool, - /** - * Array containing all the bookmark items - */ - bookmarks: PropTypes.array, - /** - * function that removes a bookmark - */ - removeBookmark: PropTypes.func, - /** - * Default protocol of the browser, for ex. https - */ - defaultProtocol: PropTypes.string, - /** - * Default search engine - */ - searchEngine: PropTypes.string - }; - - state = { - searchInputValue: '', - inputValue: '', - inputWidth: Platform.OS === 'android' ? '99%' : undefined, - tabViewStyle: null - }; - - searchInput = React.createRef(); - scrollView = React.createRef(); - - actionSheet = null; - - handleTabHeight(obj) { - const refName = obj.ref.ref; - if (refName === 'featuredTab') { - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.BROWSER_FEATURED_APPS); - } else { - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.BROWSER_FAVORITES); - } - setTimeout(() => { - // eslint-disable-next-line - this.refs[refName].measureMyself((x, y, w, h, l, t) => { - if (h !== 0) { - this.setState({ tabViewStyle: { height: h + NAVBAR_HEIGHT } }); - } - }); - }, 100); - } - - bookmarkIndexToRemove = null; - - componentDidMount = () => { - if (Platform.OS === 'android') { - this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide); - } - this.mounted = true; - // Workaround https://github.com/facebook/react-native/issues/9958 - this.state.inputWidth && - setTimeout(() => { - this.mounted && this.setState({ inputWidth: '100%' }); - }, 100); - }; - - componentWillUnmount() { - Platform.OS === 'android' && this.keyboardDidHideListener.remove(); - this.mounted = false; - } - - onInitialUrlChange = searchInputValue => { - this.setState({ searchInputValue }); - }; - - onInitialUrlSubmit = () => { - Analytics.trackEvent(ANALYTICS_EVENT_OPTS.BROWSER_SEARCH); - this.props.onInitialUrlSubmit(this.state.searchInputValue); - this.setState({ searchInputValue: '' }); - }; - - backupAlertPress = () => { - this.props.navigation.navigate('AccountBackupStep1'); - }; - - renderTabBar() { - return ( - - ); - } - onUrlInputSubmit = async (input = null) => { - this.searchInput && this.searchInput.current && this.searchInput.current.blur(); - const inputValue = (typeof input === 'string' && input) || this.state.inputValue; - const { defaultProtocol, searchEngine } = this.props; - const sanitizedInput = onUrlSubmit(inputValue, searchEngine, defaultProtocol); - if (sanitizedInput) { - await this.props.goTo(sanitizedInput); - } else { - this.onInitialUrlSubmit(input); - } - this.mounted && this.setState({ inputValue: '' }); - }; - - onAutocomplete = link => { - this.setState({ inputValue: link, searchInputValue: '' }, () => { - this.onUrlInputSubmit(link); - }); - }; - - dismissKeyboardAndClear = () => { - this.mounted && this.setState({ searchInputValue: '' }); - this.searchInput && this.searchInput.current && this.searchInput.current.blur(); - }; - - keyboardDidHide = () => { - this.mounted && this.setState({ searchInputValue: '' }); - }; - - focusInput = () => { - this.searchInput && this.searchInput.current && this.searchInput.current.focus(); - }; - - render() { - return ( - - - - - - - - - - - - - - - - - | - {strings('browser.dapp_browser')} - - - - - {strings('browser.welcome')} - - {strings('browser.dapp_browser_message')} - - - - - this.handleTabHeight(obj)} - style={this.state.tabViewStyle} - > - - - - - - - {this.state.searchInputValue.length > 1 && ( - - - - )} - {this.props.passwordSet && !this.props.seedphraseBackedUp && ( - - - - - - - {strings('home_page.backup_alert_title')} - - {strings('home_page.backup_alert_message')} - - - - - )} - - ); - } -} - -const mapStateToProps = state => ({ - seedphraseBackedUp: state.user.seedphraseBackedUp, - passwordSet: state.user.passwordSet, - bookmarks: state.bookmarks -}); - -const mapDispatchToProps = dispatch => ({ - removeBookmark: bookmark => dispatch(removeBookmark(bookmark)) -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(HomePage); diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index 2b9096c729f..c6cae1d5350 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -3,8 +3,17 @@ import NavbarTitle from '../NavbarTitle'; import ModalNavbarTitle from '../ModalNavbarTitle'; import AccountRightButton from '../AccountRightButton'; import NavbarBrowserTitle from '../NavbarBrowserTitle'; -import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; -import { Text, Platform, TouchableOpacity, View, StyleSheet, Image, Keyboard, InteractionManager } from 'react-native'; +import { + Alert, + Text, + Platform, + TouchableOpacity, + View, + StyleSheet, + Image, + Keyboard, + InteractionManager +} from 'react-native'; import { fontStyles, colors } from '../../../styles/common'; import IonicIcon from 'react-native-vector-icons/Ionicons'; import AntIcon from 'react-native-vector-icons/AntDesign'; @@ -13,11 +22,14 @@ import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIc import URL from 'url-parse'; import { strings } from '../../../../locales/i18n'; import AppConstants from '../../../core/AppConstants'; -import TabCountIcon from '../../UI/Tabs/TabCountIcon'; -import WalletConnect from '../../../core/WalletConnect'; +import DeeplinkManager from '../../../core/DeeplinkManager'; import Analytics from '../../../core/Analytics'; import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; -const HOMEPAGE_URL = 'about:blank'; +import { importAccountFromPrivateKey } from '../../../util/address'; +import { isGatewayUrl } from '../../../lib/ens-ipfs/resolver'; +import { getHost } from '../../../util/browser'; + +const { HOMEPAGE_URL } = AppConstants; const trackEvent = event => { InteractionManager.runAfterInteractions(() => { @@ -73,7 +85,6 @@ const styles = StyleSheet.create({ color: colors.blue }, moreIcon: { - marginRight: 15, marginTop: 5 }, flex: { @@ -92,16 +103,14 @@ const styles = StyleSheet.create({ alignItems: 'center', flex: 1 }, - browserRightButtonAndroid: { - flex: 1, - width: 95, - flexDirection: 'row' - }, browserRightButton: { - flex: 1 + flex: 1, + marginRight: Platform.OS === 'android' ? 10 : 0 }, - browserMoreIconAndroid: { - paddingTop: 10 + tabIconAndroidWrapper: { + alignItems: 'center', + width: 36, + marginLeft: 5 }, disabled: { opacity: 0.3 @@ -113,7 +122,7 @@ const styles = StyleSheet.create({ }, tabIconAndroid: { marginTop: 13, - marginLeft: -10, + marginLeft: 0, marginRight: 3, width: 24, height: 24 @@ -272,6 +281,29 @@ export function getTransactionOptionsTitle(title, navigation) { headerRight: }; } + +/** + * Function that returns the navigation options for InstaPay screend + * + * @param {string} title - Title name to use with strings + * @returns {Object} - Corresponding navbar options containing title and headerTitleStyle + */ +export function getInstaPayNavigations(title, navigation) { + return { + headerTitle: , + headerLeft: ( + // eslint-disable-next-line react/jsx-no-bind + navigation.pop()} style={styles.backButton}> + + + ), + headerRight: + }; +} /** * Function that returns the navigation options * This is used by views that will show our custom navbar @@ -284,11 +316,14 @@ export function getBrowserViewNavbarOptions(navigation) { const url = navigation.getParam('url', ''); let hostname = null; let isHttps = false; - if (url && url !== HOMEPAGE_URL) { - isHttps = url.toLowerCase().substr(0, 6) === 'https:'; + + const isHomepage = url => getHost(url) === getHost(HOMEPAGE_URL); + + if (url && !isHomepage(url)) { + isHttps = url && url.toLowerCase().substr(0, 6) === 'https:'; const urlObj = new URL(url); hostname = urlObj.hostname.toLowerCase().replace('www.', ''); - if (hostname === 'ipfs.io' && url.search(`${AppConstants.IPFS_OVERRIDE_PARAM}=false`) === -1) { + if (isGatewayUrl(urlObj) && url.search(`${AppConstants.IPFS_OVERRIDE_PARAM}=false`) === -1) { const ensUrl = navigation.getParam('currentEnsName', ''); if (ensUrl) { hostname = ensUrl.toLowerCase().replace('www.', ''); @@ -304,8 +339,6 @@ export function getBrowserViewNavbarOptions(navigation) { trackEvent(ANALYTICS_EVENT_OPTS.COMMON_TAPS_HAMBURGER_MENU); } - const optionsDisabled = hostname === strings('browser.title'); - return { headerLeft: ( @@ -318,29 +351,8 @@ export function getBrowserViewNavbarOptions(navigation) { ), headerTitle: , headerRight: ( - + - {Platform.OS === 'android' ? ( - - { - navigation.navigate('BrowserView', { ...navigation.state.params, showTabs: true }); - }} - style={styles.tabIconAndroid} - /> - { - navigation.navigate('BrowserView', { ...navigation.state.params, showOptions: true }); - }} - style={[styles.browserMoreIconAndroid, optionsDisabled ? styles.disabled : null]} - disabled={optionsDisabled} - > - - - - ) : null} ) }; @@ -438,6 +450,37 @@ export function getClosableNavigationOptions(title, backButtonText, navigation) }; } +/** + * Function that returns the navigation options + * for our closable screens, + * + * @returns {Object} - Corresponding navbar options containing headerTitle, headerTitle and headerTitle + */ +export function getOfflineModalNavbar(navigation) { + return { + headerStyle: { + shadowColor: colors.transparent, + elevation: 0, + backgroundColor: colors.white, + borderBottomWidth: 0 + }, + headerLeft: + Platform.OS === 'android' ? ( + // eslint-disable-next-line react/jsx-no-bind + navigation.pop()} style={styles.backButton}> + + + ) : null, + headerRight: + Platform.OS === 'ios' ? ( + // eslint-disable-next-line react/jsx-no-bind + navigation.pop()} style={styles.backButton}> + + + ) : null + }; +} + /** * Function that returns the navigation options * for our wallet screen, @@ -448,8 +491,37 @@ export function getWalletNavbarOptions(title, navigation) { const onScanSuccess = data => { if (data.target_address) { navigation.navigate('SendView', { txMeta: data }); + } else if (data.private_key) { + Alert.alert( + strings('wallet.private_key_detected'), + strings('wallet.do_you_want_to_import_this_account'), + [ + { + text: strings('wallet.cancel'), + onPress: () => false, + style: 'cancel' + }, + { + text: strings('wallet.yes'), + onPress: async () => { + try { + await importAccountFromPrivateKey(data.private_key); + navigation.navigate('ImportPrivateKeySuccess'); + } catch (e) { + Alert.alert( + strings('import_private_key.error_title'), + strings('import_private_key.error_message') + ); + } + } + } + ], + { cancelable: false } + ); } else if (data.walletConnectURI) { - WalletConnect.newSession(data.walletConnectURI); + setTimeout(() => { + DeeplinkManager.parse(data.walletConnectURI); + }, 500); } }; diff --git a/app/components/UI/NavbarBrowserTitle/index.js b/app/components/UI/NavbarBrowserTitle/index.js index 32de73f0b8b..65f17caf447 100644 --- a/app/components/UI/NavbarBrowserTitle/index.js +++ b/app/components/UI/NavbarBrowserTitle/index.js @@ -5,8 +5,6 @@ import { Platform, TouchableOpacity, View, StyleSheet, Text } from 'react-native import { colors, fontStyles } from '../../../styles/common'; import Networks from '../../../util/networks'; import Icon from 'react-native-vector-icons/FontAwesome'; -import { toggleNetworkModal } from '../../../actions/modals'; -import { strings } from '../../../../locales/i18n'; const styles = StyleSheet.create({ wrapper: { @@ -75,23 +73,15 @@ class NavbarBrowserTitle extends Component { /** * Boolean that specifies if it is a secure website */ - https: PropTypes.bool, - /** - * Action that toggles the network modal - */ - toggleNetworkModal: PropTypes.func + https: PropTypes.bool }; onTitlePress = () => { - if (this.props.hostname === strings('browser.title')) { - this.props.toggleNetworkModal(); - } else { - this.props.navigation.setParams({ - ...this.props.navigation.state.params, - url: this.props.url, - showUrlModal: true - }); - } + this.props.navigation.setParams({ + ...this.props.navigation.state.params, + url: this.props.url, + showUrlModal: true + }); }; render = () => { @@ -127,10 +117,5 @@ class NavbarBrowserTitle extends Component { } const mapStateToProps = state => ({ network: state.engine.backgroundState.NetworkController }); -const mapDispatchToProps = dispatch => ({ - toggleNetworkModal: () => dispatch(toggleNetworkModal()) -}); -export default connect( - mapStateToProps, - mapDispatchToProps -)(NavbarBrowserTitle); + +export default connect(mapStateToProps)(NavbarBrowserTitle); diff --git a/app/components/UI/NetworkList/__snapshots__/index.test.js.snap b/app/components/UI/NetworkList/__snapshots__/index.test.js.snap index d895ce82243..1703a165a4e 100644 --- a/app/components/UI/NetworkList/__snapshots__/index.test.js.snap +++ b/app/components/UI/NetworkList/__snapshots__/index.test.js.snap @@ -360,7 +360,7 @@ exports[`NetworkList should render correctly 1`] = ` + + - + + Goerli Test Network + + + + { - const { PreferencesController } = Engine.context; - PreferencesController.removeFromFrequentRpcList(rpcTarget); - }; - networkElement = (selected, onPress, name, color, i, network) => ( { const { frequentRpcList, provider } = this.props; - return frequentRpcList.map(({ rpcUrl }, i) => { - const { color, name } = { name: rpcUrl, color: null }; + return frequentRpcList.map(({ nickname, rpcUrl }, i) => { + const { color, name } = { name: nickname || rpcUrl, color: null }; const selected = provider.rpcTarget === rpcUrl && provider.type === 'rpc' ? ( - ) : ( - this.removeRpcTarget(rpcUrl)} // eslint-disable-line - /> - ); + ) : null; return this.networkElement(selected, this.onSetRpcTarget, name, color, i, rpcUrl); }); }; diff --git a/app/components/UI/OnboardingWizard/Coachmark/__snapshots__/index.test.js.snap b/app/components/UI/OnboardingWizard/Coachmark/__snapshots__/index.test.js.snap index af375ef9f2b..7b3e03225d3 100644 --- a/app/components/UI/OnboardingWizard/Coachmark/__snapshots__/index.test.js.snap +++ b/app/components/UI/OnboardingWizard/Coachmark/__snapshots__/index.test.js.snap @@ -15,6 +15,7 @@ exports[`Coachmark should render correctly 1`] = ` style={ Object { "alignItems": "flex-start", + "bottom": -2, "marginBottom": 10, "marginLeft": 30, } @@ -25,7 +26,7 @@ exports[`Coachmark should render correctly 1`] = ` Object { "backgroundColor": "transparent", "borderBottomColor": "#037dd6", - "borderBottomWidth": 10, + "borderBottomWidth": 12, "borderLeftColor": "transparent", "borderLeftWidth": 15, "borderRightColor": "transparent", @@ -116,11 +117,11 @@ exports[`Coachmark should render correctly 1`] = ` Array [ Object { "backgroundColor": "#FFFFFF", - "borderRadius": 3.5, - "height": 7, - "margin": 5, + "borderRadius": 3, + "height": 6, + "margin": 3, "opacity": 0.4, - "width": 7, + "width": 6, }, Object { "opacity": 1, @@ -134,11 +135,11 @@ exports[`Coachmark should render correctly 1`] = ` Array [ Object { "backgroundColor": "#FFFFFF", - "borderRadius": 3.5, - "height": 7, - "margin": 5, + "borderRadius": 3, + "height": 6, + "margin": 3, "opacity": 0.4, - "width": 7, + "width": 6, }, Object {}, ] @@ -150,11 +151,11 @@ exports[`Coachmark should render correctly 1`] = ` Array [ Object { "backgroundColor": "#FFFFFF", - "borderRadius": 3.5, - "height": 7, - "margin": 5, + "borderRadius": 3, + "height": 6, + "margin": 3, "opacity": 0.4, - "width": 7, + "width": 6, }, Object {}, ] @@ -166,11 +167,11 @@ exports[`Coachmark should render correctly 1`] = ` Array [ Object { "backgroundColor": "#FFFFFF", - "borderRadius": 3.5, - "height": 7, - "margin": 5, + "borderRadius": 3, + "height": 6, + "margin": 3, "opacity": 0.4, - "width": 7, + "width": 6, }, Object {}, ] @@ -182,11 +183,27 @@ exports[`Coachmark should render correctly 1`] = ` Array [ Object { "backgroundColor": "#FFFFFF", - "borderRadius": 3.5, - "height": 7, - "margin": 5, + "borderRadius": 3, + "height": 6, + "margin": 3, "opacity": 0.4, - "width": 7, + "width": 6, + }, + Object {}, + ] + } + /> + { Animated.timing(this.opacity, { toValue: 1, - duration: 1000, + duration: 500, useNativeDriver: true, isInteraction: false }).start(); @@ -167,7 +172,7 @@ export default class Coachmark extends Component { componentWillUnmount = () => { Animated.timing(this.opacity, { toValue: 0, - duration: 1000, + duration: 500, useNativeDriver: true, isInteraction: false }).start(); @@ -237,7 +242,7 @@ export default class Coachmark extends Component { {strings('onboarding_wizard.coachmark.progress_back')} - {[1, 2, 3, 4, 5].map(i => ( + {[1, 2, 3, 4, 5, 6].map(i => ( ))} diff --git a/app/components/UI/OnboardingWizard/Step2/index.js b/app/components/UI/OnboardingWizard/Step2/index.js index 7ea47d722ad..17fb5c8f2ad 100644 --- a/app/components/UI/OnboardingWizard/Step2/index.js +++ b/app/components/UI/OnboardingWizard/Step2/index.js @@ -1,12 +1,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { Platform, View, Text, StyleSheet } from 'react-native'; +import { View, Text, StyleSheet } from 'react-native'; import Coachmark from '../Coachmark'; import setOnboardingWizardStep from '../../../../actions/wizard'; import { strings } from '../../../../../locales/i18n'; import onboardingStyles from './../styles'; +const INDICATOR_HEIGHT = 10; const styles = StyleSheet.create({ main: { flex: 1 @@ -18,8 +19,7 @@ const styles = StyleSheet.create({ flex: 1, position: 'absolute', left: 0, - right: 0, - top: Platform.OS === 'ios' ? 290 : 250 + right: 0 } }); @@ -28,7 +28,30 @@ class Step2 extends Component { /** * Dispatch set onboarding wizard step */ - setOnboardingWizardStep: PropTypes.func + setOnboardingWizardStep: PropTypes.func, + /** + * Coachmark ref to get position + */ + coachmarkRef: PropTypes.object + }; + + state = { + coachmarkTop: 0 + }; + + componentDidMount = () => { + this.getPosition(this.props.coachmarkRef.mainView); + }; + + /** + * If component ref defined, calculate its position and position coachmark accordingly + */ + getPosition = ref => { + ref && + ref.current && + ref.current.measure((fx, fy, width, height, px, py) => { + this.setState({ coachmarkTop: py + height - INDICATOR_HEIGHT }); + }); }; /** @@ -60,7 +83,7 @@ class Step2 extends Component { render() { return ( - + - - - - - - - 'Account 1' isn't that catchy. So why not name your account something a little more memorable. - - - - Long tap - - - now to edit account name. - - - } - currentStep={2} - onBack={[Function]} - onNext={[Function]} - style={ - Object { - "marginHorizontal": 45, - } - } - title="Edit Account Name" - topIndicatorPosition="topCenter" - /> - - -`; +exports[`Step3 should render correctly 1`] = `""`; diff --git a/app/components/UI/OnboardingWizard/Step3/index.js b/app/components/UI/OnboardingWizard/Step3/index.js index 15726becc64..f8534e1d386 100644 --- a/app/components/UI/OnboardingWizard/Step3/index.js +++ b/app/components/UI/OnboardingWizard/Step3/index.js @@ -5,29 +5,27 @@ import { Platform, Text, View, StyleSheet } from 'react-native'; import Coachmark from '../Coachmark'; import setOnboardingWizardStep from '../../../../actions/wizard'; import { colors, fontStyles } from '../../../../styles/common'; -import { renderAccountName } from '../../../../util/address'; import AccountOverview from '../../AccountOverview'; import { strings } from '../../../../../locales/i18n'; import onboardingStyles from './../styles'; const styles = StyleSheet.create({ main: { - flex: 1 + flex: 1, + position: 'absolute' }, some: { marginHorizontal: 45 }, coachmarkContainer: { flex: 1, - position: 'absolute', left: 0, - right: 0, - top: Platform.OS === 'ios' ? 210 : 180 + right: 0 }, accountLabelContainer: { + flex: 1, alignItems: 'center', - marginTop: Platform.OS === 'ios' ? 88 : 57, - backgroundColor: colors.white + backgroundColor: colors.transparent } }); @@ -52,21 +50,56 @@ class Step3 extends Component { /** * Dispatch set onboarding wizard step */ - setOnboardingWizardStep: PropTypes.func + setOnboardingWizardStep: PropTypes.func, + /** + * Coachmark ref to get position + */ + coachmarkRef: PropTypes.object }; state = { - accountLabel: '', - accountLabelEditable: false + coachmarkTop: 0, + viewTop: 0, + coachmarkTopReady: false, + viewTopReady: false }; /** * Sets corresponding account label */ componentDidMount = () => { - const { identities, selectedAddress } = this.props; - const accountLabel = renderAccountName(selectedAddress, identities); - this.setState({ accountLabel }); + this.getViewPosition(this.props.coachmarkRef.scrollViewContainer); + this.getCoachmarkPosition(this.props.coachmarkRef.editableLabelRef); + }; + + /** + * Sets coachmark top position getting AccountOverview component ref from Wallet + */ + getCoachmarkPosition = ref => { + ref && + ref.current && + ref.current.measure((fx, fy, width, height) => { + this.setState({ + coachmarkTop: 2 * height, + coachmarkTopReady: true + }); + }); + }; + + /** + * Sets view top position getting accountOverview component ref from Wallet + */ + getViewPosition = ref => { + ref && + ref.current && + ref.current.measure((fx, fy, width, height, px, py) => { + // Adding one for android + const viewTop = Platform.OS === 'ios' ? py : py + 1; + this.setState({ + viewTop, + viewTopReady: true + }); + }); }; /** @@ -101,14 +134,14 @@ class Step3 extends Component { render() { const { selectedAddress, identities, accounts, currentCurrency } = this.props; const account = { address: selectedAddress, ...identities[selectedAddress], ...accounts[selectedAddress] }; - + const { coachmarkTopReady, viewTopReady } = this.state; + if (!coachmarkTopReady || !viewTopReady) return null; return ( - + - - + { } }; - const wrapper = shallow(, { + const wrapper = shallow(, { context: { store: mockStore(initialState) } }); expect(wrapper.dive()).toMatchSnapshot(); diff --git a/app/components/UI/OnboardingWizard/Step4/index.js b/app/components/UI/OnboardingWizard/Step4/index.js index d58bbbe1bb2..92d37b4ca44 100644 --- a/app/components/UI/OnboardingWizard/Step4/index.js +++ b/app/components/UI/OnboardingWizard/Step4/index.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { Platform, View, Text, StyleSheet } from 'react-native'; +import { View, Text, StyleSheet } from 'react-native'; import Coachmark from '../Coachmark'; import setOnboardingWizardStep from '../../../../actions/wizard'; import { strings } from '../../../../../locales/i18n'; @@ -20,8 +20,7 @@ const styles = StyleSheet.create({ flex: 1, position: 'absolute', left: 0, - right: 0, - top: Platform.OS === 'ios' ? 90 : 60 + right: 0 } }); @@ -34,7 +33,32 @@ class Step4 extends Component { /** * Dispatch set onboarding wizard step */ - setOnboardingWizardStep: PropTypes.func + setOnboardingWizardStep: PropTypes.func, + /** + * Coachmark ref to get position + */ + coachmarkRef: PropTypes.object + }; + + state = { + viewTop: 0 + }; + + componentDidMount = () => { + this.getViewPosition(this.props.coachmarkRef.scrollViewContainer); + }; + + /** + * Sets coachmark top position getting AccountOverview component ref from Wallet + */ + getViewPosition = ref => { + ref && + ref.current && + ref.current.measure((fx, fy, width, height, px, py) => { + this.setState({ + viewTop: py + }); + }); }; /** @@ -69,7 +93,7 @@ class Step4 extends Component { render() { return ( - + { + setTimeout(() => { + this.getPosition(this.props.coachmarkRef); + }, 300); + }; + + /** + * If component ref defined, calculate its position and position coachmark accordingly + */ + getPosition = ref => { + ref && + ref.current && + ref.current.measure((a, b, width, height, px, py) => { + this.setState({ coachmarkTop: height + py - INDICATOR_HEIGHT }); + }); }; /** @@ -74,9 +101,11 @@ class Step5 extends Component { ); render() { + if (this.state.coachmarkTop === 0) return null; + return ( - + { + this.getPosition(); + }, 1200); } + /** + * If component ref defined, calculate its position and position coachmark accordingly + */ + getPosition = () => { + const position = Platform.OS === 'android' ? 270 : DeviceSize.isIphoneX() ? 300 : 270; + this.setState({ coachmarkTop: position, ready: true }); + }; + /** * Dispatches 'setOnboardingWizardStep' with next step * Closing drawer and navigating to 'WalletView' @@ -73,7 +85,7 @@ class Step6 extends Component { if (!ready) return null; return ( - + { + const position = Platform.OS === 'android' ? 85 : DeviceSize.isIphoneX() ? 120 : 100; + this.setState({ coachmarkTop: position }); + }; + /** * Dispatches 'setOnboardingWizardStep' with back step */ @@ -61,14 +77,14 @@ class Step7 extends Component { render() { return ( - + diff --git a/app/components/UI/OnboardingWizard/__snapshots__/index.test.js.snap b/app/components/UI/OnboardingWizard/__snapshots__/index.test.js.snap index 7923ba49d6f..68e4af7f04e 100644 --- a/app/components/UI/OnboardingWizard/__snapshots__/index.test.js.snap +++ b/app/components/UI/OnboardingWizard/__snapshots__/index.test.js.snap @@ -1,16 +1,74 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`OnboardingWizard should render correctly 1`] = ` - - + `; diff --git a/app/components/UI/OnboardingWizard/index.js b/app/components/UI/OnboardingWizard/index.js index 3d0fcb584d3..78551c946f1 100644 --- a/app/components/UI/OnboardingWizard/index.js +++ b/app/components/UI/OnboardingWizard/index.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { TouchableOpacity, View, StyleSheet, Text } from 'react-native'; +import { Platform, TouchableOpacity, View, StyleSheet, Text } from 'react-native'; import { colors, fontStyles } from '../../../styles/common'; import { connect } from 'react-redux'; import Step1 from './Step1'; @@ -14,27 +14,51 @@ import setOnboardingWizardStep from '../../../actions/wizard'; import { DrawerActions } from 'react-navigation-drawer'; // eslint-disable-line import { strings } from '../../../../locales/i18n'; import AsyncStorage from '@react-native-community/async-storage'; +import ElevatedView from 'react-native-elevated-view'; +import Modal from 'react-native-modal'; +import DeviceSize from '../../../util/DeviceSize'; const styles = StyleSheet.create({ root: { - left: 0, - right: 0, top: 0, bottom: 0, + left: 0, + right: 0, + flex: 1, + margin: 0, position: 'absolute' }, main: { flex: 1, backgroundColor: colors.transparent }, + skipWrapper: { + alignItems: 'center', + alignSelf: 'center', + bottom: Platform.OS === 'ios' && DeviceSize.isIphoneX() ? 98 : 66 + }, skip: { height: 30, - bottom: 30 + borderRadius: 30, + backgroundColor: colors.white, + alignItems: 'center' + }, + androidElevated: { + width: 120, + borderRadius: 30 + }, + iosTouchable: { + width: 120 + }, + skipTextWrapper: { + flex: 1, + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center' }, skipText: { ...fontStyles.normal, - textAlign: 'center', - fontSize: 18, + fontSize: 12, color: colors.blue } }); @@ -52,7 +76,11 @@ class OnboardingWizard extends Component { /** * Dispatch set onboarding wizard step */ - setOnboardingWizardStep: PropTypes.func + setOnboardingWizardStep: PropTypes.func, + /** + * Coachmark ref to get position + */ + coachmarkRef: PropTypes.object }; /** @@ -65,14 +93,17 @@ class OnboardingWizard extends Component { navigation && navigation.dispatch(DrawerActions.closeDrawer()); }; - onboardingWizardNavigator = { - 1: , - 2: , - 3: , - 4: , - 5: , - 6: , - 7: + onboardingWizardNavigator = step => { + const steps = { + 1: , + 2: , + 3: , + 4: , + 5: , + 6: , + 7: + }; + return steps[step]; }; render() { @@ -80,14 +111,32 @@ class OnboardingWizard extends Component { wizard: { step } } = this.props; return ( - - {this.onboardingWizardNavigator[step]} + + {this.onboardingWizardNavigator(step)} {step !== 1 && ( - - {strings('onboarding_wizard.skip_tutorial')} - + + + + {strings('onboarding_wizard.skip_tutorial')} + + + )} - + ); } } diff --git a/app/components/UI/PaymentChannelApproval/__snapshots__/index.test.js.snap b/app/components/UI/PaymentChannelApproval/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..52675347936 --- /dev/null +++ b/app/components/UI/PaymentChannelApproval/__snapshots__/index.test.js.snap @@ -0,0 +1,228 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaymentChannelApproval should render correctly 1`] = ` + + + + + PAYMENT REQUEST + + + + + + + + + + Account 1 + + + + + + + + + + + + + + Coffe Shop + + + + + is requesting you to pay + + + + $ + 1.00 + + + + + +`; diff --git a/app/components/UI/PaymentChannelApproval/index.js b/app/components/UI/PaymentChannelApproval/index.js new file mode 100644 index 00000000000..8e3221b1d64 --- /dev/null +++ b/app/components/UI/PaymentChannelApproval/index.js @@ -0,0 +1,308 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { Platform, Animated, StyleSheet, Text, View } from 'react-native'; +import Icon from 'react-native-vector-icons/FontAwesome'; +import ActionView from '../ActionView'; +import ElevatedView from 'react-native-elevated-view'; +import Identicon from '../Identicon'; +import { strings } from '../../../../locales/i18n'; +import { colors, fontStyles } from '../../../styles/common'; +import DeviceSize from '../../../util/DeviceSize'; +import WebsiteIcon from '../WebsiteIcon'; +import { renderAccountName, renderShortAddress } from '../../../util/address'; + +const styles = StyleSheet.create({ + root: { + backgroundColor: colors.white, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + minHeight: Platform.OS === 'ios' ? '62%' : '80%', + paddingBottom: DeviceSize.isIphoneX() ? 20 : 0 + }, + wrapper: { + paddingHorizontal: 25 + }, + iconWrapper: { + marginTop: 60, + alignItems: 'center', + justifyContent: 'center' + }, + title: { + ...fontStyles.bold, + color: colors.fontPrimary, + fontSize: 14, + marginVertical: 24, + textAlign: 'center' + }, + intro: { + ...fontStyles.normal, + textAlign: 'center', + color: colors.fontPrimary, + fontSize: 20, + marginVertical: 24 + }, + total: { + flex: 1, + marginTop: 15, + marginBottom: 30 + }, + totalPrice: { + textAlign: 'center', + ...fontStyles.bold, + color: colors.fontPrimary, + fontSize: 55 + }, + permissions: { + alignItems: 'flex-start', + borderColor: colors.grey100, + borderTopWidth: 1, + display: 'flex', + flexDirection: 'row', + paddingVertical: 16 + }, + permissionText: { + textAlign: 'left', + ...fontStyles.normal, + color: colors.fontPrimary, + flexGrow: 1, + fontSize: 14 + }, + permission: { + ...fontStyles.bold, + color: colors.fontPrimary, + fontSize: 14 + }, + header: { + alignItems: 'flex-start', + display: 'flex', + flexDirection: 'row', + marginBottom: 12 + }, + headerTitle: { + ...fontStyles.normal, + color: colors.fontPrimary, + fontSize: 16, + textAlign: 'center' + }, + selectedAddress: { + ...fontStyles.normal, + color: colors.fontPrimary, + fontSize: 16, + marginTop: 12, + textAlign: 'center' + }, + dapp: { + alignItems: 'center', + paddingHorizontal: 14, + width: '50%' + }, + graphic: { + alignItems: 'center', + position: 'absolute', + top: 12, + width: '100%' + }, + check: { + alignItems: 'center', + height: 2, + width: '33%' + }, + border: { + borderColor: colors.grey400, + borderStyle: 'dashed', + borderWidth: 1, + left: 0, + overflow: 'hidden', + position: 'absolute', + top: 12, + width: '100%', + zIndex: 1 + }, + checkWrapper: { + alignItems: 'center', + backgroundColor: colors.green500, + borderRadius: 12, + height: 24, + position: 'relative', + width: 24, + zIndex: 2 + }, + checkIcon: { + color: colors.white, + fontSize: 14, + lineHeight: 24 + }, + icon: { + borderRadius: 27, + marginBottom: 12, + height: 54, + width: 54 + }, + successIcon: { + color: colors.green500, + marginBottom: 30 + } +}); + +/** + * Payment channel request approval component + */ +class PaymentChannelApproval extends Component { + static propTypes = { + /** + * Object containing current title, amount and detail + */ + info: PropTypes.object, + /** + * Callback triggered on account access approval + */ + onConfirm: PropTypes.func, + /** + * Callback triggered on account access rejection + */ + onCancel: PropTypes.func, + /** + /* Identities object required to get account name + */ + identities: PropTypes.object, + /** + * A string that represents the selected address + */ + selectedAddress: PropTypes.string, + /** + * A bool that determines when the payment is in progress + */ + loading: PropTypes.bool, + /** + * A bool that determines when the payment is in progress + */ + complete: PropTypes.bool + }; + + iconSpringVal = new Animated.Value(0.4); + + animateIcon() { + Animated.spring(this.iconSpringVal, { + toValue: 1, + friction: 2, + useNativeDriver: true, + isInteraction: false + }).start(); + } + + componentDidUpdate(prevProps) { + if (!prevProps.complete && this.props.complete) { + this.animateIcon(); + } + } + + getFormattedAmount = () => + parseFloat(this.props.info.amount) + .toFixed(2) + .toString(); + + render = () => { + const { + info: { title, detail, to }, + onConfirm, + onCancel, + selectedAddress, + identities, + loading, + complete + } = this.props; + const formattedAmount = this.getFormattedAmount(); + + if (complete) { + return ( + + + + {strings('paymentRequest.title_complete')} + + + + + + + ); + } + + return ( + + + + {strings('paymentRequest.title')} + + + + + + + + + {renderAccountName(selectedAddress, identities)} + + + + + + + + + + + + {title ? ( + + ) : ( + + )} + {title ? ( + + {title} + + ) : ( + {renderShortAddress(to)} + )} + + + {strings('paymentRequest.is_requesting_you_to_pay')} + + ${formattedAmount} + + {detail && ( + + + {detail} + + + )} + + + + ); + }; +} + +const mapStateToProps = state => ({ + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + identities: state.engine.backgroundState.PreferencesController.identities +}); + +export default connect(mapStateToProps)(PaymentChannelApproval); diff --git a/app/components/UI/PaymentChannelApproval/index.test.js b/app/components/UI/PaymentChannelApproval/index.test.js new file mode 100644 index 00000000000..5943adb4170 --- /dev/null +++ b/app/components/UI/PaymentChannelApproval/index.test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PaymentChannelApproval from './'; +import { shallow } from 'enzyme'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); + +describe('PaymentChannelApproval', () => { + it('should render correctly', () => { + const initialState = { + engine: { + backgroundState: { + PreferencesController: { + selectedAddress: '0xe7E125654064EEa56229f273dA586F10DF96B0a1', + identities: { '0xe7E125654064EEa56229f273dA586F10DF96B0a1': { name: 'Account 1' } } + } + } + } + }; + + const wrapper = shallow( + , + { + context: { store: mockStore(initialState) } + } + ); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/PaymentRequest/index.js b/app/components/UI/PaymentRequest/index.js index 51646252864..5c5c49ebb6f 100644 --- a/app/components/UI/PaymentRequest/index.js +++ b/app/components/UI/PaymentRequest/index.js @@ -1,5 +1,15 @@ import React, { Component } from 'react'; -import { Platform, SafeAreaView, TextInput, Text, StyleSheet, View, TouchableOpacity } from 'react-native'; +import { + Platform, + SafeAreaView, + TextInput, + Text, + StyleSheet, + View, + TouchableOpacity, + KeyboardAvoidingView, + InteractionManager +} from 'react-native'; import { connect } from 'react-redux'; import { colors, fontStyles, baseStyles } from '../../../styles/common'; import { getPaymentRequestOptionsTitle } from '../../UI/Navbar'; @@ -24,9 +34,10 @@ import { strings } from '../../../../locales/i18n'; import FontAwesome from 'react-native-vector-icons/FontAwesome'; import StyledButton from '../StyledButton'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; -import { generateETHLink, generateERC20Link } from '../../../util/eip681-link-generator'; +import { generateETHLink, generateERC20Link, generateUniversalLinkRequest } from '../../../util/payment-link-generator'; import NetworkList from '../../../util/networks'; +const KEYBOARD_OFFSET = 120; const styles = StyleSheet.create({ wrapper: { backgroundColor: colors.white, @@ -241,6 +252,8 @@ class PaymentRequest extends Component { networkType: PropTypes.string }; + amountInput = React.createRef(); + state = { searchInputValue: '', results: [], @@ -268,6 +281,12 @@ class PaymentRequest extends Component { } }; + componentDidUpdate = () => { + InteractionManager.runAfterInteractions(() => { + this.amountInput.current && this.amountInput.current.focus(); + }); + }; + /** * Go to asset selection view and modify navbar accordingly */ @@ -465,13 +484,13 @@ class PaymentRequest extends Component { * Updates internalPrimaryCurrency */ switchPrimaryCurrency = async () => { - const { internalPrimaryCurrency } = this.state; + const { internalPrimaryCurrency, secondaryAmount } = this.state; const primarycurrencies = { ETH: 'Fiat', Fiat: 'ETH' }; await this.setState({ internalPrimaryCurrency: primarycurrencies[internalPrimaryCurrency] }); - this.updateAmount(); + this.updateAmount(secondaryAmount.split(' ')[0]); }; /** @@ -489,15 +508,20 @@ class PaymentRequest extends Component { const { selectedAddress, navigation } = this.props; const { cryptoAmount, selectedAsset, chainId } = this.state; try { - let link; + let eth_link; if (selectedAsset.isETH) { - link = generateETHLink(selectedAddress, cryptoAmount, chainId); + eth_link = generateETHLink(selectedAddress, cryptoAmount, chainId); } else { - link = generateERC20Link(selectedAddress, selectedAsset.address, cryptoAmount, chainId); + eth_link = generateERC20Link(selectedAddress, selectedAsset.address, cryptoAmount, chainId); } + + // Convert to universal link / app link + const link = generateUniversalLinkRequest(eth_link); + navigation && navigation.replace('PaymentRequestSuccess', { link, + qrLink: eth_link, amount: cryptoAmount, symbol: selectedAsset.symbol }); @@ -540,6 +564,7 @@ class PaymentRequest extends Component { style={styles.input} value={amount} onSubmitEditing={this.onNext} + ref={this.amountInput} /> {symbol} @@ -575,7 +600,12 @@ class PaymentRequest extends Component { )} - + {strings('payment_request.reset')} @@ -589,7 +619,7 @@ class PaymentRequest extends Component { {strings('payment_request.next')} - + ); }; diff --git a/app/components/UI/PaymentRequestSuccess/index.js b/app/components/UI/PaymentRequestSuccess/index.js index 2a2db02667f..81ac6117a8b 100644 --- a/app/components/UI/PaymentRequestSuccess/index.js +++ b/app/components/UI/PaymentRequestSuccess/index.js @@ -166,6 +166,7 @@ class PaymentRequestSuccess extends Component { state = { link: '', + qrLink: '', amount: '', symbol: '', qrModalVisible: false @@ -177,9 +178,10 @@ class PaymentRequestSuccess extends Component { componentDidMount = () => { const { navigation } = this.props; const link = navigation && navigation.getParam('link', ''); + const qrLink = navigation && navigation.getParam('qrLink', ''); const amount = navigation && navigation.getParam('amount', ''); const symbol = navigation && navigation.getParam('symbol', ''); - this.setState({ link, amount, symbol }); + this.setState({ link, qrLink, amount, symbol }); }; /** @@ -304,7 +306,7 @@ class PaymentRequestSuccess extends Component { - + diff --git a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap index b6103645a83..65313f41823 100644 --- a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap +++ b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap @@ -124,10 +124,12 @@ exports[`ReceiveRequest should render correctly 1`] = ` actionDescription="Request assets from friends" actionTitle="Request" icon={ - diff --git a/app/components/UI/ReceiveRequest/index.js b/app/components/UI/ReceiveRequest/index.js index f3111a88aa9..ba5002c60aa 100644 --- a/app/components/UI/ReceiveRequest/index.js +++ b/app/components/UI/ReceiveRequest/index.js @@ -30,6 +30,7 @@ import IonicIcon from 'react-native-vector-icons/Ionicons'; import DeviceSize from '../../../util/DeviceSize'; import { showAlert } from '../../../actions/alert'; import GlobalAlert from '../GlobalAlert'; +import { generateUniversalLinkAddress } from '../../../util/payment-link-generator'; const TOTAL_PADDING = 64; const ACTION_WIDTH = (Dimensions.get('window').width - TOTAL_PADDING) / 2; @@ -187,7 +188,7 @@ class ReceiveRequest extends Component { onShare = () => { const { selectedAddress } = this.props; Share.open({ - message: `ethereum:${selectedAddress}` + message: generateUniversalLinkAddress(selectedAddress) }).catch(err => { Logger.log('Error while trying to share address', err); }); @@ -262,6 +263,7 @@ class ReceiveRequest extends Component { render() { const { qrModalVisible, buyModalVisible } = this.state; + return ( diff --git a/app/components/UI/SearchTokenAutocomplete/__snapshots__/index.test.js.snap b/app/components/UI/SearchTokenAutocomplete/__snapshots__/index.test.js.snap index a7b77d7048f..a833e6662af 100644 --- a/app/components/UI/SearchTokenAutocomplete/__snapshots__/index.test.js.snap +++ b/app/components/UI/SearchTokenAutocomplete/__snapshots__/index.test.js.snap @@ -14,6 +14,7 @@ exports[`SearchTokenAutocomplete should render correctly 1`] = ` cancelTestID="" cancelText="CANCEL" confirmButtonMode="normal" + confirmDisabled={true} confirmTestID="" confirmText="ADD TOKEN" confirmed={false} diff --git a/app/components/UI/SearchTokenAutocomplete/index.js b/app/components/UI/SearchTokenAutocomplete/index.js index 75b762af9c9..d547343c63c 100644 --- a/app/components/UI/SearchTokenAutocomplete/index.js +++ b/app/components/UI/SearchTokenAutocomplete/index.js @@ -53,6 +53,7 @@ export default class SearchTokenAutocomplete extends Component { render = () => { const { searchResults, selectedAsset, searchQuery } = this.state; + const { address, symbol, decimals } = selectedAsset; return ( @@ -61,6 +62,7 @@ export default class SearchTokenAutocomplete extends Component { confirmText={strings('add_asset.tokens.add_token')} onCancelPress={this.cancelAddToken} onConfirmPress={this.addToken} + confirmDisabled={!(address && symbol && decimals)} > diff --git a/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap b/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap index 1accc41d5f4..541aaeda5e3 100644 --- a/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap +++ b/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap @@ -34,17 +34,24 @@ exports[`SignatureRequest should render correctly 1`] = ` - + {strings('signature_request.account_title')} - - + + - - {accountLabel} + + + {accountLabel} + diff --git a/app/components/UI/Tabs/TabCountIcon/__snapshots__/index.test.js.snap b/app/components/UI/Tabs/TabCountIcon/__snapshots__/index.test.js.snap index 137bb3ead73..4579f1f5b7a 100644 --- a/app/components/UI/Tabs/TabCountIcon/__snapshots__/index.test.js.snap +++ b/app/components/UI/Tabs/TabCountIcon/__snapshots__/index.test.js.snap @@ -1,16 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TabCountIcon should render correctly 1`] = ` - 1 - + `; diff --git a/app/components/UI/Tabs/TabCountIcon/index.js b/app/components/UI/Tabs/TabCountIcon/index.js index f3a9847426a..3ed8a6a9af0 100644 --- a/app/components/UI/Tabs/TabCountIcon/index.js +++ b/app/components/UI/Tabs/TabCountIcon/index.js @@ -1,14 +1,14 @@ import React, { Component } from 'react'; -import { Platform, TouchableOpacity, StyleSheet, Text } from 'react-native'; +import { Platform, View, StyleSheet, Text } from 'react-native'; import PropTypes from 'prop-types'; import { colors, fontStyles } from '../../../../styles/common'; import { connect } from 'react-redux'; const styles = StyleSheet.create({ tabIcon: { - borderWidth: Platform.OS === 'android' ? 2 : 3, + borderWidth: 2, borderColor: colors.grey500, - borderRadius: 8, + borderRadius: 6, alignItems: 'center', justifyContent: 'center' }, @@ -29,10 +29,6 @@ const styles = StyleSheet.create({ */ class TabCountIcon extends Component { static propTypes = { - /** - * Shows the tabs view - */ - onPress: PropTypes.func, /** * Switches to a specific tab */ @@ -44,12 +40,12 @@ class TabCountIcon extends Component { }; render() { - const { tabCount, onPress, style } = this.props; + const { tabCount, style } = this.props; return ( - + {tabCount} - + ); } } diff --git a/app/components/UI/Tabs/TabThumbnail/__snapshots__/index.test.js.snap b/app/components/UI/Tabs/TabThumbnail/__snapshots__/index.test.js.snap index d9405534078..21c126b84ef 100644 --- a/app/components/UI/Tabs/TabThumbnail/__snapshots__/index.test.js.snap +++ b/app/components/UI/Tabs/TabThumbnail/__snapshots__/index.test.js.snap @@ -10,7 +10,9 @@ exports[`TabThumbnail should render correctly 1`] = ` } } > - - - - New tab - - + /> + @@ -109,7 +104,7 @@ exports[`TabThumbnail should render correctly 1`] = ` style={ Object { "color": "#FFFFFF", - "fontSize": 38, + "fontSize": 32, "marginTop": -7, "paddingHorizontal": 10, "paddingTop": 3, @@ -120,9 +115,7 @@ exports[`TabThumbnail should render correctly 1`] = ` /> - - - + + `; diff --git a/app/components/UI/Tabs/TabThumbnail/index.js b/app/components/UI/Tabs/TabThumbnail/index.js index 3dbe88c1a7a..f8be94fcf48 100644 --- a/app/components/UI/Tabs/TabThumbnail/index.js +++ b/app/components/UI/Tabs/TabThumbnail/index.js @@ -6,10 +6,11 @@ import WebsiteIcon from '../../WebsiteIcon'; import { strings } from '../../../../../locales/i18n'; import IonIcon from 'react-native-vector-icons/Ionicons'; import { colors, fontStyles } from '../../../../styles/common'; -import URL from 'url-parse'; import DeviceSize from '../../../../util/DeviceSize'; +import AppConstants from '../../../../core/AppConstants'; +import { getHost } from '../../../../util/browser'; -const margin = 16; +const margin = 15; const width = Dimensions.get('window').width - margin * 2; const height = Dimensions.get('window').height / (DeviceSize.isIphone5S() ? 4 : 5); let paddingTop = Dimensions.get('window').height - 190; @@ -24,8 +25,8 @@ if (Platform.OS === 'android') { const styles = StyleSheet.create({ tabFavicon: { alignSelf: 'flex-start', - width: 24, - height: 24, + width: 22, + height: 22, marginRight: 5, marginLeft: 2, marginTop: 1 @@ -33,22 +34,21 @@ const styles = StyleSheet.create({ tabSiteName: { color: colors.white, ...fontStyles.bold, - fontSize: 24, + fontSize: 18, marginRight: 40, marginLeft: 5, - marginTop: Platform.OS === 'ios' ? 0 : -5 + marginTop: 0 }, tabHeader: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'flex-start', backgroundColor: colors.grey500, - paddingVertical: 15, - paddingHorizontal: 10, - minHeight: 25 + paddingVertical: 10, + paddingHorizontal: 10 }, tabWrapper: { - marginBottom: 20, + marginBottom: 15, borderRadius: 10, elevation: 8, justifyContent: 'space-evenly', @@ -82,7 +82,7 @@ const styles = StyleSheet.create({ closeTabIcon: { paddingHorizontal: 10, paddingTop: 3, - fontSize: 38, + fontSize: 32, color: colors.white, right: 0, marginTop: -7, @@ -96,12 +96,13 @@ const styles = StyleSheet.create({ }, closeTabButton: { backgroundColor: colors.transparent, - width: 36, - height: 36 + width: Platform.OS === 'ios' ? 30 : 35, + height: 24, + marginRight: -5 } }); -const HOMEPAGE_URL = 'about:blank'; +const { HOMEPAGE_URL } = AppConstants; const METAMASK_FOX = require('../../../../images/fox.png'); // eslint-disable-line import/no-commonjs /** @@ -128,35 +129,31 @@ export default class TabThumbnail extends Component { onSwitch: PropTypes.func }; - getHostName = () => { - const urlObj = new URL(this.props.tab.url); - return urlObj.hostname.toLowerCase().replace('www.', ''); - }; - getContainer = () => (Platform.OS === 'android' ? View : ElevatedView); render() { const { isActiveTab, tab, onClose, onSwitch } = this.props; const Container = this.getContainer(); - const hostname = this.getHostName(); + const hostname = getHost(tab.url); + const isHomepage = hostname === getHost(HOMEPAGE_URL); return ( - + onSwitch(tab)} // eslint-disable-line react/jsx-no-bind + style={[styles.tabWrapper, isActiveTab && styles.activeTab]} + > - onSwitch(tab)} // eslint-disable-line react/jsx-no-bind - style={styles.titleButton} - > - {tab.url !== HOMEPAGE_URL ? ( + + {!isHomepage ? ( ) : ( )} - {tab.url === HOMEPAGE_URL ? strings('browser.new_tab') : hostname} + {isHomepage ? strings('browser.new_tab') : hostname} - + onClose(tab)} // eslint-disable-line react/jsx-no-bind style={styles.closeTabButton} @@ -164,14 +161,10 @@ export default class TabThumbnail extends Component { - onSwitch(tab)} - > + - - + + ); } diff --git a/app/components/UI/Tabs/TabThumbnail/index.test.js b/app/components/UI/Tabs/TabThumbnail/index.test.js index a142568396c..fbe24541a9f 100644 --- a/app/components/UI/Tabs/TabThumbnail/index.test.js +++ b/app/components/UI/Tabs/TabThumbnail/index.test.js @@ -8,6 +8,7 @@ describe('TabThumbnail', () => { it('should render correctly', () => { const foo = () => null; const wrapper = shallow( + // eslint-disable-next-line react/jsx-no-bind ); expect(wrapper).toMatchSnapshot(); diff --git a/app/components/UI/Tabs/__snapshots__/index.test.js.snap b/app/components/UI/Tabs/__snapshots__/index.test.js.snap index ebe4acc021f..c6210a651c7 100644 --- a/app/components/UI/Tabs/__snapshots__/index.test.js.snap +++ b/app/components/UI/Tabs/__snapshots__/index.test.js.snap @@ -102,6 +102,7 @@ exports[`Tabs should render correctly 1`] = ` > { + this.props.newTab(); + }; + renderTabActions() { - const { tabs, closeAllTabs, newTab, closeTabsView } = this.props; + const { tabs, closeAllTabs, closeTabsView } = this.props; return ( @@ -244,7 +248,7 @@ export default class Tabs extends PureComponent { - + diff --git a/app/components/UI/TransactionEdit/__snapshots__/index.test.js.snap b/app/components/UI/TransactionEdit/__snapshots__/index.test.js.snap index 32a81f9e4ee..4b3e31b8692 100644 --- a/app/components/UI/TransactionEdit/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionEdit/__snapshots__/index.test.js.snap @@ -24,12 +24,14 @@ exports[`TransactionEdit should render correctly 1`] = ` > { const { transaction } = this.props; if (transaction && transaction.value) { - this.props.handleUpdateAmount(transaction.value); + this.props.handleUpdateAmount(transaction.value, true); } if (transaction && transaction.assetType === 'ETH') { this.props.handleUpdateReadableValue(fromWei(transaction.value)); @@ -227,9 +231,8 @@ class TransactionEdit extends Component { }; fillMax = () => { - const { gas, gasPrice, from, selectedAsset, assetType } = this.props.transaction; + const { gas, gasPrice, from, selectedAsset, assetType, paymentChannelTransaction } = this.props.transaction; const { balance } = this.props.accounts[from]; - const { contractBalances } = this.props; let value, readableValue; if (assetType === 'ETH') { @@ -240,6 +243,10 @@ class TransactionEdit extends Component { ? hexToBN(balance).sub(totalGas) : fromWei(0); readableValue = fromWei(value); + } else if (paymentChannelTransaction) { + const state = PaymentChannelsClient.getState(); + value = toTokenMinimalUnit(state.balance, selectedAsset.decimals); + readableValue = state.balance; } else if (assetType === 'ERC20') { value = hexToBN(contractBalances[selectedAsset.address].toString(16)); readableValue = fromTokenMinimalUnit(value, selectedAsset.decimals); @@ -345,7 +352,17 @@ class TransactionEdit extends Component { render() { const { navigation, - transaction: { value, gas, gasPrice, from, to, selectedAsset, readableValue, ensRecipient }, + transaction: { + value, + gas, + gasPrice, + from, + to, + selectedAsset, + readableValue, + ensRecipient, + paymentChannelTransaction + }, showHexData } = this.props; const { gasError, toAddressError, toAddressWarning, data, accountSelectIsOpen, ethInputIsOpen } = this.state; @@ -353,13 +370,17 @@ class TransactionEdit extends Component { return ( - + {strings('transaction.from')}: @@ -403,35 +424,39 @@ class TransactionEdit extends Component { isOpen={accountSelectIsOpen} /> - - - {strings('transaction.gas_fee')}: - {gasError ? {gasError} : null} - - - - - {showHexData && ( + {!paymentChannelTransaction && ( + - {strings('transaction.hex_data')}: + {strings('transaction.gas_fee')}: + {gasError ? {gasError} : null} - )} - {showHexData && ( - - )} - + + )} + {!paymentChannelTransaction && ( + + {showHexData && ( + + {strings('transaction.hex_data')}: + + )} + {showHexData && ( + + )} + + )} diff --git a/app/components/UI/TransactionEditor/index.js b/app/components/UI/TransactionEditor/index.js index 4c973f15910..2de85e8327b 100644 --- a/app/components/UI/TransactionEditor/index.js +++ b/app/components/UI/TransactionEditor/index.js @@ -4,7 +4,7 @@ import { StyleSheet, View } from 'react-native'; import { colors } from '../../../styles/common'; import TransactionReview from '../TransactionReview'; import TransactionEdit from '../TransactionEdit'; -import { isBN, hexToBN, toBN } from '../../../util/number'; +import { isBN, hexToBN, toBN, isDecimal } from '../../../util/number'; import { isValidAddress, toChecksumAddress, BN } from 'ethereumjs-util'; import { strings } from '../../../../locales/i18n'; import { connect } from 'react-redux'; @@ -13,6 +13,7 @@ import { setTransactionObject } from '../../../actions/transaction'; import Engine from '../../../core/Engine'; import collectiblesTransferInformation from '../../../util/collectibles-transfer'; import contractMap from 'eth-contract-metadata'; +import PaymentChannelsClient from '../../../core/PaymentChannelsClient'; const styles = StyleSheet.create({ root: { @@ -147,21 +148,23 @@ class TransactionEditor extends Component { * If is an asset transaction it generates data to send and estimates gas again with new value and new data * * @param {object} amount - BN object containing transaction amount + * @param {bool} mounting - Whether the view is mounting, in that case it should use the gas from transaction state */ - handleUpdateAmount = async amount => { + handleUpdateAmount = async (amount, mounting = false) => { const { - transaction: { to, data, assetType } + transaction: { to, data, assetType, gas: gasLimit } } = this.props; // If ETH transaction, there is no need to generate new data if (assetType === 'ETH') { - const { gas } = await this.estimateGas({ amount, data, to }); + const { gas } = mounting ? { gas: gasLimit } : await this.estimateGas({ amount, data, to }); this.props.setTransactionObject({ value: amount, to, gas: hexToBN(gas) }); } // If selectedAsset defined, generates data else if (assetType === 'ERC20') { - const { data, gas } = await this.handleDataGeneration({ value: amount }); - this.props.setTransactionObject({ value: amount, to, gas: hexToBN(gas), data }); + const res = await this.handleDataGeneration({ value: amount }); + const gas = mounting ? gasLimit : res.gas; + this.props.setTransactionObject({ value: amount, to, gas: hexToBN(gas), data: res.data }); } }; @@ -228,7 +231,7 @@ class TransactionEditor extends Component { this.props.setTransactionObject({ value: undefined, data: undefined, - selectedAsset: { symbol: 'ETH' }, + selectedAsset: { symbol: 'ETH', isETH: true }, gas: hexToBN(gas) }); } else { @@ -306,8 +309,11 @@ class TransactionEditor extends Component { */ validateAmount = async (allowEmpty = true) => { const { - transaction: { assetType } + transaction: { assetType, paymentChannelTransaction } } = this.props; + if (paymentChannelTransaction) { + return this.validatePaymentChannelAmount(allowEmpty); + } const validations = { ETH: () => this.validateEtherAmount(allowEmpty), ERC20: async () => await this.validateTokenAmount(allowEmpty), @@ -414,6 +420,30 @@ class TransactionEditor extends Component { return error; }; + /** + * Validates payment request transaction + * + * @param {bool} allowEmpty - Whether the validation allows empty amount or not + * @returns {string} - String containing error message whether the Ether transaction amount is valid or not + */ + validatePaymentChannelAmount = allowEmpty => { + let error; + if (!allowEmpty) { + const { + transaction: { value, readableValue, from } + } = this.props; + if (!value || !from || !readableValue) { + return strings('transaction.invalid_amount'); + } + if (value && !isBN(value)) return strings('transaction.invalid_amount'); + const state = PaymentChannelsClient.getState(); + if (isDecimal(state.balance) && parseFloat(readableValue) > parseFloat(state.balance)) { + return strings('transaction.insufficient'); + } + } + return error; + }; + /** * Validates transaction gas * @@ -422,8 +452,10 @@ class TransactionEditor extends Component { validateGas = () => { let error; const { - transaction: { gas, gasPrice, from } + transaction: { gas, gasPrice, from, paymentChannelTransaction } } = this.props; + // If its handling a payment request transaction it won't do any gas validation + if (paymentChannelTransaction) return; if (!gas) return strings('transaction.invalid_gas'); if (gas && !isBN(gas)) return strings('transaction.invalid_gas'); if (!gasPrice) return strings('transaction.invalid_gas_price'); diff --git a/app/components/UI/TransactionElement/TransactionDetails/index.js b/app/components/UI/TransactionElement/TransactionDetails/index.js index 3954c373738..4b8ddb7dbab 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/index.js +++ b/app/components/UI/TransactionElement/TransactionDetails/index.js @@ -8,7 +8,7 @@ import Button from '../../Button'; import ActionModal from '../../../UI/ActionModal'; import Engine from '../../../../core/Engine'; import { renderFromWei } from '../../../../util/number'; -import { CANCEL_RATE } from 'gaba/TransactionController'; +import { CANCEL_RATE } from 'gaba/transaction/TransactionController'; import { getNetworkTypeById, findBlockExplorerForRpc, getBlockExplorerName } from '../../../../util/networks'; import { getEtherscanTransactionUrl, getEtherscanBaseUrl } from '../../../../util/etherscan'; import Logger from '../../../../util/Logger'; diff --git a/app/components/UI/TransactionElement/index.js b/app/components/UI/TransactionElement/index.js index 39877de7433..55a9e0ff39f 100644 --- a/app/components/UI/TransactionElement/index.js +++ b/app/components/UI/TransactionElement/index.js @@ -4,7 +4,7 @@ import { Platform, TouchableHighlight, StyleSheet, Text, View, Image } from 'rea import { colors, fontStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import { toLocaleDateTime } from '../../../util/date'; -import { renderFromWei, weiToFiat, hexToBN, toBN, isBN, renderToGwei } from '../../../util/number'; +import { renderFromWei, weiToFiat, hexToBN, toBN, isBN, renderToGwei, balanceToFiat } from '../../../util/number'; import { toChecksumAddress } from 'ethereumjs-util'; import Identicon from '../Identicon'; import { getActionKey, decodeTransferData, getTicker } from '../../../util/transactions'; @@ -15,6 +15,12 @@ import TokenImage from '../TokenImage'; import contractMap from 'eth-contract-metadata'; import TransferElement from './TransferElement'; import { connect } from 'react-redux'; +import AppConstants from '../../../core/AppConstants'; +import Ionicons from 'react-native-vector-icons/Ionicons'; + +const { + CONNEXT: { CONTRACTS } +} = AppConstants; const styles = StyleSheet.create({ row: { @@ -93,6 +99,25 @@ const styles = StyleSheet.create({ width: 24, height: 24, borderRadius: 12 + }, + paymentChannelTransactionIconWrapper: { + justifyContent: 'center', + alignItems: 'center', + borderWidth: 1, + borderColor: colors.grey100, + borderRadius: 12, + width: 24, + height: 24, + backgroundColor: colors.white + }, + paymentChannelTransactionDepositIcon: { + marginTop: 2, + marginLeft: 1 + }, + paymentChannelTransactionWithdrawIcon: { + marginBottom: 2, + marginRight: 1, + transform: [{ rotate: '180deg' }] } }); @@ -159,7 +184,8 @@ class TransactionElement extends PureComponent { /** * Current provider ticker */ - ticker: PropTypes.string + ticker: PropTypes.string, + exchangeRate: PropTypes.number }; state = { @@ -171,7 +197,7 @@ class TransactionElement extends PureComponent { componentDidMount = async () => { this.mounted = true; const { tx, selectedAddress, ticker } = this.props; - const actionKey = await getActionKey(tx, selectedAddress, ticker); + const actionKey = tx.actionKey || (await getActionKey(tx, selectedAddress, ticker)); this.mounted && this.setState({ actionKey }); }; @@ -201,7 +227,7 @@ class TransactionElement extends PureComponent { const selfSent = incoming && toChecksumAddress(tx.transaction.from) === selectedAddress; return ( - {(!incoming || selfSent) && `#${hexToBN(tx.transaction.nonce).toString()} - `} + {(!incoming || selfSent) && tx.transaction.nonce && `#${hexToBN(tx.transaction.nonce).toString()} - `} {`${toLocaleDateTime(tx.time)}`} ); @@ -220,6 +246,70 @@ class TransactionElement extends PureComponent { ) : null; }; + renderTxElementImage = transactionElement => { + const { + addressTo, + addressFrom, + actionKey, + contractDeployment = false, + paymentChannelTransaction + } = transactionElement; + const { + tx: { networkID } + } = this.props; + const checksumAddress = toChecksumAddress(addressTo); + let logo; + + if (contractDeployment) { + return ( + + + + ); + } else if (actionKey === strings('transactions.smart_contract_interaction')) { + if (checksumAddress in contractMap) { + logo = contractMap[checksumAddress].logo; + } + return ( + + ); + } else if (paymentChannelTransaction) { + const contract = CONTRACTS[networkID]; + const isDeposit = contract && addressTo.toLowerCase() === contract.toLowerCase(); + if (isDeposit) { + return ( + + + + ); + } + const isWithdraw = addressFrom === CONTRACTS[networkID]; + if (isWithdraw) { + return ( + + + + ); + } + } + return ; + }; + /** * Renders an horizontal bar with basic tx information * @@ -229,31 +319,17 @@ class TransactionElement extends PureComponent { const { tx: { status } } = this.props; - const { addressTo, actionKey, value, fiatValue, contractDeployment = false } = transactionElement; + const { addressTo, actionKey, value, fiatValue = false } = transactionElement; const checksumAddress = toChecksumAddress(addressTo); - let symbol, logo; + let symbol; if (checksumAddress in contractMap) { symbol = contractMap[checksumAddress].symbol; - logo = contractMap[checksumAddress].logo; } return ( {this.renderTxTime()} - {contractDeployment ? ( - - - - ) : actionKey === strings('transactions.smart_contract_interaction') ? ( - - ) : ( - - )} + {this.renderTxElementImage(transactionElement)} {symbol ? symbol + ' ' + actionKey : actionKey} @@ -313,6 +389,7 @@ class TransactionElement extends PureComponent { const transactionElement = { addressTo, + addressFrom, actionKey, value: `${strings('unit.token_id')}${tokenId}`, fiatValue: collectible ? collectible.symbol : undefined @@ -354,6 +431,7 @@ class TransactionElement extends PureComponent { const transactionElement = { addressTo: to, + addressFrom: from, actionKey, value: renderTotalEth, fiatValue: renderTotalEthFiat @@ -383,6 +461,7 @@ class TransactionElement extends PureComponent { const transactionElement = { addressTo: to, + addressFrom: from, actionKey, value: renderTotalEth, fiatValue: renderTotalEthFiat, @@ -402,9 +481,56 @@ class TransactionElement extends PureComponent { return [transactionElement, transactionDetails]; }; + renderPaymentChannelTx = () => { + const { + tx: { + networkID, + transactionHash, + transaction: { value, gas, gasPrice, from, to } + }, + conversionRate, + currentCurrency, + exchangeRate + } = this.props; + let { actionKey } = this.state; + const contract = CONTRACTS[networkID]; + const isDeposit = contract && to.toLowerCase() === contract.toLowerCase(); + actionKey = actionKey && actionKey.replace(strings('unit.eth'), strings('unit.dai')); + const totalEth = hexToBN(value); + const totalEthFiat = weiToFiat(totalEth, conversionRate, currentCurrency.toUpperCase()); + const readableTotalEth = renderFromWei(totalEth); + const renderTotalEth = readableTotalEth + ' ' + (isDeposit ? strings('unit.eth') : strings('unit.dai')); + const renderTotalEthFiat = isDeposit + ? totalEthFiat + : balanceToFiat(parseFloat(readableTotalEth), conversionRate, exchangeRate, currentCurrency); + + const transactionDetails = { + renderFrom: renderFullAddress(from), + renderTo: renderFullAddress(to), + transactionHash, + renderGas: gas ? parseInt(gas, 16).toString() : strings('transactions.tx_details_not_available'), + renderGasPrice: gasPrice ? renderToGwei(gasPrice) : strings('transactions.tx_details_not_available'), + renderValue: renderTotalEth, + renderTotalValue: renderTotalEth, + renderTotalValueFiat: isDeposit && totalEthFiat + }; + + const transactionElement = { + addressTo: to, + addressFrom: from, + actionKey, + value: renderTotalEth, + fiatValue: renderTotalEthFiat, + paymentChannelTransaction: true + }; + + return [transactionElement, transactionDetails]; + }; + render() { const { tx: { + paymentChannelTransaction, transaction: { gas, gasPrice } }, selected, @@ -433,15 +559,19 @@ class TransactionElement extends PureComponent { /> ); } - switch (actionKey) { - case strings('transactions.sent_collectible'): - [transactionElement, transactionDetails] = this.renderTransferFromElement(totalGas); - break; - case strings('transactions.contract_deploy'): - [transactionElement, transactionDetails] = this.renderDeploymentElement(totalGas); - break; - default: - [transactionElement, transactionDetails] = this.renderConfirmElement(totalGas); + if (paymentChannelTransaction) { + [transactionElement, transactionDetails] = this.renderPaymentChannelTx(); + } else { + switch (actionKey) { + case strings('transactions.sent_collectible'): + [transactionElement, transactionDetails] = this.renderTransferFromElement(totalGas); + break; + case strings('transactions.contract_deploy'): + [transactionElement, transactionDetails] = this.renderDeploymentElement(totalGas); + break; + default: + [transactionElement, transactionDetails] = this.renderConfirmElement(totalGas); + } } return ( { _getIcon = () => { switch (type) { case 'pending': + case 'pending_withdrawal': + case 'pending_deposit': return ; + case 'success_deposit': + case 'success_withdrawal': case 'success': case 'received': + case 'received_payment': return ; case 'cancelled': case 'error': @@ -76,13 +81,23 @@ export const TransactionNotification = props => { switch (type) { case 'pending': return strings('notifications.pending_title'); + case 'pending_deposit': + return strings('notifications.pending_deposit_title'); + case 'pending_withdrawal': + return strings('notifications.pending_withdrawal_title'); case 'success': return strings('notifications.success_title', { nonce: transaction.nonce }); + case 'success_deposit': + return strings('notifications.success_deposit_title'); + case 'success_withdrawal': + return strings('notifications.success_withdrawal_title'); case 'received': return strings('notifications.received_title', { amount: transaction.amount, assetType: transaction.assetType }); + case 'received_payment': + return strings('notifications.received_payment_title'); case 'cancelled': return strings('notifications.cancelled_title'); case 'error': @@ -91,7 +106,12 @@ export const TransactionNotification = props => { }; // eslint-disable-next-line no-undef - _getDescription = () => strings(`notifications.${type}_message`); + _getDescription = () => { + if (transaction && transaction.amount) { + return strings(`notifications.${type}_message`, { amount: transaction.amount }); + } + return strings(`notifications.${type}_message`); + }; // eslint-disable-next-line _getContent = () => ( diff --git a/app/components/UI/TransactionReview/TransactionReviewInformation/__snapshots__/index.test.js.snap b/app/components/UI/TransactionReview/TransactionReviewInformation/__snapshots__/index.test.js.snap index efbbbe60941..583b2b50d09 100644 --- a/app/components/UI/TransactionReview/TransactionReviewInformation/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionReview/TransactionReviewInformation/__snapshots__/index.test.js.snap @@ -12,13 +12,17 @@ exports[`TransactionReviewInformation should render correctly 1`] = ` > - - - EDIT - - - + {strings('transaction.gas_fee').toUpperCase()} - - - {strings('transaction.edit').toUpperCase()} - - {totalGasFiat} {totalGasEth} diff --git a/app/components/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap b/app/components/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap index b912d5f673c..5b7eeae89c6 100644 --- a/app/components/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap @@ -23,7 +23,7 @@ exports[`TransactionReviewSummary should render correctly 1`] = ` "fontWeight": "400", "lineHeight": 22, "textAlign": "center", - "width": 144, + "width": "50%", } } /> diff --git a/app/components/UI/TransactionReview/TransactionReviewSummary/index.js b/app/components/UI/TransactionReview/TransactionReviewSummary/index.js index 40b14a8d487..7ab34b58178 100644 --- a/app/components/UI/TransactionReview/TransactionReviewSummary/index.js +++ b/app/components/UI/TransactionReview/TransactionReviewSummary/index.js @@ -23,7 +23,7 @@ const styles = StyleSheet.create({ fontSize: 12, lineHeight: 22, textAlign: 'center', - width: 144 + width: '50%' }, summary: { backgroundColor: colors.beige, diff --git a/app/components/UI/Transactions/index.js b/app/components/UI/Transactions/index.js index b7036663df6..da93239da0f 100644 --- a/app/components/UI/Transactions/index.js +++ b/app/components/UI/Transactions/index.js @@ -105,7 +105,8 @@ class Transactions extends PureComponent { /** * Optional header height */ - headerHeight: PropTypes.number + headerHeight: PropTypes.number, + exchangeRate: PropTypes.number }; static defaultProps = { @@ -226,6 +227,7 @@ class Transactions extends PureComponent { tokens={this.props.tokens} collectibleContracts={this.props.collectibleContracts} contractExchangeRates={this.props.contractExchangeRates} + exchangeRate={this.props.exchangeRate} conversionRate={this.props.conversionRate} currentCurrency={this.props.currentCurrency} showAlert={this.props.showAlert} diff --git a/app/components/UI/UrlAutocomplete/index.js b/app/components/UI/UrlAutocomplete/index.js index 4c85824015e..e651425619c 100644 --- a/app/components/UI/UrlAutocomplete/index.js +++ b/app/components/UI/UrlAutocomplete/index.js @@ -147,7 +147,7 @@ class UrlAutocomplete extends Component { ); render() { - if (this.props.input.length < 2) return null; + if (this.props.input.length < 2) return ; if (this.state.results.length === 0) { return ( diff --git a/app/components/UI/WalletConnectSessionApproval/index.js b/app/components/UI/WalletConnectSessionApproval/index.js index 79b5b18adde..cfa56afa500 100644 --- a/app/components/UI/WalletConnectSessionApproval/index.js +++ b/app/components/UI/WalletConnectSessionApproval/index.js @@ -168,7 +168,27 @@ class WalletConnectSessionApproval extends Component { /** * A string that represents the selected address */ - selectedAddress: PropTypes.string + selectedAddress: PropTypes.string, + /** + * A boolean to requests permission to autosign + */ + autosign: PropTypes.bool + }; + + renderSpecialPermissions = () => { + if (this.props.autosign) { + return ( + + + {strings('accountApproval.sign_messages')} + {` `} + {strings('accountApproval.on_your_behalf')} + + + + ); + } + return null; }; render = () => { @@ -230,6 +250,7 @@ class WalletConnectSessionApproval extends Component { + {this.renderSpecialPermissions()} {strings('accountApproval.warning')} diff --git a/app/components/UI/WebviewError/index.js b/app/components/UI/WebviewError/index.js new file mode 100644 index 00000000000..1c897698846 --- /dev/null +++ b/app/components/UI/WebviewError/index.js @@ -0,0 +1,107 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Platform, Image, StyleSheet, View, Text } from 'react-native'; +import AnimatedFox from 'react-native-animated-fox'; +import StyledButton from '../StyledButton'; +import { strings } from '../../../../locales/i18n'; +import { fontStyles, colors } from '../../../styles/common'; + +const styles = StyleSheet.create({ + wrapper: { + ...StyleSheet.absoluteFillObject, + backgroundColor: colors.white, + justifyContent: 'center', + alignItems: 'center' + }, + foxWrapper: { + backgroundColor: colors.white, + marginTop: -100, + width: 110, + marginBottom: 20, + height: 110 + }, + textWrapper: { + width: 300, + justifyContent: 'center', + alignItems: 'center' + }, + image: { + alignSelf: 'center', + width: 110, + height: 110 + }, + errorTitle: { + color: colors.fontPrimary, + ...fontStyles.bold, + fontSize: 18, + marginBottom: 15 + }, + errorMessage: { + textAlign: 'center', + color: colors.fontSecondary, + ...fontStyles.normal, + fontSize: 14, + marginBottom: 10 + }, + errorInfo: { + color: colors.fontTertiary, + ...fontStyles.normal, + fontSize: 12 + }, + buttonWrapper: { + width: 120, + marginTop: 30 + } +}); + +/** + * View that renders custom error page for the browser + */ +export default class WebviewError extends Component { + static propTypes = { + /** + * error info + */ + error: PropTypes.object, + /** + * Function that reloads the page + */ + onReload: PropTypes.func + }; + + onReload = () => { + this.props.onReload(); + }; + + render() { + const { error } = this.props; + if (!error) { + return null; + } + return ( + + + {Platform.OS === 'android' ? ( + + ) : ( + + )} + + + {strings('webview_error.title')} + {strings('webview_error.message')} + {error.description ? ( + {`${strings('webview_error.reason')}: ${ + error.description + }`} + ) : null} + + + + {strings('webview_error.try_again')} + + + + ); + } +} diff --git a/app/components/Views/AccountBackupStep4/index.js b/app/components/Views/AccountBackupStep4/index.js index a4ef3999124..fa5f788c9af 100644 --- a/app/components/Views/AccountBackupStep4/index.js +++ b/app/components/Views/AccountBackupStep4/index.js @@ -35,6 +35,9 @@ const styles = StyleSheet.create({ content: { alignItems: 'flex-start' }, + passwordRequiredContent: { + marginBottom: 20 + }, title: { fontSize: 32, marginTop: 10, @@ -166,10 +169,14 @@ export default class AccountBackupStep4 extends Component { this.words = this.props.navigation.getParam('words', []); // If the user is going to the backup seed flow directly if (!this.words.length) { - const credentials = await SecureKeychain.getGenericPassword(); - if (credentials) { - this.words = await this.tryExportSeedPhrase(credentials.password); - } else { + try { + const credentials = await SecureKeychain.getGenericPassword(); + if (credentials) { + this.words = await this.tryExportSeedPhrase(credentials.password); + } else { + this.setState({ view: CONFIRM_PASSWORD }); + } + } catch (e) { this.setState({ view: CONFIRM_PASSWORD }); } } @@ -197,7 +204,7 @@ export default class AccountBackupStep4 extends Component { this.setState({ view: SEED_PHRASE, ready: true }); } catch (e) { let msg = strings('reveal_credential.warning_incorrect_password'); - if (e.toString() !== WRONG_PASSWORD_ERROR) { + if (e.toString().toLowerCase() !== WRONG_PASSWORD_ERROR.toLowerCase()) { msg = strings('reveal_credential.unknown_error'); } this.setState({ @@ -260,7 +267,7 @@ export default class AccountBackupStep4 extends Component { const { warningIncorrectPassword } = this.state; return ( - + {strings('account_backup_step_4.confirm_password')} {strings('account_backup_step_4.before_continiuing')} diff --git a/app/components/Views/AssetCard/index.js b/app/components/Views/AssetCard/index.js new file mode 100644 index 00000000000..062828b65a5 --- /dev/null +++ b/app/components/Views/AssetCard/index.js @@ -0,0 +1,89 @@ +import React, { Component } from 'react'; +import { View, StyleSheet, Text, ImageBackground } from 'react-native'; +import { colors, fontStyles } from '../../../styles/common'; +import PropTypes from 'prop-types'; +import ElevatedView from 'react-native-elevated-view'; + +const styles = StyleSheet.create({ + wrapper: { + backgroundColor: colors.white, + borderRadius: 8, + height: 200 + }, + contentWrapper: { + marginHorizontal: 30, + marginVertical: 30 + }, + title: { + ...fontStyles.normal, + fontSize: 12 + }, + balance: { + ...fontStyles.normal, + fontSize: 40 + }, + balanceFiat: { + ...fontStyles.normal, + fontSize: 12, + color: colors.grey200 + }, + description: { + ...fontStyles.normal, + fontSize: 10, + color: colors.grey500, + marginBottom: 20 + }, + descriptionWrapper: { + flex: 1, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'flex-end' + }, + watermarkWrapper: { + flex: 1 + }, + watermarkImage: { + flex: 1, + borderRadius: 8, + width: '70%', + left: '30%', + opacity: 0.5 + } +}); + +const paymentChannelWatermark = require('../../../images/payment-channel-watermark.png'); // eslint-disable-line + +/** + * View that displays an asset card + */ +export default class AssetCard extends Component { + static propTypes = { + balance: PropTypes.string, + balanceFiat: PropTypes.string, + description: PropTypes.string + }; + + render() { + const { balance, balanceFiat, description } = this.props; + return ( + + + + Balance + {balance} + {balanceFiat} + + {description && ( + + {description} + + )} + + + ); + } +} diff --git a/app/components/Views/Browser/index.js b/app/components/Views/Browser/index.js index 04ae0db83cb..e48de3cb1fa 100644 --- a/app/components/Views/Browser/index.js +++ b/app/components/Views/Browser/index.js @@ -8,6 +8,7 @@ import { getBrowserViewNavbarOptions } from '../../UI/Navbar'; import { captureScreen } from 'react-native-view-shot'; import Logger from '../../../util/Logger'; import BrowserTab from '../BrowserTab'; +import AppConstants from '../../../core/AppConstants'; const margin = 16; const THUMB_WIDTH = Dimensions.get('window').width / 2 - margin * 2; @@ -83,10 +84,10 @@ class Browser extends PureComponent { this.tabs[tab.id] = React.createElement(BrowserTab, { id: tab.id, key: `tab_${tab.id}`, - initialUrl: tab.url || 'about:blank', + initialUrl: tab.url || AppConstants.HOMEPAGE_URL, updateTabInfo: (url, tabID) => this.updateTabInfo(url, tabID), showTabs: () => this.showTabs(), - newTab: () => this.newTab() + newTab: url => this.newTab(url) }); } }); @@ -132,8 +133,8 @@ class Browser extends PureComponent { } }; - newTab = () => { - this.props.createNewTab('about:blank'); + newTab = url => { + this.props.createNewTab(url || AppConstants.HOMEPAGE_URL); setTimeout(() => { const { tabs } = this.props; this.switchToTab(tabs[tabs.length - 1]); diff --git a/app/components/Views/BrowserHome/__snapshots__/index.test.js.snap b/app/components/Views/BrowserHome/__snapshots__/index.test.js.snap deleted file mode 100644 index d43b90029e5..00000000000 --- a/app/components/Views/BrowserHome/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BrowserHome should render correctly 1`] = ``; diff --git a/app/components/Views/BrowserHome/index.js b/app/components/Views/BrowserHome/index.js deleted file mode 100644 index 73f788ce8d4..00000000000 --- a/app/components/Views/BrowserHome/index.js +++ /dev/null @@ -1,75 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import HomePage from '../../UI/HomePage'; -import onUrlSubmit from '../../../util/browser'; - -/** - * Complete Web browser component with URL entry and history management - */ -class BrowserHome extends Component { - static defaultProps = { - defaultProtocol: 'https://' - }; - - static propTypes = { - /** - * Protocol string to append to URLs that have none - */ - defaultProtocol: PropTypes.string, - /** - * Initial URL to load in the WebView - */ - defaultURL: PropTypes.string, - /** - * Active search engine - */ - searchEngine: PropTypes.string, - /** - * react-navigation object used to switch between screens - */ - navigation: PropTypes.object, - /** - * Time to auto-lock the app after it goes in background mode - */ - goToUrl: PropTypes.func - }; - - state = { - url: this.props.defaultURL || '' - }; - - mounted = false; - lockTimer = null; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - go = async url => { - this.props.goToUrl(url); - }; - - onInitialUrlSubmit = async url => { - if (url === '') { - return false; - } - const { defaultProtocol, searchEngine } = this.props; - const sanitizedInput = onUrlSubmit(url, searchEngine, defaultProtocol); - await this.go(sanitizedInput); - }; - - render = () => ( - - ); -} - -const mapStateToProps = state => ({ - searchEngine: state.settings.searchEngine -}); - -export default connect(mapStateToProps)(BrowserHome); diff --git a/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap index 60fd15309b9..d86460d8f62 100644 --- a/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap +++ b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap @@ -12,42 +12,13 @@ exports[`Browser should render correctly 1`] = ` ] } > - - - - - - - - - - - - - - + `; diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js index 68479d3bf42..8e41eec49cd 100644 --- a/app/components/Views/BrowserTab/index.js +++ b/app/components/Views/BrowserTab/index.js @@ -1,6 +1,5 @@ import React, { PureComponent } from 'react'; import { - Dimensions, Text, ActivityIndicator, Platform, @@ -9,7 +8,6 @@ import { View, TouchableWithoutFeedback, Alert, - Animated, TouchableOpacity, Linking, Keyboard, @@ -21,7 +19,7 @@ import Web3Webview from 'react-native-web3-webview'; import Icon from 'react-native-vector-icons/FontAwesome'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'; -import IonIcon from 'react-native-vector-icons/Ionicons'; +import BrowserBottomBar from '../../UI/BrowserBottomBar'; import PropTypes from 'prop-types'; import RNFS from 'react-native-fs'; import Share from 'react-native-share'; // eslint-disable-line import/default @@ -30,16 +28,11 @@ import BackgroundBridge from '../../../core/BackgroundBridge'; import Engine from '../../../core/Engine'; import PhishingModal from '../../UI/PhishingModal'; import WebviewProgressBar from '../../UI/WebviewProgressBar'; -import BrowserHome from '../../Views/BrowserHome'; import { colors, baseStyles, fontStyles } from '../../../styles/common'; import Networks from '../../../util/networks'; import Logger from '../../../util/Logger'; import onUrlSubmit, { getHost } from '../../../util/browser'; -import { - SPA_urlChangeListener, - JS_WINDOW_INFORMATION, - JS_WINDOW_INFORMATION_HEIGHT -} from '../../../util/browserSripts'; +import { SPA_urlChangeListener, JS_WINDOW_INFORMATION, JS_DESELECT_TEXT } from '../../../util/browserSripts'; import resolveEnsToIpfsContentId from '../../../lib/ens-ipfs/resolver'; import Button from '../../UI/Button'; import { strings } from '../../../../locales/i18n'; @@ -47,8 +40,9 @@ import URL from 'url-parse'; import Modal from 'react-native-modal'; import UrlAutocomplete from '../../UI/UrlAutocomplete'; import AccountApproval from '../../UI/AccountApproval'; +import WebviewError from '../../UI/WebviewError'; import { approveHost } from '../../../actions/privacy'; -import { addBookmark } from '../../../actions/bookmarks'; +import { addBookmark, removeBookmark } from '../../../actions/bookmarks'; import { addToHistory, addToWhitelist } from '../../../actions/browser'; import { setTransactionObject } from '../../../actions/transaction'; import DeviceSize from '../../../util/DeviceSize'; @@ -57,14 +51,16 @@ import SearchApi from 'react-native-search-api'; import DeeplinkManager from '../../../core/DeeplinkManager'; import Branch from 'react-native-branch'; import WatchAssetRequest from '../../UI/WatchAssetRequest'; -import TabCountIcon from '../../UI/Tabs/TabCountIcon'; import Analytics from '../../../core/Analytics'; import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; import { toggleNetworkModal } from '../../../actions/modals'; +import setOnboardingWizardStep from '../../../actions/wizard'; +import OnboardingWizard from '../../UI/OnboardingWizard'; +import BackupAlert from '../../UI/BackupAlert'; +import DrawerStatusTracker from '../../../core/DrawerStatusTracker'; -const HOMEPAGE_URL = 'about:blank'; +const { HOMEPAGE_URL } = AppConstants; const SUPPORTED_TOP_LEVEL_DOMAINS = ['eth', 'test']; -const BOTTOM_NAVBAR_HEIGHT = Platform.OS === 'ios' && DeviceSize.isIphoneX() ? 86 : 60; const styles = StyleSheet.create({ wrapper: { @@ -78,17 +74,6 @@ const styles = StyleSheet.create({ width: 0, height: 0 }, - icon: { - color: colors.grey500, - height: 28, - lineHeight: 28, - textAlign: 'center', - width: 36, - alignSelf: 'center' - }, - disabledIcon: { - color: colors.grey100 - }, progressBarWrapper: { height: 3, width: '100%', @@ -115,87 +100,65 @@ const styles = StyleSheet.create({ position: 'absolute', zIndex: 99999999, width: 200, - borderWidth: StyleSheet.hairlineWidth, + borderWidth: 1, borderColor: colors.grey100, - backgroundColor: colors.grey000 + backgroundColor: colors.white, + borderRadius: 10, + paddingBottom: 5, + paddingTop: 10 }, optionsWrapperAndroid: { - top: 0, - right: 0, - elevation: 5 + shadowColor: colors.grey400, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.5, + shadowRadius: 3, + bottom: 65, + right: 5 }, optionsWrapperIos: { shadowColor: colors.grey400, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.5, shadowRadius: 3, - bottom: 75, - right: 3 + bottom: 90, + right: 5 }, option: { - backgroundColor: colors.grey000, + paddingVertical: 10, + height: 44, + paddingHorizontal: 15, + backgroundColor: colors.white, flexDirection: 'row', - alignItems: 'flex-start', + alignItems: 'center', justifyContent: 'flex-start', marginTop: Platform.OS === 'android' ? 0 : -5 }, optionText: { - fontSize: 14, - color: colors.fontPrimary, - ...fontStyles.normal + fontSize: 16, + lineHeight: 16, + alignSelf: 'center', + justifyContent: 'center', + marginTop: 3, + color: colors.blue, + ...fontStyles.fontPrimary }, - optionIcon: { - width: 18, - color: colors.grey500, + optionIconWrapper: { flex: 0, - height: 15, - lineHeight: 15, + borderRadius: 5, + backgroundColor: colors.blue000, + padding: 3, marginRight: 10, - textAlign: 'center', alignSelf: 'center' }, - webview: { - ...baseStyles.flexGrow - }, - bottomBar: { - backgroundColor: colors.grey000, - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - paddingTop: Platform.OS === 'ios' && DeviceSize.isIphoneX() ? 15 : 12, - paddingBottom: Platform.OS === 'ios' && DeviceSize.isIphoneX() ? 32 : 8, - flexDirection: 'row', - paddingHorizontal: 10, - flex: 1 - }, - iconSearch: { - alignSelf: 'flex-end', - alignContent: 'flex-end' - }, - iconMore: { - alignSelf: 'flex-end', - alignContent: 'flex-end' - }, - iconsLeft: { - flex: 1, - alignContent: 'flex-start', - flexDirection: 'row' - }, - iconsMiddle: { - flex: 1, - alignContent: 'center', - flexDirection: 'row', - justifyContent: 'center' - }, - iconsRight: { - flex: 1, - flexDirection: 'row', - justifyContent: 'flex-end' + optionIcon: { + color: colors.blue, + textAlign: 'center', + alignSelf: 'center', + fontSize: 18 }, - tabIcon: { - width: 30, - height: 30 + webview: { + ...baseStyles.flexGrow, + zIndex: 1 }, urlModalContent: { flexDirection: 'row', @@ -253,13 +216,11 @@ const styles = StyleSheet.create({ fullScreenModal: { flex: 1 }, - homepage: { - flex: 1, + backupAlert: { position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0 + bottom: Platform.OS === 'ios' ? (DeviceSize.isIphoneX() ? 100 : 90) : 70, + left: 16, + right: 16 } }); @@ -346,6 +307,10 @@ export class BrowserTab extends PureComponent { * Function to store bookmarks */ addBookmark: PropTypes.func, + /** + * Function to remove bookmarks + */ + removeBookmark: PropTypes.func, /** * Array of bookmarks */ @@ -373,14 +338,29 @@ export class BrowserTab extends PureComponent { /** * Function to update the tab information */ - showTabs: PropTypes.func + showTabs: PropTypes.func, + /** + * Action to set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func, + /** + * Current onboarding wizard step + */ + wizardStep: PropTypes.number, + /** + * redux flag that indicates if the user set a password + */ + passwordSet: PropTypes.bool, + /** + * redux flag that indicates if the user + * completed the seed phrase backup flow + */ + seedphraseBackedUp: PropTypes.bool }; constructor(props) { super(props); - const { scrollAnim, offsetAnim, clampedScroll } = this.initScrollVariables(); - this.state = { approvedOrigin: false, currentEnsName: null, @@ -388,88 +368,88 @@ export class BrowserTab extends PureComponent { currentPageUrl: '', currentPageIcon: undefined, entryScriptWeb3: null, + homepageScripts: null, fullHostname: '', hostname: '', - inputValue: '', + inputValue: props.initialUrl || HOMEPAGE_URL, autocompleteInputValue: '', - ipfsGateway: 'https://ipfs.io/ipfs/', - ipfsHash: null, + ipfsGateway: AppConstants.IPFS_DEFAULT_GATEWAY_URL, + contentId: null, ipfsWebsite: false, showApprovalDialog: false, showPhishingModal: false, timeout: false, url: props.initialUrl || HOMEPAGE_URL, - scrollAnim, - offsetAnim, - clampedScroll, contentHeight: 0, forwardEnabled: false, forceReload: false, suggestedAssetMeta: undefined, watchAsset: false, - activated: props.id === props.activeTab + activated: props.id === props.activeTab, + lastError: null }; } webview = React.createRef(); inputRef = React.createRef(); - timeoutHandler = null; snapshotTimer = null; - prevScrollOffset = 0; goingBack = false; + wizardScrollAdjusted = false; forwardHistoryStack = []; approvalRequest; accountsRequest; - clampedScrollValue = 0; - offsetValue = 0; - scrollValue = 0; - scrollStopTimer = null; - - initScrollVariables() { - const scrollAnim = Platform.OS === 'ios' ? new Animated.Value(0) : null; - const offsetAnim = Platform.OS === 'ios' ? new Animated.Value(0) : null; - let clampedScroll = null; - if (Platform.OS === 'ios') { - clampedScroll = Animated.diffClamp( - Animated.add( - scrollAnim.interpolate({ - inputRange: [0, 1], - outputRange: [0, 1], - extrapolateLeft: 'clamp' - }), - offsetAnim - ), - 0, - BOTTOM_NAVBAR_HEIGHT - ); + /** + * Check that page metadata is available and call callback + * if not, get metadata first + */ + checkForPageMeta = callback => { + const { currentPageTitle } = this.state; + if (!currentPageTitle || currentPageTitle !== {}) { + // We need to get the title to add bookmark + const { current } = this.webview; + Platform.OS === 'ios' + ? current && current.evaluateJavaScript(JS_WINDOW_INFORMATION) + : current && current.injectJavaScript(JS_WINDOW_INFORMATION); } - - return { scrollAnim, offsetAnim, clampedScroll }; - } + setTimeout(() => { + callback(); + }, 500); + }; getPageMeta() { - return { - meta: { - title: this.state.currentPageTitle, - url: this.state.currentPageUrl - } - }; + return new Promise(resolve => { + this.checkForPageMeta(() => + resolve({ + meta: { + title: this.state.currentPageTitle || '', + url: this.state.currentPageUrl || '' + } + }) + ); + }); } + reloadFromError = () => { + this.reload(true); + }; + async componentDidMount() { - if (this.state.url !== HOMEPAGE_URL && Platform.OS === 'android' && this.isTabActive()) { - this.reload(); + if (this.isTabActive()) { + this.reload(true); + } else if (this.isTabActive() && this.isENSUrl(this.state.url)) { + this.go(this.state.url); } this.mounted = true; this.backgroundBridge = new BackgroundBridge(Engine, this.webview, { eth_sign: async payload => { const { PersonalMessageManager } = Engine.context; try { + const pageMeta = await this.getPageMeta(); const rawSig = await PersonalMessageManager.addUnapprovedMessageAsync({ data: payload.params[1], from: payload.params[0], - ...this.getPageMeta() + ...pageMeta }); return Promise.resolve({ result: rawSig, jsonrpc: payload.jsonrpc, id: payload.id }); } catch (error) { @@ -479,10 +459,11 @@ export class BrowserTab extends PureComponent { personal_sign: async payload => { const { PersonalMessageManager } = Engine.context; try { + const pageMeta = await this.getPageMeta(); const rawSig = await PersonalMessageManager.addUnapprovedMessageAsync({ data: payload.params[0], from: payload.params[1], - ...this.getPageMeta() + ...pageMeta }); return Promise.resolve({ result: rawSig, jsonrpc: payload.jsonrpc, id: payload.id }); } catch (error) { @@ -492,11 +473,12 @@ export class BrowserTab extends PureComponent { eth_signTypedData: async payload => { const { TypedMessageManager } = Engine.context; try { + const pageMeta = await this.getPageMeta(); const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( { data: payload.params[0], from: payload.params[1], - ...this.getPageMeta() + ...pageMeta }, 'V1' ); @@ -508,11 +490,12 @@ export class BrowserTab extends PureComponent { eth_signTypedData_v3: async payload => { const { TypedMessageManager } = Engine.context; try { + const pageMeta = await this.getPageMeta(); const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( { data: payload.params[1], from: payload.params[0], - ...this.getPageMeta() + ...pageMeta }, 'V3' ); @@ -534,7 +517,8 @@ export class BrowserTab extends PureComponent { // Otherwise we don't get enough time to load the metadata // (title, icon, etc) - setTimeout(() => { + setTimeout(async () => { + await this.getPageMeta(); this.setState({ showApprovalDialog: true }); }, 1000); } @@ -585,7 +569,60 @@ export class BrowserTab extends PureComponent { }, metamask_isApproved: async ({ hostname }) => ({ isApproved: !!this.props.approvedHosts[hostname] - }) + }), + metamask_removeFavorite: ({ params }) => { + const promise = new Promise((resolve, reject) => { + if (!this.isHomepage()) { + reject({ error: 'unauthorized' }); + } + + Alert.alert(strings('browser.remove_bookmark_title'), strings('browser.remove_bookmark_msg'), [ + { + text: strings('browser.cancel'), + onPress: () => { + resolve({ + favorites: this.props.bookmarks + }); + }, + style: 'cancel' + }, + { + text: strings('browser.yes'), + onPress: () => { + const bookmark = { url: params[0] }; + this.props.removeBookmark(bookmark); + resolve({ + favorites: this.props.bookmarks + }); + } + } + ]); + }); + return promise; + }, + metamask_showTutorial: () => { + this.wizardScrollAdjusted = false; + this.props.setOnboardingWizardStep(1); + this.props.navigation.navigate('WalletView'); + + return Promise.resolve({ result: true }); + }, + metamask_showAutocomplete: () => { + this.fromHomepage = true; + this.setState( + { + autocompleteInputValue: '' + }, + () => { + this.showUrlModal(true); + setTimeout(() => { + this.fromHomepage = false; + }, 1500); + } + ); + + return Promise.resolve({ result: true }); + } }); const entryScriptWeb3 = @@ -599,34 +636,47 @@ export class BrowserTab extends PureComponent { ? `'${this.props.network}'` : `'${Networks[this.props.networkType].networkId}'` ); - await this.setState({ entryScriptWeb3: updatedentryScriptWeb3 + SPA_urlChangeListener }); + + const homepageScripts = ` + window.__mmFavorites = ${JSON.stringify(this.props.bookmarks)}; + window.__mmSearchEngine="${this.props.searchEngine}"; + `; + + await this.setState({ entryScriptWeb3: updatedentryScriptWeb3 + SPA_urlChangeListener, homepageScripts }); Engine.context.AssetsController.hub.on('pendingSuggestedAsset', suggestedAssetMeta => { if (!this.isTabActive()) return false; this.setState({ watchAsset: true, suggestedAssetMeta }); }); - Branch.subscribe(this.handleDeeplinks); - - if (Platform.OS === 'ios') { - this.state.scrollAnim.addListener(({ value }) => { - const diff = value - this.scrollValue; - this.scrollValue = value; - this.clampedScrollValue = Math.min(Math.max(this.clampedScrollValue + diff, 0), BOTTOM_NAVBAR_HEIGHT); - }); - - this.state.offsetAnim.addListener(({ value }) => { - this.offsetValue = value; - }); - } else { - this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide); + // Deeplink handling + this.unsubscribeFromBranch = Branch.subscribe(this.handleDeeplinks); + // Check if there's a deeplink pending from launch + const pendingDeeplink = DeeplinkManager.getPendingDeeplink(); + if (pendingDeeplink) { + // Expire it to avoid duplicate actions + DeeplinkManager.expireDeeplink(); + // Handle it + setTimeout(() => { + this.handleBranchDeeplink(pendingDeeplink); + }, 1000); } + this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide); + // Listen to network changes Engine.context.TransactionController.hub.on('networkChange', this.reload); BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBackPress); + + if (Platform.OS === 'android') { + DrawerStatusTracker.hub.on('drawer::open', this.drawerOpenHandler); + } } + drawerOpenHandler = () => { + this.dismissTextSelectionIfNeeded(); + }; + handleDeeplinks = async ({ error, params }) => { if (!this.isTabActive()) return false; if (error) { @@ -634,8 +684,7 @@ export class BrowserTab extends PureComponent { return; } if (params['+non_branch_link']) { - const dm = new DeeplinkManager(this.props.navigation); - dm.parse(params['+non_branch_link']); + this.handleBranchDeeplink(params['+non_branch_link']); } else if (params.spotlight_identifier) { setTimeout(() => { this.props.navigation.setParams({ @@ -647,10 +696,17 @@ export class BrowserTab extends PureComponent { } }; + handleBranchDeeplink(deeplink_url) { + Logger.log('Branch Deeplink detected!', deeplink_url); + DeeplinkManager.parse(deeplink_url, url => { + this.openNewTab(url); + }); + } + handleAndroidBackPress = () => { if (!this.isTabActive()) return false; - if (this.state.url === HOMEPAGE_URL && this.props.navigation.getParam('url', null) === null) { + if (this.isHomepage() && this.props.navigation.getParam('url', null) === null) { return false; } this.goBack(); @@ -700,20 +756,25 @@ export class BrowserTab extends PureComponent { // Remove all Engine listeners Engine.context.AssetsController.hub.removeAllListeners(); Engine.context.TransactionController.hub.removeListener('networkChange', this.reload); - if (Platform.OS === 'ios') { - this.state.scrollAnim && this.state.scrollAnim.removeAllListeners(); - this.state.offsetAnim && this.state.offsetAnim.removeAllListeners(); - } else { - this.keyboardDidHideListener && this.keyboardDidHideListener.remove(); + this.keyboardDidHideListener && this.keyboardDidHideListener.remove(); + if (Platform.OS === 'android') { BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBackPress); + DrawerStatusTracker && DrawerStatusTracker.hub && DrawerStatusTracker.hub.removeAllListeners(); + } + if (this.unsubscribeFromBranch) { + this.unsubscribeFromBranch(); + this.unsubscribeFromBranch = null; } } keyboardDidHide = () => { if (!this.isTabActive()) return false; - const showUrlModal = (this.props.navigation && this.props.navigation.getParam('showUrlModal', false)) || false; - if (showUrlModal) { - this.hideUrlModal(); + if (!this.fromHomepage) { + const showUrlModal = + (this.props.navigation && this.props.navigation.getParam('showUrlModal', false)) || false; + if (showUrlModal) { + this.hideUrlModal(); + } } }; @@ -753,26 +814,27 @@ export class BrowserTab extends PureComponent { } go = async url => { - const hasProtocol = url.match(/^[a-z]*:\/\//) || url === HOMEPAGE_URL; + const hasProtocol = url.match(/^[a-z]*:\/\//) || this.isHomepage(url); const sanitizedURL = hasProtocol ? url : `${this.props.defaultProtocol}${url}`; const urlObj = new URL(sanitizedURL); const { hostname, query, pathname } = urlObj; - const { ipfsGateway } = this.props; - let ipfsContent = null; let currentEnsName = null; - let ipfsHash = null; - + let contentId = null; + let contentUrl = null; + let contentType = null; if (this.isENSUrl(sanitizedURL)) { - ipfsContent = await this.handleIpfsContent(sanitizedURL, { hostname, query, pathname }); - - if (ipfsContent) { + const { url: tmpUrl, hash, type } = await this.handleIpfsContent(sanitizedURL, { + hostname, + query, + pathname + }); + contentUrl = tmpUrl; + contentType = type; + if (contentUrl) { const urlObj = new URL(sanitizedURL); currentEnsName = urlObj.hostname; - ipfsHash = ipfsContent - .replace(ipfsGateway, '') - .split('/') - .shift(); + contentId = hash; } // Needed for the navbar to mask the URL this.props.navigation.setParams({ @@ -780,79 +842,56 @@ export class BrowserTab extends PureComponent { currentEnsName: urlObj.hostname }); } - const urlToGo = ipfsContent || sanitizedURL; + const urlToGo = contentUrl || sanitizedURL; if (this.isAllowedUrl(urlToGo)) { this.setState({ url: urlToGo, progress: 0, - ipfsWebsite: !!ipfsContent, + ipfsWebsite: !!contentUrl, inputValue: sanitizedURL, currentEnsName, - ipfsHash, + contentId, + contentType, hostname: this.formatHostname(hostname) }); this.updateTabInfo(sanitizedURL); - this.timeoutHandler && clearTimeout(this.timeoutHandler); - if (urlToGo !== HOMEPAGE_URL) { - this.timeoutHandler = setTimeout(() => { - this.urlTimedOut(urlToGo); - }, 60000); - } - return sanitizedURL; } this.handleNotAllowedUrl(urlToGo, hostname); return null; }; - urlTimedOut(url) { - Logger.log('Browser::url::Timeout!', url); - } - - urlNotFound(url) { - Logger.log('Browser::url::Not found!', url); - } - - urlNotSupported(url) { - Logger.log('Browser::url::Not supported!', url); - } - - urlErrored(url) { - Logger.log('Browser::url::Unknown error!', url); - } - async handleIpfsContent(fullUrl, { hostname, pathname, query }) { const { provider } = Engine.context.NetworkController; const { ipfsGateway } = this.props; - - let ipfsHash; + let gatewayUrl; try { - ipfsHash = await resolveEnsToIpfsContentId({ provider, name: hostname }); + const { type, hash } = await resolveEnsToIpfsContentId({ provider, name: hostname }); + if (type === 'ipfs-ns') { + gatewayUrl = `${ipfsGateway}${hash}${pathname || '/'}${query || ''}`; + const response = await fetch(gatewayUrl); + const statusCode = response.status; + if (statusCode >= 400) { + Logger.log('Status code ', statusCode, gatewayUrl); + this.urlNotFound(gatewayUrl); + return null; + } + } else if (type === 'swarm-ns') { + gatewayUrl = `${AppConstants.SWARM_DEFAULT_GATEWAY_URL}${hash}${pathname || '/'}${query || ''}`; + } + + return { + url: gatewayUrl, + hash, + type + }; } catch (err) { - this.timeoutHandler && clearTimeout(this.timeoutHandler); Logger.error('Failed to resolve ENS name', err); - err === 'unsupport' ? this.urlNotSupported(fullUrl) : this.urlErrored(fullUrl); + Alert.alert(strings('browser.error'), strings('browser.failed_to_resolve_ens_name')); return null; } - - const gatewayUrl = `${ipfsGateway}${ipfsHash}${pathname || '/'}${query || ''}`; - - try { - const response = await fetch(gatewayUrl, { method: 'HEAD' }); - const statusCode = response.status; - if (statusCode !== 200) { - this.urlNotFound(gatewayUrl); - return null; - } - return gatewayUrl; - } catch (err) { - // If there's an error our fallback mechanism is - // to point straight to the ipfs gateway - Logger.error('Failed to fetch ipfs website via ens', err); - return `https://ipfs.io/ipfs/${ipfsHash}/`; - } } onUrlInputSubmit = async (input = null) => { @@ -875,8 +914,9 @@ export class BrowserTab extends PureComponent { setTimeout(() => { this.goingBack = false; }, 500); - - if (this.initialUrl && this.state.inputValue !== this.initialUrl) { + if (this.initialUrl === this.state.inputValue) { + this.goBackToHomepage(); + } else { const { current } = this.webview; current && current.goBack(); setTimeout(() => { @@ -886,43 +926,13 @@ export class BrowserTab extends PureComponent { url: this.state.inputValue }); }, 100); - } else { - this.goBackToHomepage(); } }; - goBackToHomepage = () => { + goBackToHomepage = async () => { this.toggleOptionsIfNeeded(); - this.props.navigation.setParams({ - url: null - }); - - const { scrollAnim, offsetAnim, clampedScroll } = this.initScrollVariables(); - - this.setState({ - approvedOrigin: false, - currentEnsName: null, - currentPageTitle: '', - currentPageUrl: '', - currentPageIcon: undefined, - fullHostname: '', - hostname: '', - inputValue: '', - autocompleteInputValue: '', - ipfsHash: null, - ipfsWebsite: false, - showApprovalDialog: false, - showPhishingModal: false, - timeout: false, - url: HOMEPAGE_URL, - scrollAnim, - offsetAnim, - clampedScroll, - contentHeight: 0, - forwardEnabled: false - }); - - this.initialUrl = null; + await this.go(HOMEPAGE_URL); + this.reload(true); Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_HOME); }; @@ -935,7 +945,7 @@ export class BrowserTab extends PureComponent { if (this.canGoForward()) { this.toggleOptionsIfNeeded(); const { current } = this.webview; - this.setState({ forwardEnabled: false }); + this.setState({ forwardEnabled: current.canGoForward() }); current && current.goForward(); setTimeout(() => { this.props.navigation.setParams({ @@ -946,19 +956,18 @@ export class BrowserTab extends PureComponent { } }; - reload = () => { + reload = (force = false) => { this.toggleOptionsIfNeeded(); - if (Platform.OS === 'ios') { + if (!force) { const { current } = this.webview; current && current.reload(); } else { + const url2Reload = this.state.inputValue; // Force unmount the webview to avoid caching problems this.setState({ forceReload: true }, () => { setTimeout(() => { this.setState({ forceReload: false }, () => { - setTimeout(() => { - this.go(this.state.inputValue); - }, 300); + this.go(url2Reload); }); }, 300); }); @@ -972,14 +981,7 @@ export class BrowserTab extends PureComponent { Alert.alert(strings('browser.error'), strings('browser.bookmark_already_exists')); return false; } - if (!this.state.currentPageTitle) { - // We need to get the title to add bookmark - const { current } = this.webview; - Platform.OS === 'ios' - ? current.evaluateJavaScript(JS_WINDOW_INFORMATION) - : current.injectJavaScript(JS_WINDOW_INFORMATION); - } - setTimeout(() => { + this.checkForPageMeta(() => this.props.navigation.push('AddBookmarkView', { title: this.state.currentPageTitle || '', url: this.state.inputValue, @@ -999,9 +1001,14 @@ export class BrowserTab extends PureComponent { Logger.error('Error adding to spotlight', e); } } + const homepageScripts = ` + window.__mmFavorites = ${JSON.stringify(this.props.bookmarks)}; + window.__mmSearchEngine="${this.props.searchEngine}"; + `; + this.setState({ homepageScripts }); } - }); - }, 500); + }) + ); Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_ADD_TO_FAVORITE); }; @@ -1021,10 +1028,13 @@ export class BrowserTab extends PureComponent { }, 300); }; - openNewTab = () => { + onNewTabPress = () => { + this.openNewTab(); + }; + openNewTab = url => { this.toggleOptionsIfNeeded(); setTimeout(() => { - this.props.newTab(); + this.props.newTab(url); }, 300); }; @@ -1036,6 +1046,17 @@ export class BrowserTab extends PureComponent { Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_OPEN_IN_BROWSER); }; + dismissTextSelectionIfNeeded() { + if (this.isTabActive() && Platform.OS === 'android') { + const { current } = this.webview; + if (current) { + setTimeout(() => { + current.injectJavaScript(JS_DESELECT_TEXT); + }, 50); + } + } + } + toggleOptionsIfNeeded() { if ( this.props.navigation && @@ -1047,6 +1068,8 @@ export class BrowserTab extends PureComponent { } toggleOptions = () => { + this.dismissTextSelectionIfNeeded(); + this.props.navigation && this.props.navigation.setParams({ ...this.props.navigation.state.params, @@ -1066,16 +1089,6 @@ export class BrowserTab extends PureComponent { return; } switch (data.type) { - case 'GET_HEIGHT': - this.setState({ contentHeight: data.payload.height }); - // Reset the navbar every time we change the page - if (Platform.OS === 'ios') { - setTimeout(() => { - this.state.scrollAnim.setValue(0); - this.state.offsetAnim.setValue(0); - }, 100); - } - break; case 'NAV_CHANGE': { const { url, title } = data.payload; this.setState({ @@ -1086,11 +1099,6 @@ export class BrowserTab extends PureComponent { }); this.props.navigation.setParams({ url: data.payload.url, silent: true, showUrlModal: false }); this.updateTabInfo(data.payload.url); - if (Platform.OS === 'ios') { - setTimeout(() => { - this.resetBottomBarPosition(); - }, 100); - } break; } case 'INPAGE_REQUEST': @@ -1111,41 +1119,34 @@ export class BrowserTab extends PureComponent { } }; - resetBottomBarPosition() { - const { scrollAnim, offsetAnim, clampedScroll } = this.initScrollVariables(); - - this.mounted && - this.setState({ - scrollAnim, - offsetAnim, - clampedScroll - }); - } - onPageChange = ({ url }) => { const { ipfsGateway } = this.props; - if ((this.goingBack && url === 'about:blank') || (this.initialUrl === url && url === 'about:blank')) { - this.goBackToHomepage(); - return; - } - - // Reset the navbar every time we change the page - if (Platform.OS === 'ios') { - this.resetBottomBarPosition(); - } - this.forwardHistoryStack = []; const data = {}; const urlObj = new URL(url); data.fullHostname = urlObj.hostname; if (!this.state.ipfsWebsite) { - data.inputValue = url; + // If we're coming from a link / internal redirect + // We need to re-check if it's an ens url, + // then act accordingly + if (!this.isENSUrl(url)) { + data.inputValue = url; + } else { + this.go(url); + } } else if (url.search(`${AppConstants.IPFS_OVERRIDE_PARAM}=false`) === -1) { - data.inputValue = url.replace( - `${ipfsGateway}${this.state.ipfsHash}/`, - `https://${this.state.currentEnsName}/` - ); + if (this.state.contentType === 'ipfs-ns') { + data.inputValue = url.replace( + `${ipfsGateway}${this.state.contentId}/`, + `https://${this.state.currentEnsName}/` + ); + } else { + data.inputValue = url.replace( + `${AppConstants.SWARM_GATEWAY_URL}${this.state.contentId}/`, + `https://${this.state.currentEnsName}/` + ); + } } else if (this.isENSUrl(url)) { this.go(url); return; @@ -1184,12 +1185,6 @@ export class BrowserTab extends PureComponent { }; onLoadEnd = () => { - if (Platform.OS === 'ios') { - setTimeout(() => { - this.state.scrollAnim.setValue(0); - }, 100); - } - const { approvedHosts, privacyMode } = this.props; if (!privacyMode || approvedHosts[this.state.fullHostname]) { this.backgroundBridge.enableAccounts(); @@ -1204,19 +1199,23 @@ export class BrowserTab extends PureComponent { }, 500); // Let's wait for potential redirects that might break things - if (!this.initialUrl || this.initialUrl === HOMEPAGE_URL) { + if (!this.initialUrl || this.isHomepage(this.initialUrl)) { setTimeout(() => { this.initialUrl = this.state.inputValue; }, 1000); } - // We need to get the title of the page and the height const { current } = this.webview; + // Inject favorites on the homepage + if (this.isHomepage() && current) { + const js = this.state.homepageScripts; + Platform.OS === 'ios' ? current.evaluateJavaScript(js) : current.injectJavaScript(js); + } + }; - Platform.OS === 'ios' - ? current.evaluateJavaScript(JS_WINDOW_INFORMATION_HEIGHT) - : current.injectJavaScript(JS_WINDOW_INFORMATION_HEIGHT); - clearTimeout(this.timeoutHandler); + onError = ({ nativeEvent: errorInfo }) => { + Logger.log(errorInfo); + this.setState({ lastError: errorInfo }); }; renderLoader = () => ( @@ -1237,15 +1236,19 @@ export class BrowserTab extends PureComponent { Platform.OS === 'android' ? styles.optionsWrapperAndroid : styles.optionsWrapperIos ]} > - {this.renderNonHomeOptions()} - ) : null} - {Platform.OS === 'android' && this.canGoForward() ? ( - - ) : null} - -