diff --git a/.env b/.env index bb9bd3ee932d..9aef94b941ab 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 674176602b00..b4aded76cdfd 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 b17eabb7789d..61d44279191f 100644 --- a/externs.js +++ b/externs.js @@ -563,5 +563,7 @@ var TopLevel = { "createAppContainer" : function () {}, "useScreens" : function () {}, "multiAccountGenerateAndDeriveAddresses" : function () {}, - "multiAccountStoreDerived" : function () {} + "multiAccountStoreDerived" : function () {}, + "multiAccountImportMnemonic" : function () {}, + "multiAccountDeriveAddresses" : 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 7777a8d83540..35719102f707 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 @@ -698,6 +698,44 @@ public void run() { 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); + } + + @ReactMethod + public void multiAccountDeriveAddresses(final String json, final Callback callback) { + Log.d(TAG, "multiAccountDeriveAddresses"); + if (!checkAvailability()) { + callback.invoke(false); + return; + } + Runnable r = new Runnable() { + @Override + public void run() { + String res = Statusgo.multiAccountDeriveAddresses(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 7535501e0673..d11d9de07b40 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -355,6 +355,26 @@ - (void)handleSignal:(NSString *)signal callback(@[result]); } +//////////////////////////////////////////////////////////////////// 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 + NSLog(@"MultiAccountDeriveAddresses() method called"); +#endif + NSString *result = StatusgoMultiAccountDeriveAddresses(json); + callback(@[result]); +} + //////////////////////////////////////////////////////////////////// login RCT_EXPORT_METHOD(login:(NSString *)json diff --git a/resources/images/ui/keycard-logo-blue.png b/resources/images/ui/keycard-logo-blue.png new file mode 100644 index 000000000000..ad505daee459 Binary files /dev/null and b/resources/images/ui/keycard-logo-blue.png differ diff --git a/resources/images/ui/keycard-logo-gray.png b/resources/images/ui/keycard-logo-gray.png new file mode 100644 index 000000000000..f6491618dfac Binary files /dev/null and b/resources/images/ui/keycard-logo-gray.png differ diff --git a/src/status_im/ethereum/mnemonic.cljs b/src/status_im/ethereum/mnemonic.cljs index 94d62038e866..443579bf678b 100644 --- a/src/status_im/ethereum/mnemonic.cljs +++ b/src/status_im/ethereum/mnemonic.cljs @@ -21,17 +21,25 @@ (defn valid-word-counts? [v] (boolean (valid-word-counts (count v)))) +(defn words-count [s] + (if (empty? s) + nil + (-> 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 bb5166f90c55..d515e60c5bfe 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 074117352ec4..5584383222a4 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 145bfdc62420..14400e3c6fc1 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 e618759cb941..b1becc0478ef 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 b6a0ecd06e41..1eb7dc891fdd 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 0e681e2f5bd2..3ab87cd3ba2b 100644 --- a/src/status_im/native_module/core.cljs +++ b/src/status_im/native_module/core.cljs @@ -28,6 +28,12 @@ (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 multiaccount-derive-addresses [account-id paths callback] + (native-module/multiaccount-derive-addresses account-id paths 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 ecb35566b345..e76e748350ba 100644 --- a/src/status_im/native_module/impl/module.cljs +++ b/src/status_im/native_module/impl/module.cljs @@ -80,6 +80,22 @@ 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 multiaccount-derive-addresses [account-id paths on-result] + (when (and @node-started (status)) + (.multiAccountDeriveAddresses (status) + (types/clj->json {:accountID account-id + :paths paths}) + + 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 70c4f5e06948..71a5305f6e25 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 1dece8d3547e..7253157fe16c 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 53ec72f312fe..4aa5c7a74731 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 fadab53d50c8..27fbd0e3e89a 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 9f91fe753a86..0217b1da260e 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 7323999fa0ab..06c2ae9840c3 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 5ac3e9736704..094f21bc515f 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 2e4948aca546..a1adf30af38e 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 896861489c34..37167712de38 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 8031f35a349c..3351c2a22703 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] @@ -70,7 +70,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 @@ -93,7 +98,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 76d48e3eb850..42bb8e851092 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 db2ff1ab4fe5..166ba9bbc459 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 @@ def __init__(self, driver): 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 a29c24e297f4..b52765e3cda9 100644 --- a/test/appium/views/sign_in_view.py +++ b/test/appium/views/sign_in_view.py @@ -34,15 +34,15 @@ def __init__(self, driver): 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 @@ def recover_access(self, passphrase: str, password: str = common_password): 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 7a4601329f8d..d20dbbd97244 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-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-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-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"))) +(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 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 12e600dd1bee..5d7bd4b4f033 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 31582d2361c9..ca1602f509b3 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", @@ -1259,5 +1259,16 @@ "not-keycard-title": "Not a Keycard", "not-keycard-text": "The card you used is not a Keycard. You need to purchase a Keycard to use it", "create-new-key": "Create a new key", - "add-another-key": "Add another key" + "add-another-key": "Add another key", + "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" }