From b635691c25ef53e0b0488eb5ec5f1091a69fee91 Mon Sep 17 00:00:00 2001 From: Dmitry Novotochinov Date: Thu, 1 Aug 2019 18:49:33 +0300 Subject: [PATCH] recovery flow v1 Signed-off-by: Dmitry Novotochinov --- .env | 2 +- .env.jenkins | 2 +- externs.js | 3 +- .../status/ethereum/module/StatusModule.java | 19 + .../ios/RCTStatus/RCTStatus.m | 12 +- resources/images/ui/keycard-logo-blue.png | Bin 0 -> 1498 bytes resources/images/ui/keycard-logo-gray.png | Bin 0 -> 1609 bytes src/status_im/ethereum/mnemonic.cljs | 20 +- src/status_im/events.cljs | 7 - src/status_im/hardwallet/core.cljs | 18 +- src/status_im/init/core.cljs | 4 + src/status_im/multiaccounts/login/core.cljs | 22 +- src/status_im/multiaccounts/recover/core.cljs | 266 ++++++++++--- src/status_im/native_module/core.cljs | 3 + src/status_im/native_module/impl/module.cljs | 8 + src/status_im/react_native/resources.cljs | 2 + src/status_im/signals/core.cljs | 10 +- .../action_button/action_button.cljs | 10 +- .../ui/components/tooltip/styles.cljs | 1 + src/status_im/ui/screens/intro/views.cljs | 30 +- .../ui/screens/keycard/onboarding/views.cljs | 55 --- .../ui/screens/multiaccounts/login/views.cljs | 8 +- .../screens/multiaccounts/recover/views.cljs | 352 +++++++++++++++++- .../ui/screens/routing/intro_login_stack.cljs | 16 +- src/status_im/ui/screens/routing/screens.cljs | 10 +- src/status_im/ui/screens/views.cljs | 6 +- test/appium/views/recover_access_view.py | 18 +- test/appium/views/sign_in_view.py | 19 +- .../status_im/test/ethereum/mnemonic.cljs | 24 +- .../test/multiaccounts/recover/core.cljs | 52 +-- translations/en.json | 15 +- 31 files changed, 796 insertions(+), 218 deletions(-) create mode 100644 resources/images/ui/keycard-logo-blue.png create mode 100644 resources/images/ui/keycard-logo-gray.png diff --git a/.env b/.env index bb9bd3ee93..9aef94b941 100644 --- a/.env +++ b/.env @@ -7,7 +7,7 @@ ETHEREUM_DEV_CLUSTER=1 EXTENSIONS=0 FLEET=eth.beta GROUP_CHATS_ENABLED=1 -HARDWALLET_ENABLED=0 +HARDWALLET_ENABLED=1 LOG_LEVEL_STATUS_GO=info LOG_LEVEL=debug MAILSERVER_CONFIRMATIONS_ENABLED=1 diff --git a/.env.jenkins b/.env.jenkins index 674176602b..b4aded76cd 100644 --- a/.env.jenkins +++ b/.env.jenkins @@ -6,7 +6,7 @@ ETHEREUM_DEV_CLUSTER=1 EXTENSIONS=0 FLEET=eth.beta GROUP_CHATS_ENABLED=1 -HARDWALLET_ENABLED=0 +HARDWALLET_ENABLED=1 LOG_LEVEL_STATUS_GO=info LOG_LEVEL=debug MAILSERVER_CONFIRMATIONS_ENABLED=1 diff --git a/externs.js b/externs.js index c013b8572a..2be39fa276 100644 --- a/externs.js +++ b/externs.js @@ -568,5 +568,6 @@ var TopLevel = { "multiAccountDeriveAddresses" : function () {}, "multiAccountReset" : function () {}, "multiAccountLoadAccount" : function () {}, - "multiAccountStoreAccount" : function () {} + "multiAccountStoreAccount" : function () {}, + "multiAccountImportMnemonic" : function () {}, } diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java index 81db30c3c1..734fab11c8 100644 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java @@ -774,6 +774,25 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL StatusThreadPoolExecutor.getInstance().execute(r); } + @ReactMethod + public void multiAccountImportMnemonic(final String json, final Callback callback) { + Log.d(TAG, "multiAccountImportMnemonic"); + if (!checkAvailability()) { + callback.invoke(false); + return; + } + Runnable r = new Runnable() { + @Override + public void run() { + String res = Statusgo.multiAccountImportMnemonic(json); + + callback.invoke(res); + } + }; + + StatusThreadPoolExecutor.getInstance().execute(r); + } + private String createIdentifier() { return UUID.randomUUID().toString(); } diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m index 00e4230df4..7d21278420 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -384,7 +384,17 @@ RCT_EXPORT_METHOD(multiAccountStoreDerived:(NSString *)json callback(@[result]); } -//////////////////////////////////////////////////////////////////// MultiAccountDeriveAddresses +//////////////////////////////////////////////////////////////////// multiAccountImportMnemonic +RCT_EXPORT_METHOD(multiAccountImportMnemonic:(NSString *)json + callback:(RCTResponseSenderBlock)callback) { +#if DEBUG + NSLog(@"MultiAccountImportMnemonic() method called"); +#endif + NSString *result = StatusgoMultiAccountImportMnemonic(json); + callback(@[result]); +} + +//////////////////////////////////////////////////////////////////// multiAccountDeriveAddresses RCT_EXPORT_METHOD(multiAccountDeriveAddresses:(NSString *)json callback:(RCTResponseSenderBlock)callback) { #if DEBUG diff --git a/resources/images/ui/keycard-logo-blue.png b/resources/images/ui/keycard-logo-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..ad505daee459f65706e11c904c4fbcf16495d90d GIT binary patch literal 1498 zcmV<01tt24P)@~0drDELIAGL9O(c600d`2O+f$vv5yPy;pCI-LtehbA3Cx}#?ptTtRP+hro z_yIsI@$*doB5DOt2@n-&cO9gUmYlpM%f}4x# zMF2hex0Yr#j_8x8?rODpdk^NlM7;x8l*OV3CV26J?m<`=-D5hR_`6t?IGRo^&1MtG zSb0UB+)(R+dEP}lrx77rn_Ed%dxPFhkj?XZQ}Pe!m>tO9P0y8*OJG9p0^lafhL4c zn*gS@M;L!5zj#=LO{Sn#MAI1x)S1>EFqI777@6!g z90>_2Ea^@(KgH<6(HM?+Ykm&P$&pEV67X}A515XX#_}mE5 zyEvMjJwb|d$&8@#o4#klzVl1QMY=(vq0}KBWd-g)m#}?~DsTy;0#VN$#3M&B~qy2qjIinQQ)#mzp zHi5OL5I7}ym?4>wV_+-5SV#f{CJ7LjBtT%20D&0OGUJ(cAzeN?Xo`j|K#84y@$dojx%i*4g_zlyqCpElx9dLsGu@gv zG*cAaB1|)a5-z4T6gi~`Dli~QFGwd`j3p*JG({YC$x#YGF@Kx|*6R_bkXdi^?VD)# z0nH`d=1W2p$n!q#D{4d38?hZI@`{Y6XcE{IMJ<3%KiXW=c}P@~0drDELIAGL9O(c600d`2O+f$vv5yPQ+SK~#7F?VNv3 z<3<$6-jHv^1Ref$XYu%%|4sSX6o6s@^5M6G@Cj;)GXg|W)cHFRNBr><{5#pjAef?n zQEMh2K7I_RP$R4hu+qf;9~Od0o)3Nv&!8q)6(BP;C?cM?E|dui5u|dn4y!?cB|eWM ze(S}3>65QO=q7=9yu6OKz;Ubya5C zL}HY-)4zsc`24*Qr|dnoX0Qc99Ao}I(n$i0_~#}#hOrfJc|AGf$^WDqGk@b@ba=3H z4zKy{@t@n{JGn{FLP8@QqRJ!o1yrK9+b+0{L#)s}fTLjv`JA~3Vx%2}xe3bi ze2Amg!NJZuD3Ocn$v54`%x36ESIm%s6(HVgpK=lnN(LopH`;b8eg;P{VS@U)hqIRa z3MG@Y@Yyx=d8+Lyv4l?$*4v|`;DE}-8o?Dk5iv^R2r zMmL6tZq_ax&4%bCXF#u=wpJ?AtNx-i;d(%Blq+t)Aj8?x{a;R0vlOQ!ZeT@Jl$$1Uqb`%LaO#ZMd~d*G6%Q2OA1LPW zF^hQaQi`D{P9D-zC^3S(c1FVf;NXKZiM6+2-2^Q|ZPc&^I4i)q;03UZ7r-)J0Lyp* zEMx49m`&7~WKLacJEFOeKb^JGq3Kwn@dC^zUw$l zQHDcHH?A~R097ZJB0IE~$H?X(?nVkwv`7yQlqnmz2W0IjkkcW7cr)1hV7*4%xD}ud zgp;l@cg_<}eh3cGs1=Z11d?|ee_p1{J5Owk#;kz({X>w`mgruMxDf&raW}K85I}Cb zRwJ&x07czRo4&q|S_^I85^kGDuq&e?-Mv;w`Wi6)|;gtjvReWwI_M$(LIbouerNj0h;_ zHbm^ZXo_ktfSP#aYNi35UQ1#{$Jgi zST|GDxJ`JmBD6_2Hbti@g9;ANNG~V}vVx1Y#YCcrT!@kzDS&Eiry|!QB>6Y`R!w8$ zfWdxtNq4Lp!~4Zi755drhDHZsY>Jdur0;ekyQZko0%WIbFvm4T=-M s + passphrase->words + count))) + (defn- valid-word? [s] (re-matches #"^[A-z]+$" s)) -(defn valid-words? [v] - (and (valid-word-counts? v) - (every? valid-word? v))) - -(defn valid-phrase? [s] +(defn valid-length? [s] (-> s passphrase->words - valid-words?)) + valid-word-counts?)) + +(defn valid-words? [s] + (->> s + passphrase->words + (every? valid-word?))) (defn status-generated-phrase? [s] (every? dictionary (passphrase->words s))) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index cc2a81bb20..126d0f5295 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -307,13 +307,6 @@ (fn [cofx _] (multiaccounts.recover/recover-multiaccount cofx))) -(handlers/register-handler-fx - :multiaccounts.recover.callback/recover-multiaccount-success - [(re-frame/inject-cofx :random-guid-generator) - (re-frame/inject-cofx :multiaccounts.create/get-signing-phrase)] - (fn [cofx [_ result password]] - (multiaccounts.recover/on-multiaccount-recovered cofx result password))) - ;; multiaccounts login module (handlers/register-handler-fx diff --git a/src/status_im/hardwallet/core.cljs b/src/status_im/hardwallet/core.cljs index 074117352e..5584383222 100644 --- a/src/status_im/hardwallet/core.cljs +++ b/src/status_im/hardwallet/core.cljs @@ -404,18 +404,19 @@ (navigation/navigate-to-cofx :keycard-recovery-enter-mnemonic nil))) (fx/defn start-import-flow - {:events [:recovery.ui/recover-with-keycard-pressed + {:events [:recover.ui/recover-with-keycard-pressed :keycard.login.ui/recover-key-pressed]} [{:keys [db] :as cofx}] (fx/merge cofx {:db (assoc-in db [:hardwallet :flow] :import) + :dispatch [:bottom-sheet/hide-sheet] :hardwallet/check-nfc-enabled nil} (navigation/navigate-to-cofx :keycard-recovery-intro nil))) (fx/defn access-key-pressed {:events [:multiaccounts.recover.ui/recover-multiaccount-button-pressed]} [cofx] - (multiaccounts.recover/navigate-to-recover-multiaccount-screen cofx)) + {:dispatch [:bottom-sheet/show-sheet :recover-sheet]}) (fx/defn recovery-keycard-selected {:events [:recovery.ui/keycard-option-pressed]} @@ -425,19 +426,6 @@ :hardwallet/check-nfc-enabled nil} (navigation/navigate-to-cofx :keycard-onboarding-intro nil))) -;NOTE to be removed when Recovery flow will be implemented -(fx/defn enter-mnemonic-next-button-pressed - {:events [:keycard.recovery.enter-mnemonic.ui/input-submitted - :keycard.recovery.enter-mnemonic.ui/next-pressed]} - [cofx] - (recovery-keycard-selected cofx)) - -;NOTE to be removed when Recovery flow will be implemented -(fx/defn enter-mnemonic-input-changed - {:events [:keycard.recovery.enter-mnemonic.ui/input-changed]} - [{:keys [db]} input] - {:db (assoc-in db [:hardwallet :secrets :mnemonic] input)}) - (fx/defn password-option-pressed [{:keys [db] :as cofx}] (if (= (get-in db [:hardwallet :flow]) :create) diff --git a/src/status_im/init/core.cljs b/src/status_im/init/core.cljs index 145bfdc624..14400e3c6f 100644 --- a/src/status_im/init/core.cljs +++ b/src/status_im/init/core.cljs @@ -228,6 +228,9 @@ (= (get-in cofx [:db :view-id]) :create-multiaccount)) +(defn recovering-multiaccount? [cofx] + (boolean (get-in cofx [:db :multiaccounts/recover]))) + (defn- keycard-setup? [cofx] (boolean (get-in cofx [:db :hardwallet :flow]))) @@ -242,6 +245,7 @@ (stickers/init-stickers-packs) (multiaccounts.update/update-sign-in-time) #(when-not (or (creating-multiaccount? %) + (recovering-multiaccount? %) (keycard-setup? %)) (login-only-events % address stored-pns))))) diff --git a/src/status_im/multiaccounts/login/core.cljs b/src/status_im/multiaccounts/login/core.cljs index 9f92833103..7f0e273604 100644 --- a/src/status_im/multiaccounts/login/core.cljs +++ b/src/status_im/multiaccounts/login/core.cljs @@ -283,11 +283,23 @@ (fx/defn verify-multiaccount [{:keys [db] :as cofx} {:keys [realm-error]}] - (fx/merge cofx - {:db (-> db - (assoc :node/on-ready :verify-multiaccount) - (assoc :realm-error realm-error))} - (node/initialize nil))) + (if (get-in db [:multiaccounts/recover]) + (fx/merge cofx + {:db (-> db + (update :multiaccounts/recover assoc + :processing? false + :password "" + :password-confirmation "" + :password-error :recover-password-invalid) + (update :multiaccounts/recover dissoc + :password-valid?)) + :node/stop nil} + (navigation/navigate-to-cofx :recover-multiaccount-enter-password nil)) + (fx/merge cofx + {:db (-> db + (assoc :node/on-ready :verify-multiaccount) + (assoc :realm-error realm-error))} + (node/initialize nil)))) (fx/defn unknown-realm-error [cofx {:keys [realm-error erase-button]}] diff --git a/src/status_im/multiaccounts/recover/core.cljs b/src/status_im/multiaccounts/recover/core.cljs index b6a0ecd06e..1eb7dc891f 100644 --- a/src/status_im/multiaccounts/recover/core.cljs +++ b/src/status_im/multiaccounts/recover/core.cljs @@ -20,11 +20,14 @@ (defn check-phrase-errors [recovery-phrase] (cond (string/blank? recovery-phrase) :required-field - (not (mnemonic/valid-phrase? recovery-phrase)) :recovery-phrase-invalid)) + (not (mnemonic/valid-words? recovery-phrase)) :recovery-phrase-invalid + (not (mnemonic/valid-length? recovery-phrase)) :recovery-phrase-wrong-length + (not (mnemonic/status-generated-phrase? recovery-phrase)) :recovery-phrase-unknown-words)) (defn check-phrase-warnings [recovery-phrase] - (when (not (mnemonic/status-generated-phrase? recovery-phrase)) - :recovery-phrase-unknown-words)) + (cond (string/blank? recovery-phrase) :required-field + (not (mnemonic/valid-words? recovery-phrase)) :recovery-phrase-invalid + (not (mnemonic/status-generated-phrase? recovery-phrase)) :recovery-phrase-unknown-words)) (defn recover-multiaccount! [masked-passphrase password] (status/recover-multiaccount @@ -42,22 +45,31 @@ (fx/defn set-phrase [{:keys [db]} masked-recovery-phrase] (let [recovery-phrase (security/safe-unmask-data masked-recovery-phrase)] - {:db (update db :multiaccounts/recover assoc - :passphrase (string/lower-case recovery-phrase) - :passphrase-valid? (not (check-phrase-errors recovery-phrase)))})) + (fx/merge + {:db (update db :multiaccounts/recover assoc + :passphrase (string/lower-case recovery-phrase) + :passphrase-error nil + :next-button-disabled? (or (empty? recovery-phrase) + (not (mnemonic/valid-length? recovery-phrase))))}))) (fx/defn validate-phrase [{:keys [db]}] (let [recovery-phrase (get-in db [:multiaccounts/recover :passphrase])] {:db (update db :multiaccounts/recover assoc - :passphrase-error (check-phrase-errors recovery-phrase) - :passphrase-warning (check-phrase-warnings recovery-phrase))})) + :passphrase-error (check-phrase-errors recovery-phrase))})) + +(fx/defn validate-phrase-for-warnings + [{:keys [db]}] + (let [recovery-phrase (get-in db [:multiaccounts/recover :passphrase])] + {:db (update db :multiaccounts/recover assoc + :passphrase-error (check-phrase-warnings recovery-phrase))})) (fx/defn set-password [{:keys [db]} masked-password] (let [password (security/safe-unmask-data masked-password)] {:db (update db :multiaccounts/recover assoc :password password + :password-error nil :password-valid? (not (check-password-errors password)))})) (fx/defn validate-password @@ -66,52 +78,51 @@ {:db (assoc-in db [:multiaccounts/recover :password-error] (check-password-errors password))})) (fx/defn validate-recover-result - [{:keys [db] :as cofx} {:keys [error pubkey address walletAddress walletPubKey chatAddress chatPubKey]} password] - (if (empty? error) - (let [multiaccount-address (-> address - (string/lower-case) - (string/replace-first "0x" "")) - keycard-multiaccount? (boolean (get-in db [:multiaccounts/multiaccounts multiaccount-address :keycard-instance-uid]))] - (if keycard-multiaccount? - ;; trying to recover multiaccount created with keycard - {:db (-> db - (update :multiaccounts/recover assoc - :processing? false - :passphrase-error :recover-keycard-multiaccount-not-supported) - (update :multiaccounts/recover dissoc - :passphrase-valid?)) - :node/stop nil} - (let [multiaccount {:derived {constants/path-whisper-keyword {:publicKey chatPubKey - :address chatAddress} - constants/path-default-wallet-keyword {:publicKey walletPubKey - :address walletAddress}} - :address address - :mnemonic ""}] - (multiaccounts.create/on-multiaccount-created - cofx multiaccount password {:seed-backed-up? true})))) - {:db (-> db - (update :multiaccounts/recover assoc - :processing? false - :password "" - :password-error :recover-password-invalid) - (update :multiaccounts/recover dissoc - :password-valid?)) - :node/stop nil})) + [{:keys [db] :as cofx} password] + (let [multiaccount (get-in db [:multiaccounts/recover :root-key]) + multiaccount-address (-> (:address multiaccount) + (string/lower-case) + (string/replace-first "0x" "")) + keycard-multiaccount? (boolean (get-in db [:multiaccounts/multiaccounts multiaccount-address :keycard-instance-uid]))] + (if keycard-multiaccount? + ;; trying to recover multiaccount created with keycard + {:db (-> db + (update :multiaccounts/recover assoc + :processing? false + :passphrase-error :recover-keycard-multiaccount-not-supported) + (update :multiaccounts/recover dissoc + :passphrase-valid?)) + :node/stop nil} + (let [multiaccount' (assoc multiaccount :derived (get-in db [:multiaccounts/recover :derived]))] + (multiaccounts.create/on-multiaccount-created + cofx multiaccount' password {:seed-backed-up? true}))))) (fx/defn on-multiaccount-recovered - [cofx result password] - (let [data (types/json->clj result)] - (validate-recover-result cofx data password))) + {:events [:multiaccounts.recover.callback/recover-multiaccount-success] + :interceptors [(re-frame/inject-cofx :random-guid-generator) + (re-frame/inject-cofx :multiaccounts.create/get-signing-phrase)]} + [cofx password] + (validate-recover-result cofx password)) + +(fx/defn multiaccount-store-derived + [{:keys [db]}] + (let [id (get-in db [:multiaccounts/recover :root-key :id]) + password (get-in db [:multiaccounts/recover :password])] + (status/multiaccount-store-derived + id + [constants/path-whisper constants/path-default-wallet] + password + #(re-frame/dispatch [:multiaccounts.recover.callback/recover-multiaccount-success password])))) (fx/defn recover-multiaccount [{:keys [db random-guid-generator] :as cofx}] - (fx/merge - cofx - {:db (-> db - (assoc-in [:multiaccounts/recover :processing?] true) - (assoc :node/on-ready :recover-multiaccount) - (assoc :multiaccounts/new-installation-id (random-guid-generator)))} - (node/initialize nil))) + (let [{:keys [password passphrase]} (:multiaccounts/recover db)] + (fx/merge cofx + {:db (-> db + (assoc-in [:multiaccounts/recover :processing?] true) + (assoc :multiaccounts/new-installation-id (random-guid-generator))) + :multiaccounts.recover/recover-multiaccount + [(security/mask-data passphrase) password]}))) (fx/defn recover-multiaccount-with-checks [{:keys [db] :as cofx}] (let [{:keys [passphrase processing?]} (:multiaccounts/recover db)] @@ -127,9 +138,164 @@ (fx/defn navigate-to-recover-multiaccount-screen [{:keys [db] :as cofx}] (fx/merge cofx {:db (dissoc db :multiaccounts/recover)} - (navigation/navigate-to-cofx :recover nil))) + (navigation/navigate-to-cofx :recover-multiaccount nil))) (re-frame/reg-fx :multiaccounts.recover/recover-multiaccount (fn [[masked-passphrase password]] (recover-multiaccount! masked-passphrase password))) + +(re-frame/reg-fx + :multiaccounts.recover/import-mnemonic + (fn [{:keys [passphrase password]}] + (status-im.native-module.core/multiaccount-import-mnemonic + passphrase + password + (fn [result] + (re-frame/dispatch [:multiaccounts.recover/import-mnemonic-success result]))))) + +(re-frame/reg-fx + :multiaccounts.recover/derive-addresses + (fn [{:keys [account-id paths]}] + (status-im.native-module.core/multiaccount-derive-addresses + account-id + paths + (fn [result] + (re-frame/dispatch [:multiaccounts.recover/derive-addresses-success result]))))) + +(fx/defn on-import-mnemonic-success + {:events [:multiaccounts.recover/import-mnemonic-success]} + [{:keys [db] :as cofx} result] + (let [{:keys [id] :as data} (types/json->clj result)] + {:db (assoc-in db [:multiaccounts/recover :root-key] data) + :multiaccounts.recover/derive-addresses {:account-id id + :paths [constants/path-default-wallet + constants/path-whisper]}})) + +(fx/defn on-derive-addresses-success + {:events [:multiaccounts.recover/derive-addresses-success]} + [{:keys [db] :as cofx} result] + (let [data (types/json->clj result)] + (fx/merge cofx + {:db (assoc-in db [:multiaccounts/recover :derived] data)} + (navigation/navigate-to-cofx :recover-multiaccount-success nil)))) + +(fx/defn re-encrypt-pressed + {:events [:recover.success.ui/re-encrypt-pressed]} + [{:keys [db] :as cofx}] + (fx/merge cofx + {:db (assoc-in db [:intro-wizard :selected-storage-type] :default)} + (navigation/navigate-to-cofx :recover-multiaccount-select-storage nil))) + +(fx/defn enter-phrase-pressed + {:events [:recover.ui/enter-phrase-pressed]} + [{:keys [db] :as cofx}] + (fx/merge cofx + {:db (assoc db :multiaccounts/recover {:next-button-disabled? true}) + :dispatch [:bottom-sheet/hide-sheet]} + (navigation/navigate-to-cofx :recover-multiaccount-enter-phrase nil))) + +(fx/defn prepare-to-recover + [{:keys [db random-guid-generator] :as cofx}] + (fx/merge cofx + {:db (-> db + (assoc :node/on-ready :import-mnemonic) + (assoc :multiaccounts/new-installation-id (random-guid-generator)))} + (node/initialize nil))) + +(fx/defn import-mnemonic + [{:keys [db]}] + (let [{:keys [password passphrase]} (:multiaccounts/recover db)] + {:multiaccounts.recover/import-mnemonic + {:passphrase passphrase + :password password}})) + +(fx/defn proceed-to-import-mnemonic + [{:keys [db] :as cofx}] + (let [{:keys [passphrase]} (:multiaccounts/recover db) + node-started? (= :started (:node/status db))] + (when (mnemonic/valid-length? passphrase) + (if node-started? + (import-mnemonic cofx) + (prepare-to-recover cofx))))) + +(fx/defn enter-phrase-next-button-pressed + {:events [:recover.enter-passphrase.ui/input-submitted + :recover.enter-passphrase.ui/next-pressed] + :interceptors [(re-frame/inject-cofx :random-guid-generator)]} + [{:keys [db] :as cofx}] + (fx/merge cofx + (proceed-to-import-mnemonic))) + +(fx/defn cancel-pressed + {:events [:recover.ui/cancel-pressed]} + [cofx] + (navigation/navigate-back cofx)) + +(fx/defn select-storage-next-pressed + {:events [:recover.select-storage.ui/next-pressed] + :interceptors [(re-frame/inject-cofx :random-guid-generator)]} + [{:keys [db] :as cofx}] + (let [storage-type (get-in db [:intro-wizard :selected-storage-type])] + (if (= storage-type :advanced) + {:dispatch [:recovery.ui/keycard-option-pressed]}) + (navigation/navigate-to-cofx cofx :recover-multiaccount-enter-password nil))) + +(fx/defn proceed-to-password-confirm + [{:keys [db] :as cofx}] + (when (nil? (get-in db [:multiaccounts/recover :password-error])) + (navigation/navigate-to-cofx cofx :recover-multiaccount-confirm-password nil))) + +(fx/defn enter-password-next-button-pressed + {:events [:recover.enter-password.ui/input-submitted + :recover.enter-password.ui/next-pressed]} + [{:keys [db] :as cofx}] + (fx/merge cofx + (validate-password) + (proceed-to-password-confirm))) + +(fx/defn confirm-password-next-button-pressed + {:events [:recover.confirm-password.ui/input-submitted + :recover.confirm-password.ui/next-pressed] + :interceptors [(re-frame/inject-cofx :random-guid-generator)]} + [{:keys [db] :as cofx}] + (let [{:keys [password password-confirmation]} (:multiaccounts/recover db)] + (if (= password password-confirmation) + (fx/merge cofx + {:db (assoc db :intro-wizard nil)} + (multiaccount-store-derived) + (navigation/navigate-to-cofx :keycard-welcome nil)) + {:db (assoc-in db [:multiaccounts/recover :password-error] :password_error1)}))) + +(fx/defn count-words + [{:keys [db]}] + (let [passphrase (get-in db [:multiaccounts/recover :passphrase])] + {:db (assoc-in db [:multiaccounts/recover :words-count] + (mnemonic/words-count passphrase))})) + +(fx/defn run-validation + [{:keys [db] :as cofx}] + (let [passphrase (get-in db [:multiaccounts/recover :passphrase])] + (when (= (last passphrase) " ") + (fx/merge cofx + (validate-phrase-for-warnings))))) + +(fx/defn enter-phrase-input-changed + {:events [:recover.enter-passphrase.ui/input-changed]} + [cofx input] + (fx/merge cofx + (set-phrase input) + (count-words) + (run-validation))) + +(fx/defn enter-password-input-changed + {:events [:recover.enter-password.ui/input-changed]} + [cofx input] + (set-password cofx input)) + +(fx/defn confirm-password-input-changed + {:events [:recover.confirm-password.ui/input-changed]} + [{:keys [db]} input] + {:db (-> db + (assoc-in [:multiaccounts/recover :password-confirmation] input) + (assoc-in [:multiaccounts/recover :password-error] nil))}) diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs index 64d6f40418..feb42fb9ec 100644 --- a/src/status_im/native_module/core.cljs +++ b/src/status_im/native_module/core.cljs @@ -40,6 +40,9 @@ (defn multiaccount-generate-and-derive-addresses [n mnemonic-length paths callback] (native-module/multiaccount-generate-and-derive-addresses n mnemonic-length paths callback)) +(defn multiaccount-import-mnemonic [mnemonic password callback] + (native-module/multiaccount-import-mnemonic mnemonic password callback)) + (defn login [address password main-account watch-addresses callback] (native-module/login address password main-account watch-addresses callback)) diff --git a/src/status_im/native_module/impl/module.cljs b/src/status_im/native_module/impl/module.cljs index cc71a4f56e..617771e305 100644 --- a/src/status_im/native_module/impl/module.cljs +++ b/src/status_im/native_module/impl/module.cljs @@ -106,6 +106,14 @@ on-result))) +(defn multiaccount-import-mnemonic [mnemonic password on-result] + (when (and @node-started (status)) + (.multiAccountImportMnemonic (status) + (types/clj->json {:mnemonicPhrase mnemonic + :Bip39Passphrase password}) + + on-result))) + (defn login [address password main-account watch-addresses on-result] (when (and @node-started (status)) (.login (status) diff --git a/src/status_im/react_native/resources.cljs b/src/status_im/react_native/resources.cljs index 70c4f5e069..71a5305f6e 100644 --- a/src/status_im/react_native/resources.cljs +++ b/src/status_im/react_native/resources.cljs @@ -18,6 +18,8 @@ :keycard-lock (js-require/js-require "./resources/images/ui/keycard-lock.png") :keycard (js-require/js-require "./resources/images/ui/keycard.png") :keycard-logo (js-require/js-require "./resources/images/ui/keycard-logo.png") + :keycard-logo-blue (js-require/js-require "./resources/images/ui/keycard-logo-blue.png") + :keycard-logo-gray (js-require/js-require "./resources/images/ui/keycard-logo-gray.png") :keycard-key (js-require/js-require "./resources/images/ui/keycard-key.png") :keycard-empty (js-require/js-require "./resources/images/ui/keycard-empty.png") :keycard-phone (js-require/js-require "./resources/images/ui/keycard-phone.png") diff --git a/src/status_im/signals/core.cljs b/src/status_im/signals/core.cljs index 1dece8d354..7253157fe1 100644 --- a/src/status_im/signals/core.cljs +++ b/src/status_im/signals/core.cljs @@ -13,7 +13,8 @@ [status-im.utils.fx :as fx] [status-im.utils.security :as security] [status-im.utils.types :as types] - [taoensso.timbre :as log])) + [taoensso.timbre :as log] + [status-im.multiaccounts.recover.core :as multiaccounts.recover])) (fx/defn status-node-started [{db :db :as cofx}] @@ -38,11 +39,8 @@ :create-multiaccount (fn [_] {:multiaccounts.create/create-multiaccount (select-keys create [:id :password])}) - :recover-multiaccount - (fn [{:keys [db]}] - (let [{:keys [password passphrase]} (:multiaccounts/recover db)] - {:multiaccounts.recover/recover-multiaccount - [(security/mask-data passphrase) password]})) + :import-mnemonic + (multiaccounts.recover/import-mnemonic) :create-keycard-multiaccount (hardwallet/create-keycard-multiaccount) :start-onboarding diff --git a/src/status_im/ui/components/action_button/action_button.cljs b/src/status_im/ui/components/action_button/action_button.cljs index 53ec72f312..4aa5c7a747 100644 --- a/src/status_im/ui/components/action_button/action_button.cljs +++ b/src/status_im/ui/components/action_button/action_button.cljs @@ -3,16 +3,20 @@ [status-im.ui.components.common.common :refer [list-separator]] [status-im.ui.components.icons.vector-icons :as vi] [status-im.ui.components.react :as rn] - [status-im.ui.components.colors :as colors])) + [status-im.ui.components.colors :as colors] + [status-im.ui.components.react :as react] + [status-im.react-native.resources :as resources])) -(defn action-button [{:keys [label accessibility-label icon icon-opts on-press label-style cyrcle-color]}] +(defn action-button [{:keys [label accessibility-label icon icon-opts image image-opts on-press label-style cyrcle-color]}] [rn/touchable-highlight (merge {:on-press on-press :underlay-color (colors/alpha colors/gray 0.15)} (when accessibility-label {:accessibility-label accessibility-label})) [rn/view {:style st/action-button} [rn/view {:style (st/action-button-icon-container cyrcle-color)} - [vi/icon icon icon-opts]] + (if image + [react/image (assoc image-opts :source (resources/get-image image))] + [vi/icon icon icon-opts])] [rn/view st/action-button-label-container [rn/text {:style (merge st/action-button-label label-style)} label]]]]) diff --git a/src/status_im/ui/components/tooltip/styles.cljs b/src/status_im/ui/components/tooltip/styles.cljs index fadab53d50..27fbd0e3e8 100644 --- a/src/status_im/ui/components/tooltip/styles.cljs +++ b/src/status_im/ui/components/tooltip/styles.cljs @@ -31,6 +31,7 @@ {:padding-horizontal 16 :padding-vertical 9 :background-color color + :elevation 2 :border-radius 8}) (def bottom-tooltip-text-container diff --git a/src/status_im/ui/screens/intro/views.cljs b/src/status_im/ui/screens/intro/views.cljs index 9f91fe753a..0217b1da26 100644 --- a/src/status_im/ui/screens/intro/views.cljs +++ b/src/status_im/ui/screens/intro/views.cljs @@ -19,7 +19,8 @@ [status-im.ui.components.toolbar.view :as toolbar] [status-im.i18n :as i18n] [status-im.ui.components.status-bar.view :as status-bar] - [status-im.constants :as constants])) + [status-im.constants :as constants] + [status-im.utils.config :as config])) (defn dots-selector [{:keys [on-press n selected color]}] [react/view {:style (styles/dot-selector n)} @@ -128,20 +129,26 @@ (utils/get-shortened-address public-key)]] [radio/radio selected?]]]))]) -(defn storage-entry [{:keys [type icon icon-width icon-height title desc]} selected-storage-type] +(defn storage-entry [{:keys [type icon icon-width icon-height + image image-selected image-width image-height + title desc]} selected-storage-type] (let [selected? (= type selected-storage-type)] [react/view [react/view {:style {:padding-top 14 :padding-bottom 4}} [react/text {:style (assoc styles/wizard-text :text-align :left :margin-left 16)} (i18n/label type)]] [react/touchable-highlight - {:on-press #(re-frame/dispatch [:intro-wizard/on-key-storage-selected type])} + {:on-press #(re-frame/dispatch [:intro-wizard/on-key-storage-selected (if config/hardwallet-enabled? type :default)])} [react/view (assoc (styles/list-item selected?) :align-items :flex-start :padding-top 20 :padding-bottom 12) - [vector-icons/icon icon {:color (if selected? colors/blue colors/gray) - :width icon-width :height icon-height}] + (if image + [react/image + {:source (resources/get-image (if selected? image-selected image)) + :style {:width image-width :height image-height}}] + [vector-icons/icon icon {:color (if selected? colors/blue colors/gray) + :width icon-width :height icon-height}]) [react/view {:style {:margin-horizontal 16 :flex 1}} [react/text {:style (assoc styles/wizard-text :font-weight "500" :color colors/black :text-align :left)} (i18n/label title)] @@ -157,12 +164,13 @@ :icon-height 24 :title :this-device :desc :this-device-desc} - {:type :advanced - :icon :main-icons/keycard-logo - :icon-width 13 - :icon-height 22 - :title :keycard - :desc :keycard-desc}]] + {:type :advanced + :image :keycard-logo-gray + :image-selected :keycard-logo-blue + :image-width 24 + :image-height 24 + :title :keycard + :desc :keycard-desc}]] [react/view {:style {:flex 1 :justify-content :flex-end ;; We have to align top storage entry diff --git a/src/status_im/ui/screens/keycard/onboarding/views.cljs b/src/status_im/ui/screens/keycard/onboarding/views.cljs index 7323999fa0..06c2ae9840 100644 --- a/src/status_im/ui/screens/keycard/onboarding/views.cljs +++ b/src/status_im/ui/screens/keycard/onboarding/views.cljs @@ -397,58 +397,3 @@ :label (i18n/label :t/next) :disabled? (empty? input-word) :forward? true}]]]]]))) - -;NOTE temporary screen, to be removed after Recovery will be implemented -(defview enter-mnemonic [] - (letsubs [mnemonic [:hardwallet-mnemonic]] - [react/view styles/container - [toolbar/toolbar - {:transparent? true - :style {:margin-top 32}} - [toolbar/nav-text - {:handler #(re-frame/dispatch [:keycard.onboarding.ui/cancel-pressed]) - :style {:padding-left 21}} - (i18n/label :t/cancel)] - [react/text {:style {:color colors/gray}} - (i18n/label :t/step-i-of-n {:step "1" - :number "2"})]] - [react/view {:flex 1 - :flex-direction :column - :justify-content :space-between - :align-items :center} - [react/view {:flex-direction :column - :align-items :center} - [react/view {:margin-top 16} - [react/text {:style {:typography :header - :text-align :center}} - "Enter your recovery phrase"]] - [react/view {:margin-top 16 - :width "85%" - :align-items :center} - [react/text {:style {:color colors/gray - :text-align :center}} - "Enter your recovery phrase, separate the words by single spaces"]]] - [react/view - [text-input/text-input-with-label - {:on-change-text #(re-frame/dispatch [:keycard.recovery.enter-mnemonic.ui/input-changed %]) - :auto-focus true - :on-submit-editing #(re-frame/dispatch [:keycard.recovery.enter-mnemonic.ui/input-submitted]) - :placeholder nil - :height 125 - :multiline true - :auto-correct false - :container {:background-color :white} - :style {:background-color :white - :typography :header}}]] - [react/view {:flex-direction :row - :justify-content :space-between - :align-items :center - :width "100%" - :height 86} - [react/view] - [react/view {:margin-right 20} - [components.common/bottom-button - {:on-press #(re-frame/dispatch [:keycard.recovery.enter-mnemonic.ui/next-pressed]) - :label (i18n/label :t/next) - :disabled? (empty? mnemonic) - :forward? true}]]]]])) diff --git a/src/status_im/ui/screens/multiaccounts/login/views.cljs b/src/status_im/ui/screens/multiaccounts/login/views.cljs index 5ac3e97367..094f21bc51 100644 --- a/src/status_im/ui/screens/multiaccounts/login/views.cljs +++ b/src/status_im/ui/screens/multiaccounts/login/views.cljs @@ -105,10 +105,12 @@ [react/i18n-text {:style styles/processing :key :processing}]]) [react/view {:style styles/bottom-button-container} [components.common/button - {:label (i18n/label :t/access-key) + {:label (i18n/label :t/access-key) :button-style styles/bottom-button - :background? false - :on-press #(re-frame/dispatch [:multiaccounts.recover.ui/recover-multiaccount-button-pressed])}] + :background? false + :on-press #(do + (react/dismiss-keyboard!) + (re-frame/dispatch [:multiaccounts.recover.ui/recover-multiaccount-button-pressed]))}] [components.common/button {:label (i18n/label :t/submit) :button-style styles/bottom-button diff --git a/src/status_im/ui/screens/multiaccounts/recover/views.cljs b/src/status_im/ui/screens/multiaccounts/recover/views.cljs index 2e4948aca5..a1adf30af3 100644 --- a/src/status_im/ui/screens/multiaccounts/recover/views.cljs +++ b/src/status_im/ui/screens/multiaccounts/recover/views.cljs @@ -16,7 +16,17 @@ [status-im.ui.components.common.common :as components.common] [status-im.utils.security :as security] [status-im.utils.platform :as platform] - [clojure.string :as string])) + [clojure.string :as string] + [status-im.ui.components.action-button.styles :as action-button.styles] + [status-im.ui.components.action-button.action-button :as action-button] + [status-im.ui.components.colors :as colors] + [status-im.utils.gfycat.core :as gfy] + [status-im.utils.identicon :as identicon] + [status-im.ui.components.radio :as radio] + [status-im.ui.components.icons.vector-icons :as vector-icons] + [status-im.ui.screens.intro.views :as intro.views] + [status-im.utils.utils :as utils] + [status-im.constants :as constants])) (defview passphrase-input [passphrase error warning] (letsubs [input-ref (reagent/atom nil)] @@ -94,3 +104,343 @@ :label (i18n/label :t/sign-in) :disabled? disabled? :on-press sign-in}]])]))) + +(defn bottom-sheet-view [] + [react/view {:flex 1 :flex-direction :row} + [react/view action-button.styles/actions-list + [action-button/action-button + {:label (i18n/label :t/enter-seed-phrase) + :accessibility-label :enter-seed-phrase-button + :icon :main-icons/text + :icon-opts {:color colors/blue} + :on-press #(re-frame/dispatch [:recover.ui/enter-phrase-pressed])}] + [action-button/action-button + {:label (i18n/label :t/recover-with-keycard) + :label-style (if config/hardwallet-enabled? {} {:color colors/gray}) + :accessibility-label :recover-with-keycard-button + :image :keycard-logo-blue + :image-opts {:style {:width 24 :height 24}} + :on-press #(when config/hardwallet-enabled? + (re-frame/dispatch [:recover.ui/recover-with-keycard-pressed]))}]]]) + +(def bottom-sheet + {:content bottom-sheet-view + :content-height 130}) + +(defview enter-phrase [] + (letsubs [{:keys [passphrase + processing? + passphrase-error + words-count + next-button-disabled?]} [:get-recover-multiaccount]] + [react/keyboard-avoiding-view {:flex 1 + :justify-content :space-between + :background-color colors/white} + [toolbar/toolbar + {:transparent? true + :style {:margin-top 32}} + [toolbar/nav-text + {:handler #(re-frame/dispatch [:recover.ui/cancel-pressed]) + :style {:padding-left 21}} + (i18n/label :t/cancel)] + [react/text {:style {:color colors/gray}} + (i18n/label :t/step-i-of-n {:step "1" + :number "2"})]] + [react/view {:flex 1 + :flex-direction :column + :justify-content :space-between + :align-items :center} + [react/view {:flex-direction :column + :align-items :center} + [react/view {:margin-top 16} + [react/text {:style {:typography :header + :text-align :center}} + (i18n/label :t/multiaccounts-recover-enter-phrase-title)]] + [react/view {:margin-top 16} + [text-input/text-input-with-label + {:on-change-text #(re-frame/dispatch [:recover.enter-passphrase.ui/input-changed (security/mask-data %)]) + :auto-focus true + :on-submit-editing #(re-frame/dispatch [:recover.enter-passphrase.ui/input-submitted]) + :error (when passphrase-error (i18n/label passphrase-error)) + :placeholder nil + :height 120 + :multiline true + :auto-correct false + :container {:background-color :white + :min-width "50%"} + :style {:background-color :white + :text-align :center + :font-size 16 + :font-weight "700"}}]] + [react/view {:align-items :center} + (when words-count + [react/view {:flex-direction :row + :height 11 + :align-items :center} + (when-not next-button-disabled? + [vector-icons/tiny-icon :tiny-icons/tiny-check]) + [react/text {:style {:font-size 14 + :padding-left 4 + :text-align :center + :color colors/black}} + (i18n/label-pluralize words-count :t/words-n)]])] + (when next-button-disabled? + [react/view {:margin-top 17 + :align-items :center} + [react/text {:style {:color colors/black + :font-size 14 + :text-align :center}} + (i18n/label :t/multiaccounts-recover-enter-phrase-text)]])] + (when processing? + [react/view + [react/activity-indicator {:size :large + :animating true}] + [react/text {:style {:color colors/gray + :margin-top 8}} + (i18n/label :t/processing)]]) + [react/view {:flex-direction :row + :justify-content :space-between + :align-items :center + :width "100%" + :height 86} + (when-not processing? + [react/view]) + (when-not processing? + [react/view {:margin-right 20} + [components.common/bottom-button + {:on-press #(re-frame/dispatch [:recover.enter-passphrase.ui/next-pressed]) + :label (i18n/label :t/next) + :disabled? next-button-disabled? + :forward? true}]])]]])) + +(defview success [] + (letsubs [multiaccount [:get-recover-multiaccount]] + (let [pubkey (get-in multiaccount [:derived constants/path-whisper-keyword :publicKey])] + [react/view {:flex 1 + :justify-content :space-between + :background-color colors/white} + [toolbar/toolbar + {:transparent? true + :style {:margin-top 32}} + nil + nil] + [react/view {:flex 1 + :flex-direction :column + :justify-content :space-between + :align-items :center} + [react/view {:flex-direction :column + :align-items :center} + [react/view {:margin-top 16} + [react/text {:style {:typography :header + :text-align :center}} + (i18n/label :t/keycard-recovery-success-header)]] + [react/view {:margin-top 16 + :width "85%" + :align-items :center} + [react/text {:style {:color colors/gray + :text-align :center}} + (i18n/label :t/recovery-success-text)]]] + [react/view {:flex-direction :column + :flex 1 + :justify-content :center + :align-items :center} + [react/view {:margin-horizontal 16 + :flex-direction :column} + [react/view {:justify-content :center + :align-items :center + :margin-bottom 11} + [react/image {:source {:uri (identicon/identicon pubkey)} + :style {:width 61 + :height 61 + :border-radius 30 + :border-width 1 + :border-color (colors/alpha colors/black 0.1)}}]] + [react/text {:style {:text-align :center + :color colors/black + :font-weight "500"} + :number-of-lines 1 + :ellipsize-mode :middle} + (gfy/generate-gfy pubkey)] + [react/text {:style {:text-align :center + :margin-top 4 + :color colors/gray + :font-family "monospace"} + :number-of-lines 1 + :ellipsize-mode :middle} + (utils/get-shortened-address pubkey)]]] + [react/view {:margin-bottom 50} + [react/touchable-highlight + {:on-press #(re-frame/dispatch [:recover.success.ui/re-encrypt-pressed])} + [react/view {:background-color colors/gray-background + :align-items :center + :justify-content :center + :flex-direction :row + :width 193 + :height 44 + :border-radius 10} + [react/text {:style {:color colors/blue}} + (i18n/label :t/re-encrypt-key)]]]]]]))) + +(defview select-storage [] + (letsubs [{:keys [selected-storage-type]} [:intro-wizard] + {view-height :height} [:dimensions/window]] + [react/view {:flex 1 + :justify-content :space-between + :background-color colors/white} + [toolbar/toolbar + {:transparent? true + :style {:margin-top 32}} + [toolbar/nav-text + {:handler #(re-frame/dispatch [:recover.ui/cancel-pressed]) + :style {:padding-left 21}} + (i18n/label :t/cancel)] + nil] + [react/view {:flex 1 + :justify-content :space-between} + [react/view {:flex-direction :column + :align-items :center} + [react/view {:margin-top 16} + [react/text {:style {:typography :header + :text-align :center}} + (i18n/label :t/intro-wizard-title3)]] + [react/view {:margin-top 16 + :width "85%" + :align-items :center} + [react/text {:style {:color colors/gray + :text-align :center}} + (i18n/label :t/intro-wizard-text3)]]] + [intro.views/select-key-storage {:selected-storage-type (if config/hardwallet-enabled? selected-storage-type :default)} view-height] + [react/view {:flex-direction :row + :justify-content :space-between + :align-items :center + :width "100%" + :height 86} + [react/view components.styles/flex] + [react/view {:margin-right 20} + [components.common/bottom-button + {:on-press #(re-frame/dispatch [:recover.select-storage.ui/next-pressed]) + :forward? true}]]]]])) + +(defview enter-password [] + (letsubs [{:keys [password password-error]} [:get-recover-multiaccount]] + [react/keyboard-avoiding-view {:flex 1 + :justify-content :space-between + :background-color colors/white} + [toolbar/toolbar + {:transparent? true + :style {:margin-top 32}} + [toolbar/nav-text + {:handler #(re-frame/dispatch [:recover.ui/cancel-pressed]) + :style {:padding-left 21}} + (i18n/label :t/cancel)] + [react/text {:style {:color colors/gray}} + (i18n/label :t/step-i-of-n {:step "1" + :number "2"})]] + [react/view {:flex 1 + :flex-direction :column + :justify-content :space-between + :align-items :center} + [react/view {:flex-direction :column + :align-items :center} + [react/view {:margin-top 16} + [react/text {:style {:typography :header + :text-align :center}} + (i18n/label :t/intro-wizard-title-alt4)]] + [react/view {:margin-top 16 + :width "85%" + :align-items :center} + [react/text {:style {:color colors/gray + :text-align :center}} + (i18n/label :t/password-description)]] + [react/view {:margin-top 16} + [text-input/text-input-with-label + {:on-change-text #(re-frame/dispatch [:recover.enter-password.ui/input-changed (security/mask-data %)]) + :auto-focus true + :on-submit-editing #(re-frame/dispatch [:recover.enter-password.ui/input-submitted]) + :secure-text-entry true + :error (when password-error (i18n/label password-error)) + :placeholder nil + :height 125 + :multiline false + :auto-correct false + :container {:background-color :white + :min-width "50%"} + :style {:background-color :white + :width 200 + :text-align :center + :font-size 20 + :font-weight "700"}}]]] + [react/view {:flex-direction :row + :justify-content :space-between + :align-items :center + :width "100%" + :height 86} + [react/view] + [react/view {:margin-right 20} + [components.common/bottom-button + {:on-press #(re-frame/dispatch [:recover.enter-password.ui/next-pressed]) + :label (i18n/label :t/next) + :disabled? (empty? password) + :forward? true}]]]]])) + +(defview confirm-password [] + (letsubs [{:keys [password-confirmation password-error]} [:get-recover-multiaccount]] + [react/keyboard-avoiding-view {:flex 1 + :justify-content :space-between + :background-color colors/white} + [toolbar/toolbar + {:transparent? true + :style {:margin-top 32}} + [toolbar/nav-text + {:handler #(re-frame/dispatch [:recover.ui/cancel-pressed]) + :style {:padding-left 21}} + (i18n/label :t/cancel)] + [react/text {:style {:color colors/gray}} + (i18n/label :t/step-i-of-n {:step "1" + :number "2"})]] + [react/view {:flex 1 + :flex-direction :column + :justify-content :space-between + :align-items :center} + [react/view {:flex-direction :column + :align-items :center} + [react/view {:margin-top 16} + [react/text {:style {:typography :header + :text-align :center}} + (i18n/label :t/intro-wizard-title-alt5)]] + [react/view {:margin-top 16 + :width "85%" + :align-items :center} + [react/text {:style {:color colors/gray + :text-align :center}} + (i18n/label :t/password-description)]] + [react/view {:margin-top 16} + [text-input/text-input-with-label + {:on-change-text #(re-frame/dispatch [:recover.confirm-password.ui/input-changed %]) + :auto-focus true + :on-submit-editing #(re-frame/dispatch [:recover.confirm-password.ui/input-submitted]) + :error (when password-error (i18n/label password-error)) + :secure-text-entry true + :placeholder nil + :height 125 + :multiline false + :auto-correct false + :container {:background-color :white + :min-width "50%"} + :style {:background-color :white + :width 200 + :text-align :center + :font-size 20 + :font-weight "700"}}]]] + [react/view {:flex-direction :row + :justify-content :space-between + :align-items :center + :width "100%" + :height 86} + [react/view] + [react/view {:margin-right 20} + [components.common/bottom-button + {:on-press #(re-frame/dispatch [:recover.confirm-password.ui/next-pressed]) + :label (i18n/label :t/next) + :disabled? (empty? password-confirmation) + :forward? true}]]]]])) diff --git a/src/status_im/ui/screens/routing/intro_login_stack.cljs b/src/status_im/ui/screens/routing/intro_login_stack.cljs index 896861489c..37167712de 100644 --- a/src/status_im/ui/screens/routing/intro_login_stack.cljs +++ b/src/status_im/ui/screens/routing/intro_login_stack.cljs @@ -5,7 +5,12 @@ #{:login :progress :create-multiaccount - :recover + :recover-multiaccount + :recover-multiaccount-enter-phrase + :recover-multiaccount-select-storage + :recover-multiaccount-enter-password + :recover-multiaccount-confirm-password + :recover-multiaccount-success :multiaccounts :intro :intro-wizard @@ -32,7 +37,6 @@ :keycard-onboarding-recovery-phrase :keycard-onboarding-recovery-phrase-confirm-word1 :keycard-onboarding-recovery-phrase-confirm-word2 - :keycard-recovery-enter-mnemonic :keycard-recovery-intro :keycard-recovery-start :keycard-recovery-pair @@ -46,7 +50,12 @@ :screens (cond-> [:login :progress :create-multiaccount - :recover + :recover-multiaccount + :recover-multiaccount-enter-phrase + :recover-multiaccount-select-storage + :recover-multiaccount-enter-password + :recover-multiaccount-confirm-password + :recover-multiaccount-success :multiaccounts] config/hardwallet-enabled? @@ -85,7 +94,6 @@ :keycard-onboarding-recovery-phrase :keycard-onboarding-recovery-phrase-confirm-word1 :keycard-onboarding-recovery-phrase-confirm-word2 - :keycard-recovery-enter-mnemonic :keycard-recovery-intro :keycard-recovery-start :keycard-recovery-pair diff --git a/src/status_im/ui/screens/routing/screens.cljs b/src/status_im/ui/screens/routing/screens.cljs index 4aa2551e87..ac6563b3b9 100644 --- a/src/status_im/ui/screens/routing/screens.cljs +++ b/src/status_im/ui/screens/routing/screens.cljs @@ -3,7 +3,7 @@ [status-im.ui.screens.about-app.views :as about-app] [status-im.ui.screens.multiaccounts.create.views :as multiaccounts.create] [status-im.ui.screens.multiaccounts.login.views :as login] - [status-im.ui.screens.multiaccounts.recover.views :as recover] + [status-im.ui.screens.multiaccounts.recover.views :as multiaccounts.recover] [status-im.ui.screens.multiaccounts.views :as multiaccounts] [status-im.ui.screens.add-new.new-chat.views :as new-chat] [status-im.ui.screens.add-new.new-public-chat.view :as new-public-chat] @@ -71,7 +71,12 @@ {:login login/login :progress progress/progress :create-multiaccount multiaccounts.create/create-multiaccount - :recover recover/recover + :recover-multiaccount multiaccounts.recover/recover + :recover-multiaccount-enter-phrase multiaccounts.recover/enter-phrase + :recover-multiaccount-select-storage multiaccounts.recover/select-storage + :recover-multiaccount-enter-password multiaccounts.recover/enter-password + :recover-multiaccount-confirm-password multiaccounts.recover/confirm-password + :recover-multiaccount-success multiaccounts.recover/success :multiaccounts multiaccounts/multiaccounts :intro intro/intro :intro-wizard intro/wizard @@ -94,7 +99,6 @@ :keycard-onboarding-recovery-phrase keycard.onboarding/recovery-phrase :keycard-onboarding-recovery-phrase-confirm-word1 keycard.onboarding/recovery-phrase-confirm-word :keycard-onboarding-recovery-phrase-confirm-word2 keycard.onboarding/recovery-phrase-confirm-word - :keycard-recovery-enter-mnemonic keycard.onboarding/enter-mnemonic :keycard-pairing keycard/pairing :keycard-nfc-on keycard/nfc-on :keycard-connection-lost keycard/connection-lost diff --git a/src/status_im/ui/screens/views.cljs b/src/status_im/ui/screens/views.cljs index 76d48e3eb8..42bb8e8510 100644 --- a/src/status_im/ui/screens/views.cljs +++ b/src/status_im/ui/screens/views.cljs @@ -18,6 +18,7 @@ [status-im.ui.screens.routing.core :as routing] [status-im.ui.screens.signing.views :as signing] [status-im.ui.screens.popover.views :as popover] + [status-im.ui.screens.multiaccounts.recover.views :as recover.views] [status-im.utils.dimensions :as dimensions] status-im.ui.screens.wallet.collectibles.etheremon.views status-im.ui.screens.wallet.collectibles.cryptostrikers.views @@ -59,7 +60,10 @@ (merge home.sheet/private-chat-actions) (= view :group-chat-actions) - (merge home.sheet/group-chat-actions))] + (merge home.sheet/group-chat-actions) + + (= view :recover-sheet) + (merge recover.views/bottom-sheet))] [bottom-sheet/bottom-sheet opts]))) diff --git a/test/appium/views/recover_access_view.py b/test/appium/views/recover_access_view.py index db2ff1ab4f..166ba9bbc4 100644 --- a/test/appium/views/recover_access_view.py +++ b/test/appium/views/recover_access_view.py @@ -6,7 +6,21 @@ class PassphraseInput(BaseEditBox): def __init__(self, driver): super(PassphraseInput, self).__init__(driver) - self.locator = self.Locator.accessibility_id("enter-12-words") + self.locator = self.Locator.xpath_selector("//android.widget.EditText") + + +class EnterSeedPhraseButton(BaseButton): + + def __init__(self, driver): + super(EnterSeedPhraseButton, self).__init__(driver) + self.locator = self.Locator.accessibility_id("enter-seed-phrase-button") + + +class ReencryptYourKeyButton(BaseButton): + + def __init__(self, driver): + super(ReencryptYourKeyButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='Re-encrypt your key']") class ConfirmRecoverAccess(BaseButton): @@ -72,7 +86,9 @@ class RecoverAccessView(SignInView): self.driver = driver self.passphrase_input = PassphraseInput(self.driver) + self.enter_seed_phrase_button = EnterSeedPhraseButton(self.driver) self.confirm_recover_access = ConfirmRecoverAccess(self.driver) + self.reencrypt_your_key_button = ReencryptYourKeyButton(self.driver) self.warnings = Warnings(self.driver) self.confirm_phrase_button = ConfirmPhraseButton(self.driver) self.cancel_button = CancelPhraseButton(self.driver) diff --git a/test/appium/views/sign_in_view.py b/test/appium/views/sign_in_view.py index a29c24e297..b52765e3cd 100644 --- a/test/appium/views/sign_in_view.py +++ b/test/appium/views/sign_in_view.py @@ -34,15 +34,15 @@ class RecoverAccountPasswordInput(BaseEditBox): class CreatePasswordInput(BaseEditBox): def __init__(self, driver): super(CreatePasswordInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='Create a password']" - "/following-sibling::android.widget.EditText") + self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='Create a password']/.." + "//android.widget.EditText") class ConfirmYourPasswordInput(BaseEditBox): def __init__(self, driver): super(ConfirmYourPasswordInput, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='Confirm your password']" - "/following-sibling::android.widget.EditText") + self.locator = self.Locator.xpath_selector("//android.widget.TextView[@text='Confirm your password']/.." + "//android.widget.EditText") class SignInButton(BaseButton): @@ -198,11 +198,16 @@ class SignInView(BaseView): recover_access_view = self.add_existing_multiaccount_button.click() else: recover_access_view = self.access_key_button.click() + recover_access_view.enter_seed_phrase_button.click() recover_access_view.passphrase_input.click() recover_access_view.passphrase_input.set_value(passphrase) - recover_access_view.recover_account_password_input.click() - recover_access_view.recover_account_password_input.set_value(password) - recover_access_view.sign_in_button.click_until_presence_of_element(recover_access_view.home_button) + recover_access_view.next_button.click() + recover_access_view.reencrypt_your_key_button.click() + recover_access_view.next_button.click() + recover_access_view.create_password_input.set_value(password) + recover_access_view.next_button.click() + recover_access_view.confirm_your_password_input.set_value(password) + recover_access_view.next_button.click_until_presence_of_element(recover_access_view.home_button) return self.get_home_view() def sign_in(self, password=common_password): diff --git a/test/cljs/status_im/test/ethereum/mnemonic.cljs b/test/cljs/status_im/test/ethereum/mnemonic.cljs index 7a4601329f..d20dbbd972 100644 --- a/test/cljs/status_im/test/ethereum/mnemonic.cljs +++ b/test/cljs/status_im/test/ethereum/mnemonic.cljs @@ -1,16 +1,22 @@ (ns status-im.test.ethereum.mnemonic (:require [cljs.test :refer-macros [deftest is testing]] - [status-im.ethereum.mnemonic :as mnemonic])) + [status-im.ethereum.mnemonic :as mnemonic] + [clojure.string :as string])) + +(deftest valid-length? + (is (not (mnemonic/valid-length? "rate rate"))) + (is (not (mnemonic/valid-length? (string/join " " (repeat 13 "rate"))))) + (is (not (mnemonic/valid-length? (string/join " " (repeat 16 "rate"))))) + (is (mnemonic/valid-length? (string/join " " (repeat 12 "rate")))) + (is (mnemonic/valid-length? (string/join " " (repeat 15 "rate")))) + (is (mnemonic/valid-length? (string/join " " (repeat 18 "rate")))) + (is (mnemonic/valid-length? (string/join " " (repeat 21 "rate")))) + (is (mnemonic/valid-length? (string/join " " (repeat 24 "rate"))))) (deftest valid-words? - (is (not (mnemonic/valid-words? ["rate" "rate"]))) - (is (not (mnemonic/valid-words? ["rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate?"]))) - (is (mnemonic/valid-words? ["rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate" "rate"]))) - -(deftest valid-phrase - (is (not (mnemonic/valid-phrase? "rate rate"))) - (is (not (mnemonic/valid-phrase? "rate rate rate rate rate rate rate rate rate rate rate rate?"))) - (is (mnemonic/valid-phrase? "rate rate rate rate rate rate rate rate rate rate rate rate"))) + (is (not (mnemonic/valid-words? "rate! rate"))) + (is (not (mnemonic/valid-words? "rate rate rate rate rate rate rate rate rate rate rate rate?"))) + (is (mnemonic/valid-words? "rate rate rate rate rate rate rate rate rate rate rate rate"))) (deftest passphrase->words? (is (= ["one" "two" "three" "for" "five" "six" "seven" "height" "nine" "ten" "eleven" "twelve"] diff --git a/test/cljs/status_im/test/multiaccounts/recover/core.cljs b/test/cljs/status_im/test/multiaccounts/recover/core.cljs index 12e600dd1b..5d7bd4b4f0 100644 --- a/test/cljs/status_im/test/multiaccounts/recover/core.cljs +++ b/test/cljs/status_im/test/multiaccounts/recover/core.cljs @@ -21,15 +21,15 @@ (is (= :required-field (models/check-phrase-errors nil))) (is (= :required-field (models/check-phrase-errors " "))) (is (= :required-field (models/check-phrase-errors " \t\n "))) - (is (= :recovery-phrase-invalid (models/check-phrase-errors "phrase with four words"))) - (is (= :recovery-phrase-invalid (models/check-phrase-errors "phrase with five cool words"))) + (is (= :recovery-phrase-wrong-length (models/check-phrase-errors "phrase with four words"))) + (is (= :recovery-phrase-wrong-length (models/check-phrase-errors "phrase with five cool words"))) (is (nil? (models/check-phrase-errors "monkey monkey monkey monkey monkey monkey monkey monkey monkey monkey monkey monkey"))) (is (nil? (models/check-phrase-errors (string/join " " (repeat 15 "monkey"))))) (is (nil? (models/check-phrase-errors (string/join " " (repeat 18 "monkey"))))) (is (nil? (models/check-phrase-errors (string/join " " (repeat 24 "monkey"))))) - (is (= :recovery-phrase-invalid (models/check-phrase-errors (string/join " " (repeat 14 "monkey"))))) - (is (= :recovery-phrase-invalid (models/check-phrase-errors (string/join " " (repeat 11 "monkey"))))) - (is (= :recovery-phrase-invalid (models/check-phrase-errors (string/join " " (repeat 19 "monkey"))))) + (is (= :recovery-phrase-wrong-length (models/check-phrase-errors (string/join " " (repeat 14 "monkey"))))) + (is (= :recovery-phrase-wrong-length (models/check-phrase-errors (string/join " " (repeat 11 "monkey"))))) + (is (= :recovery-phrase-wrong-length (models/check-phrase-errors (string/join " " (repeat 19 "monkey"))))) (is (= :recovery-phrase-invalid (models/check-phrase-errors "monkey monkey monkey 12345 monkey adf+123 monkey monkey monkey monkey monkey monkey"))) ;;NOTE(goranjovic): the following check should be ok because we sanitize extra whitespace (is (nil? (models/check-phrase-errors " monkey monkey monkey\t monkey monkey monkey monkey monkey monkey monkey monkey monkey \t ")))) @@ -42,44 +42,49 @@ ;;;; handlers (deftest set-phrase - (is (= {:db {:multiaccounts/recover {:passphrase "game buzz method pretty olympic fat quit display velvet unveil marine crater" - :passphrase-valid? true}}} + (is (= {:db {:multiaccounts/recover {:passphrase "game buzz method pretty olympic fat quit display velvet unveil marine crater" + :passphrase-error nil + :next-button-disabled? false}}} (models/set-phrase {:db {}} (security/mask-data "game buzz method pretty olympic fat quit display velvet unveil marine crater")))) - (is (= {:db {:multiaccounts/recover {:passphrase "game buzz method pretty olympic fat quit display velvet unveil marine crater" - :passphrase-valid? true}}} + (is (= {:db {:multiaccounts/recover {:passphrase "game buzz method pretty olympic fat quit display velvet unveil marine crater" + :passphrase-error nil + :next-button-disabled? false}}} (models/set-phrase {:db {}} (security/mask-data "Game buzz method pretty Olympic fat quit DISPLAY velvet unveil marine crater")))) - (is (= {:db {:multiaccounts/recover {:passphrase "game buzz method pretty zeus fat quit display velvet unveil marine crater" - :passphrase-valid? true}}} + (is (= {:db {:multiaccounts/recover {:passphrase "game buzz method pretty zeus fat quit display velvet unveil marine crater" + :passphrase-error nil + :next-button-disabled? false}}} (models/set-phrase {:db {}} (security/mask-data "game buzz method pretty zeus fat quit display velvet unveil marine crater")))) - (is (= {:db {:multiaccounts/recover {:passphrase " game\t buzz method pretty olympic fat quit\t display velvet unveil marine crater " - :passphrase-valid? true}}} + (is (= {:db {:multiaccounts/recover {:passphrase " game\t buzz method pretty olympic fat quit\t display velvet unveil marine crater " + :passphrase-error nil + :next-button-disabled? false}}} (models/set-phrase {:db {}} (security/mask-data " game\t buzz method pretty olympic fat quit\t display velvet unveil marine crater ")))) - (is (= {:db {:multiaccounts/recover {:passphrase "game buzz method pretty 1234 fat quit display velvet unveil marine crater" - :passphrase-valid? false}}} + (is (= {:db {:multiaccounts/recover {:passphrase "game buzz method pretty 1234 fat quit display velvet unveil marine crater" + :passphrase-error nil + :next-button-disabled? false}}} (models/set-phrase {:db {}} (security/mask-data "game buzz method pretty 1234 fat quit display velvet unveil marine crater"))))) (deftest validate-phrase (is (= {:db {:multiaccounts/recover {:passphrase-error nil - :passphrase-warning nil :passphrase "game buzz method pretty olympic fat quit display velvet unveil marine crater"}}} (models/validate-phrase {:db {:multiaccounts/recover {:passphrase "game buzz method pretty olympic fat quit display velvet unveil marine crater"}}}))) - (is (= {:db {:multiaccounts/recover {:passphrase-error nil - :passphrase-warning :recovery-phrase-unknown-words - :passphrase "game buzz method pretty zeus fat quit display velvet unveil marine crater"}}} + (is (= {:db {:multiaccounts/recover {:passphrase-error :recovery-phrase-unknown-words + :passphrase "game buzz method pretty zeus fat quit display velvet unveil marine crater"}}} (models/validate-phrase {:db {:multiaccounts/recover {:passphrase "game buzz method pretty zeus fat quit display velvet unveil marine crater"}}}))) (is (= {:db {:multiaccounts/recover {:passphrase-error :recovery-phrase-invalid - :passphrase-warning :recovery-phrase-unknown-words :passphrase "game buzz method pretty 1234 fat quit display velvet unveil marine crater"}}} (models/validate-phrase {:db {:multiaccounts/recover {:passphrase "game buzz method pretty 1234 fat quit display velvet unveil marine crater"}}})))) (deftest set-password (is (= {:db {:multiaccounts/recover {:password " " + :password-error nil :password-valid? false}}} (models/set-password {:db {}} (security/mask-data " ")))) (is (= {:db {:multiaccounts/recover {:password "abc" + :password-error nil :password-valid? false}}} (models/set-password {:db {}} (security/mask-data "abc")))) (is (= {:db {:multiaccounts/recover {:password "thisisapaswoord" + :password-error nil :password-valid? true}}} (models/set-password {:db {}} (security/mask-data "thisisapaswoord"))))) @@ -99,18 +104,15 @@ :db {:multiaccounts/recover {:passphrase "game buzz method pretty zeus fat quit display velvet unveil marine crater" :password "thisisapaswoord"}}})] - (is (contains? new-cofx :node/start)) (is (= "random" (get-in new-cofx [:db :multiaccounts/new-installation-id]))) - (is (= :recover-multiaccount (get-in new-cofx [:db :node/on-ready]))))) + (is (not (nil? (get new-cofx :multiaccounts.recover/recover-multiaccount)))))) (deftest recover-multiaccount-with-checks (let [new-cofx (models/recover-multiaccount-with-checks {:random-guid-generator (constantly "random") :db {:multiaccounts/recover {:passphrase "game buzz method pretty olympic fat quit display velvet unveil marine crater" :password "thisisapaswoord"}}})] - (is (contains? new-cofx :node/start)) - (is (= "random" (get-in new-cofx [:db :multiaccounts/new-installation-id]))) - (is (= :recover-multiaccount (get-in new-cofx [:db :node/on-ready])))) + (is (= "random" (get-in new-cofx [:db :multiaccounts/new-installation-id])))) (let [new-cofx (models/recover-multiaccount-with-checks {:random-guid-generator (constantly "random") :db {:multiaccounts/recover {:passphrase "game buzz method pretty zeus fat quit display velvet unveil marine crater" diff --git a/translations/en.json b/translations/en.json index 39e86886d1..ef9f5f1152 100644 --- a/translations/en.json +++ b/translations/en.json @@ -302,7 +302,6 @@ "save-password": "Save password", "submit": "Submit", "recover-key": "Recover key", - "processing": "Processing...", "currency-display-name-kes": "Kenyan Shilling", "view-etheremon": "View in Etheremon", "wallet-transaction-fee-details": "Gas limit caps the units of gas spent on the transaction. Gas price sets the price per unit of gas. Increasing gas price can make your transaction faster.", @@ -736,6 +735,7 @@ "browser-not-secure": "Connection is not secure! Do not sign transactions or send personal data on this site.", "welcome-to-status-description": "Here you can chat with people in a secure private chat, browse and interact with DApps.", "recovery-phrase-invalid": "Recovery phrase is invalid", + "recovery-phrase-wrong-length": "Please make sure the phrase you enter has 12, 15, 18, 21, or 24 words.", "currency-display-name-cny": "China Yuan Renminbi", "clear-history-confirmation-content": "Are you sure you want to clear this chat history?", "mailserver-reconnect": "Could not connect to mailserver. Tap to reconnect", @@ -1269,5 +1269,16 @@ "account-color": "Account color", "to-encrypt-enter-password": "To encrypt the account please enter your password", "accounts": "Accounts", - "add-account-incorrect-password": "Password seems to be incorrect. Enter the password you use to unlock the app." + "add-account-incorrect-password": "Password seems to be incorrect. Enter the password you use to unlock the app.", + "remember-me": "Remember me", + "enter-seed-phrase": "Enter Seed phrase", + "recover-with-keycard": "Recover with Keycard", + "multiaccounts-recover-enter-phrase-title": "Enter your seed phrase", + "multiaccounts-recover-enter-phrase-text": "Enter 12, 15, 18, 21 or 24 words.\nSeperate words by a single space.", + "words-n": { + "one": "1 word", + "other": "{{count}} words" + }, + "re-encrypt-key": "Re-encrypt your key", + "recovery-success-text": "You will have to create a new code or password to re-encrypt your key" }