Key management screen in place

Key phrase screen in place

Added flow in a Rich comment, added storage screen, added Keycard upsell banner

Validate seed against selected multiaccount

Vvalidate seed against multiaccount

Connected migration flow to Keycard onboarding flow, unable to finish because an event called generate-and-load-key is not being emitted with the flow I made

Fixed state that was needed to start the recovery process, also removed seed-phrase from app-db when onboarding starts

Moved strings to translations in key storage views

Upsell banner bg theme, accordion data, choose-storage next bug, Hide keys managment option for Keycard accounts and from multiaccounts list screen.

Added test for subscription function

Tests for keystore move checkbox and seed phrase input

Fix translations/en.json after merge conflict, add test for seed-phrase input, seed-against-key-uid validation and state setup for redirect to Keycard screens

Update validate fn to mock mnemonic import and cascading changes

Fix linting errors

Fix seed key-uid mismatch popup, reduce styles and remove redundant views

Add dot menu in place of access-existing-keys button on login page

Make multiaccount redirect test more terse

Remove dangling comma from translations/en.json

Fix var that was missed during rebase

fx/merge and extra newline fixes

Removed nil? check, tests pass

Unknown error popup

Redirect Keycard banner to https://keycard.tech

Remove unused sub and fix linting

Fix issue#4 - Unable to enter new seeds

Hide back and disable hardware back on Keycard onboarding intro if the user is coming from migration flow. Fixes issue#1 raised by Diana.

Set state so show wallet balances show up auto magically. Fixes issue#3.

Update tests to capture recovering state

Center align sign in button since the left button was deleted.

Changes suggested by Andrey

Add top margin to Keycard intro only when user comes key storage

Hide manage key storage on ios

Signed-off-by: Shivek Khurana <shivek@status.im>
This commit is contained in:
shivekkhurana 2020-11-27 16:29:41 +05:30 committed by Shivek Khurana
parent f28f479256
commit 5c3133adb6
No known key found for this signature in database
GPG Key ID: 9BEB56E6E62968C7
24 changed files with 897 additions and 69 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,5 +1,6 @@
(ns status-im.keycard.core
(:require [status-im.keycard.change-pin :as change-pin]
(:require [re-frame.db]
[status-im.keycard.change-pin :as change-pin]
[status-im.keycard.common :as common]
status-im.keycard.delete-key
status-im.keycard.export-key
@ -528,3 +529,16 @@
{:events [:keycard.callback/on-register-card-events]}
[{:keys [db]} listeners]
{:db (update-in db [:keycard :listeners] merge listeners)})
(defn onboarding-intro-back-handler
"The back button handler is used to manage device back press.
If the handler returns false, the back button functions as usual (ie. dispatchs GO_BACK event).
If it returns true, the back button becomes inactive.
We want to deactivate the back button when the user comes from key-storage and migration flow."
[]
(-> @re-frame.db/app-db
:keycard
:from-key-storage-and-migration?
boolean))

View File

@ -15,6 +15,12 @@
[taoensso.timbre :as log]
[clojure.string :as string]))
;; validate that the given mnemonic was generated from Status Dictionary
(re-frame/reg-fx
::validate-mnemonic
(fn [[passphrase callback]]
(native-module/validate-mnemonic passphrase callback)))
(defn contact-names
"Returns map of all existing names for contact"
[{:keys [name preferred-name alias public-key ens-verified nickname]}]
@ -163,7 +169,7 @@
(defn clean-path [path]
(if path
(string/replace-first path #"file://" "")
(log/warn "[nativ-module] Empty path was provided")))
(log/warn "[native-module] Empty path was provided")))
(fx/defn save-profile-picture
{:events [::save-profile-picture]}
@ -201,3 +207,7 @@
{:events [::update-local-picture]}
[cofx pics]
(multiaccounts.update/optimistic cofx :images pics))
(comment
;; Test seed for Dim Venerated Yaffle, it's not here by mistake, this is just a test account
(native-module/validate-mnemonic "rocket mixed rebel affair umbrella legal resemble scene virus park deposit cargo" prn))

View File

@ -0,0 +1,182 @@
(ns status-im.multiaccounts.key-storage.core
(:require [clojure.string :as string]
[re-frame.core :as re-frame]
[status-im.ethereum.mnemonic :as mnemonic]
[status-im.multiaccounts.core :as multiaccounts]
[status-im.multiaccounts.recover.core :as multiaccounts.recover]
[status-im.multiaccounts.model :as multiaccounts.model]
[status-im.native-module.core :as native-module]
[status-im.navigation :as navigation]
[status-im.popover.core :as popover]
[status-im.utils.fx :as fx]
[status-im.utils.security :as security]
[status-im.utils.types :as types]))
(fx/defn key-and-storage-management-pressed
"This event can be dispatched before login and from profile and needs to redirect accordingly"
{:events [::key-and-storage-management-pressed]}
[cofx]
(navigation/navigate-to-cofx
cofx
:key-storage-stack
{:screen (if (multiaccounts.model/logged-in? cofx)
:actions-logged-in
:actions-not-logged-in)}))
(fx/defn move-keystore-checked
{:events [::move-keystore-checked]}
[{:keys [db] :as cofx} checked?]
{:db (assoc-in db [:multiaccounts/key-storage :move-keystore-checked?] checked?)})
(fx/defn enter-seed-pressed
"User is logged out and probably wants to move multiaccount to Keycard. Navigate to enter seed phrase screen"
{:events [::enter-seed-pressed]}
[cofx]
(navigation/navigate-to-cofx cofx :key-storage-stack {:screen :seed-phrase}))
(fx/defn seed-phrase-input-changed
{:events [::seed-phrase-input-changed]}
[{:keys [db] :as cofx} masked-seed-phrase]
(let [seed-phrase (security/safe-unmask-data masked-seed-phrase)]
{:db (update db :multiaccounts/key-storage assoc
:seed-phrase (when seed-phrase
(string/lower-case seed-phrase))
:seed-shape-invalid? (or (empty? seed-phrase)
(not (mnemonic/valid-length? seed-phrase)))
:seed-word-count (mnemonic/words-count seed-phrase))}))
(fx/defn key-uid-seed-mismatch
{:events [::show-seed-key-uid-mismatch-error-popup]}
[cofx _]
(popover/show-popover cofx {:view :seed-key-uid-mismatch}))
(defn validate-seed-against-key-uid
"Check if the key-uid was generated with the given seed-phrase"
[{:keys [import-mnemonic-fn on-success on-error]} {:keys [seed-phrase key-uid]}]
(import-mnemonic-fn
seed-phrase nil
(fn [result]
(let [{:keys [keyUid]} (types/json->clj result)]
;; if the key-uid from app-db is same as the one returned by multiaccount import,
;; it means that this seed was used to generate this multiaccount
(if (= key-uid keyUid)
(on-success)
(on-error))))))
(re-frame/reg-fx
::validate-seed-against-key-uid
(partial validate-seed-against-key-uid
{:import-mnemonic-fn native-module/multiaccount-import-mnemonic
:on-success #(re-frame/dispatch [:navigate-to :storage])
:on-error #(re-frame/dispatch [::show-seed-key-uid-mismatch-error-popup])}))
(fx/defn seed-phrase-validated
{:events [::seed-phrase-validated]}
[{:keys [db] :as cofx} validation-error]
(let [error? (-> validation-error
types/json->clj
:error
string/blank?
not)]
(if error?
(popover/show-popover cofx {:view :custom-seed-phrase})
{::validate-seed-against-key-uid {:seed-phrase (-> db :multiaccounts/key-storage :seed-phrase)
;; Unique key-uid of the account for which we are going to move keys
:key-uid (-> db :multiaccounts/login :key-uid)}})))
(fx/defn choose-storage-pressed
{:events [::choose-storage-pressed]}
[{:keys [db] :as cofx}]
(let [{:keys [seed-phrase]} (:multiaccounts/key-storage db)]
{::multiaccounts/validate-mnemonic
[(mnemonic/sanitize-passphrase seed-phrase)
#(re-frame/dispatch [::seed-phrase-validated %])]}))
(fx/defn keycard-storage-pressed
{:events [::keycard-storage-pressed]}
[{:keys [db]} selected?]
{:db (assoc-in db [:multiaccounts/key-storage :keycard-storage-selected?] selected?)})
(fx/defn warning-popup
{:events [::show-transfer-warning-popup]}
[cofx]
(popover/show-popover cofx {:view :transfer-multiaccount-to-keycard-warning}))
(re-frame/reg-fx
::delete-multiaccount
(fn [{:keys [key-uid on-success on-error]}]
(native-module/delete-multiaccount
key-uid
(fn [result]
(let [{:keys [error]} (types/json->clj result)]
(if-not (string/blank? error)
(on-error error)
(on-success)))))))
(fx/defn delete-multiaccount-and-init-keycard-onboarding
{:events [::delete-multiaccount-and-init-keycard-onboarding]}
[{:keys [db] :as cofx}]
(let [{:keys [key-uid]} (-> db :multiaccounts/login)]
{::delete-multiaccount {:key-uid key-uid
:on-error #(re-frame/dispatch [::delete-multiaccount-error %])
:on-success #(re-frame/dispatch [::delete-multiaccount-success])}}))
#_"Multiaccount has been deleted from device. We now need to emulate the restore seed phrase process, and make the user land on Keycard setup screen.
To ensure that keycard setup works, we need to:
1. Import multiaccount, derive required keys and save them at the correct location in app-db
2. Take the user to :keycard-onboarding-intro screen in :intro-login-stack
The exact events dispatched for this flow if consumed from the UI are:
:m.r/enter-phrase-input-changed
:m.r/enter-phrase-next-pressed
:m.r/re-encrypt-pressed
:i/on-key-storage-selected ([:intro-wizard :selected-storage-type] is set to :advanced)
:m.r/select-storage-next-pressed
We don't need to take the exact steps, just set the required state and redirect to correct screen
"
(fx/defn handle-delete-multiaccount-success
{:events [::delete-multiaccount-success]}
[{:keys [db] :as cofx} _]
{::multiaccounts.recover/import-multiaccount {:passphrase (get-in db [:multiaccounts/key-storage :seed-phrase])
:password nil
:success-event ::import-multiaccount-success}})
(fx/defn handle-multiaccount-import
{:events [::import-multiaccount-success]}
[{:keys [db] :as cofx} root-data derived-data]
(fx/merge cofx
{:db (-> db
(update :intro-wizard
assoc
:root-key root-data
:derived derived-data
:recovering? true
:selected-storage-type :advanced)
(assoc-in [:keycard :flow] :recovery)
(assoc-in [:keycard :from-key-storage-and-migration?] true)
(dissoc :multiaccounts/key-storage))}
(popover/hide-popover)
(navigation/navigate-to-cofx :intro-stack {:screen :keycard-onboarding-intro})))
(fx/defn handle-delete-multiaccount-error
{:events [::delete-multiaccount-error]}
[cofx _]
(popover/show-popover cofx {:view :transfer-multiaccount-unknown-error}))
(fx/defn goto-multiaccounts-screen
{:events [::hide-popover-and-goto-multiaccounts-screen]}
[cofx _]
(fx/merge cofx
(popover/hide-popover)
(navigation/navigate-to-cofx :intro-stack {:screen :multiaccounts})))
(comment
;; check import mnemonic output
(native-module/multiaccount-import-mnemonic "rocket mixed rebel affair umbrella legal resemble scene virus park deposit cargo" nil
(fn [result]
(prn (types/json->clj result))))
;; check delete account output
(native-module/delete-multiaccount "0x3831d0f22996a65970a214f0a94bfa9a63a21dac235d8dadb91be8e32e7d3ab7"
(fn [result]
(prn ::--delete-account-res-> result))))

View File

@ -0,0 +1,66 @@
(ns status-im.multiaccounts.key-storage.core-test
(:require [cljs.test :refer-macros [deftest is testing]]
[clojure.string :as string]
[status-im.multiaccounts.key-storage.core :as models]
[status-im.utils.security :as security]))
(deftest move-keystore-checked
(testing "Checks checkbox on-press"
(let [res (models/move-keystore-checked {:db {}} true)]
(is (= true (get-in res [:db :multiaccounts/key-storage :move-keystore-checked?]))))))
(deftest seed-phrase-input-changed
(testing "nil seed phrase shape is invalid"
(let [res (models/seed-phrase-input-changed {:db {}} (security/mask-data nil))]
(is (get-in res [:db :multiaccounts/key-storage :seed-shape-invalid?]))))
(let [sample-phrase "h h h h h h h h h h h H" ;; 12 characters
res (models/seed-phrase-input-changed {:db {}} (security/mask-data sample-phrase))]
(testing "Seed shape for 12 letter seed phrase is valid"
(is (false? (get-in res [:db :multiaccounts/key-storage :seed-shape-invalid?]))))
(testing "Seed words counted correctly"
(is (= 12 (get-in res [:db :multiaccounts/key-storage :seed-word-count]))))
(testing "Seed phrase is lowercased"
(is (= (get-in res [:db :multiaccounts/key-storage :seed-phrase])
(string/lower-case sample-phrase))))))
(def seed-key-uid-pair
{:seed-phrase "rocket mixed rebel affair umbrella legal resemble scene virus park deposit cargo"
:key-uid "0x3831d0f22996a65970a214f0a94bfa9a63a21dac235d8dadb91be8e32e7d3ab7"})
(defn mock-import-mnemonic-fn [_ _ _]
;; return json with keyUid, the real world will have more info in the response
(str "{\"keyUid\": \"" (:key-uid seed-key-uid-pair) "\"}"))
(deftest validate-seed-against-key-uid
(testing "Success event is triggered if correct seed is entered for selected multiaccount (key-uid)"
(models/validate-seed-against-key-uid
{:import-mnemonic-fn mock-import-mnemonic-fn
:on-success #(is true) ; this callback should be called
:on-error #(is false)}
{:seed-phrase (:seed-phrase seed-key-uid-pair)
:key-uid (:key-uid seed-key-uid-pair)}))
(testing "Error event is triggered if incorrect seed is entered for selected multiaccount"
(models/validate-seed-against-key-uid
{:import-mnemonic-fn mock-import-mnemonic-fn
:on-success #(is false)
:on-error #(is true)}
{:seed-phrase (:seed-phrase seed-key-uid-pair)
:key-uid "0xInvalid-Will-make-the-function-fail"})))
(deftest handle-multiaccount-import
(testing "Sets correct state for Keycard onboarding after multiaccounts seeds are derived"
(let [res (models/handle-multiaccount-import {:db {}} :passed-root-data :passed-derived-data)]
(is (= :passed-root-data (get-in res [:db :intro-wizard :root-key])))
(is (= :passed-derived-data (get-in res [:db :intro-wizard :derived])))
(is (= :advanced (get-in res [:db :intro-wizard :selected-storage-type]))) ; :advanced storage type means Keycard
(is (= :recovery (get-in res [:db :keycard :flow])))
(is (get-in res [:db :keycard :from-key-storage-and-migration?]))
(is (= {:intro-stack {:screen :keycard-onboarding-intro}} (get-in res [:db :navigation/screen-params]))))))
(comment
(security/safe-unmask-data (security/mask-data nil)))

View File

@ -6,6 +6,7 @@
[status-im.ethereum.mnemonic :as mnemonic]
[status-im.keycard.nfc :as nfc]
[status-im.i18n :as i18n]
[status-im.multiaccounts.core :as multiaccounts]
[status-im.multiaccounts.create.core :as multiaccounts.create]
[status-im.native-module.core :as status]
[status-im.popover.core :as popover]
@ -24,11 +25,6 @@
{:pre [(not (nil? key-uid))]}
(contains? multiaccounts key-uid))
(re-frame/reg-fx
::validate-mnemonic
(fn [[passphrase callback]]
(status/validate-mnemonic passphrase callback)))
(defn check-phrase-warnings [recovery-phrase]
(cond (string/blank? recovery-phrase) :t/required-field))
@ -87,7 +83,7 @@
(re-frame/reg-fx
::import-multiaccount
(fn [{:keys [passphrase password]}]
(fn [{:keys [passphrase password success-event]}]
(log/debug "[recover] ::import-multiaccount")
(status/multiaccount-import-mnemonic
passphrase
@ -113,8 +109,7 @@
(update derived-data
constants/path-whisper-keyword
merge {:name name :identicon identicon})]
(re-frame/dispatch [::import-multiaccount-success
root-data derived-data-extended]))))))))))))
(re-frame/dispatch [success-event root-data derived-data-extended]))))))))))))
(fx/defn show-existing-multiaccount-alert
[_ key-uid]
@ -168,22 +163,24 @@
(if-not (string/blank? (:error (types/json->clj phrase-warnings)))
(popover/show-popover cofx {:view :custom-seed-phrase})
(when (mnemonic/valid-length? passphrase)
{::import-multiaccount {:passphrase (mnemonic/sanitize-passphrase passphrase)
:password password}}))))
{::import-multiaccount {:passphrase (mnemonic/sanitize-passphrase passphrase)
:password password
:success-event ::import-multiaccount-success}}))))
(fx/defn seed-phrase-next-pressed
{:events [:multiaccounts.recover/enter-phrase-next-pressed]}
[{:keys [db] :as cofx}]
(let [{:keys [passphrase]} (:intro-wizard db)]
{::validate-mnemonic [passphrase #(re-frame/dispatch [:multiaccounts.recover/phrase-validated %])]}))
{::multiaccounts/validate-mnemonic [passphrase #(re-frame/dispatch [:multiaccounts.recover/phrase-validated %])]}))
(fx/defn continue-to-import-mnemonic
{:events [::continue-pressed]}
[{:keys [db] :as cofx}]
(let [{:keys [password passphrase]} (:multiaccounts/recover db)]
(fx/merge cofx
{::import-multiaccount {:passphrase passphrase
:password password}}
{::import-multiaccount {:passphrase passphrase
:password password
:success-event ::import-multiaccount-success}}
(popover/hide-popover))))
(fx/defn dec-step

View File

@ -107,6 +107,8 @@
(reg-root-key-sub :multiaccount :multiaccount)
(reg-root-key-sub :multiaccount/accounts :multiaccount/accounts)
(reg-root-key-sub :get-recover-multiaccount :multiaccounts/recover)
(reg-root-key-sub :multiaccounts/key-storage :multiaccounts/key-storage)
;;chat
(reg-root-key-sub ::cooldown-enabled? :chat/cooldown-enabled?)
(reg-root-key-sub ::chats :chats)
@ -339,6 +341,19 @@
(fn [[intro-wizard multiaccounts]]
(recover/existing-account? (:root-key intro-wizard) multiaccounts)))
(defn login-ma-keycard-pairing
"Compute the keycard-pairing value of the multiaccount selected for login"
[db _]
(when-let [acc-to-login (-> db :multiaccounts/login)]
(-> db
:multiaccounts/multiaccounts
(get (:key-uid acc-to-login))
:keycard-pairing)))
(re-frame/reg-sub
:intro-wizard/acc-to-login-keycard-pairing
login-ma-keycard-pairing)
(re-frame/reg-sub
:current-network
:<- [:networks/networks]

View File

@ -20,3 +20,20 @@
(testing "Check if transactions are sorted by date"
(is (= (#'status-im.subs/group-transactions-by-date transactions)
grouped-transactions))))
(deftest login-ma-keycard-pairing
(testing "returns nil when no :multiaccounts/login"
(let [res (status-im.subs/login-ma-keycard-pairing
{:multiaccounts/login nil
:multiaccounts/multiaccounts
{"0x1" {:keycard-pairing "keycard-pairing-code"}}}
{})]
(is (nil? res))))
(testing "returns :keycard-pairing when :multiaccounts/login is present"
(let [res (status-im.subs/login-ma-keycard-pairing
{:multiaccounts/login {:key-uid "0x1"}
:multiaccounts/multiaccounts
{"0x1" {:keycard-pairing "keycard-pairing-code"}}}
{})]
(is (= res "keycard-pairing-code")))))

View File

@ -0,0 +1,35 @@
(ns status-im.ui.components.accordion
(:require [reagent.core :as reagent]
[quo.core :as quo]
[status-im.ui.components.colors :as colors]
[status-im.ui.components.react :as react]
[status-im.ui.components.icons.vector-icons :as icons]))
(defn section
"Render collapsible section"
[_props]
(let [opened? (reagent/atom false)]
(fn [{:keys [title cnt content icon]}]
[react/view {:padding-vertical 8}
[quo/list-item
{:title title
:icon icon
:on-press #(swap! opened? not)
:accessory
[react/view {:flex-direction :row :align-items :center}
(when (pos? cnt)
[react/text {:style {:color colors/gray}} cnt])
[icons/icon (if @opened? :main-icons/dropdown-up :main-icons/dropdown)
{:container-style {:align-items :center
:margin-left 8
:justify-content :center}
:resize-mode :center
:color colors/black}]]}]
(when @opened?
content)])))
(defn accordion
"List of collapseable sections"
[]
;; TODO(shivekkhurana): Extract status-im.ui.screens.wallet.recipient.views/accordion component here
)

View File

@ -372,7 +372,7 @@
[top-bar {:step :enter-phrase}]
[enter-phrase wizard-state]
[bottom-bar (merge {:step :enter-phrase
:forward-action :multiaccounts.recover/enter-phrase-next-pressed}
:forward-action :multiaccounts.recover/enter-phrase-next-pressed}
wizard-state)]]]))
(defview wizard-recovery-success []

View File

@ -16,12 +16,15 @@
(:require-macros [status-im.utils.views :refer [defview letsubs]]))
(defview intro []
(letsubs [flow [:keycard-flow]]
(letsubs [flow [:keycard-flow]
{:keys [from-key-storage-and-migration?]} [:keycard]]
[react/view styles/container
[topbar/topbar]
(when-not from-key-storage-and-migration?
[topbar/topbar])
[react/view {:flex 1
:justify-content :space-between
:align-items :center}
:align-items :center
:margin-top (when from-key-storage-and-migration? 80)}
[react/view {:align-items :center}
[react/view
[react/view {:align-items :center

View File

@ -0,0 +1,26 @@
(ns status-im.ui.screens.multiaccounts.key-storage.styles
(:require [status-im.ui.components.colors :as colors]))
(def help-text-container
{:width "60%"
:align-self :center
:padding-vertical 24})
(def help-text
{:text-align :center})
(def popover-title
{:typography :title-bold
:margin-top 8
:margin-bottom 24})
(def popover-body-container
{:flex-wrap :wrap
:flex-direction :row
:justify-content :center
:text-align :center})
(def popover-text
{:color colors/gray
:text-align :center
:line-height 22})

View File

@ -0,0 +1,336 @@
(ns status-im.ui.screens.multiaccounts.key-storage.views
(:require-macros [status-im.utils.views :refer [defview letsubs]])
(:require [quo.core :as quo]
[re-frame.core :as re-frame]
[re-frame.db]
[status-im.i18n :as i18n]
[status-im.multiaccounts.core :as multiaccounts]
[status-im.multiaccounts.key-storage.core :as multiaccounts.key-storage]
[status-im.react-native.resources :as resources]
[status-im.ui.components.colors :as colors]
[status-im.ui.components.icons.vector-icons :as vector-icons]
[status-im.ui.components.react :as react]
[status-im.ui.components.chat-icon.screen :as chat-icon.screen]
[status-im.ui.components.topbar :as topbar]
[status-im.ui.components.toolbar :as toolbar]
[status-im.ui.components.accordion :as accordion]
[status-im.ui.screens.multiaccounts.views :as multiaccounts.views]
[status-im.ui.screens.multiaccounts.key-storage.styles :as styles]
[status-im.utils.security]))
(defn local-topbar [subtitle]
[topbar/topbar {:title (i18n/label :t/key-managment)
:subtitle subtitle}])
(defonce accordian-data
[{:id :type
:label (i18n/label :t/type)
:value (i18n/label :t/master-account)}
{:id :back-up
:label (i18n/label :t/back-up)
:value (i18n/label :t/recovery-phrase)}
{:id :storage
:label (i18n/label :t/storage)
:value (i18n/label :t/key-on-device)}])
(defn accordion-content []
[react/view {:padding-horizontal 16
:flex-direction :row}
[react/view {:flex-shrink 0
:margin-right 20}
(for [{:keys [id label]} accordian-data]
^{:key (str "left-" id)}
[react/text {:style {:color colors/gray
:padding-vertical 8}} label])]
[react/view {:flex 1}
(for [{:keys [id value]} accordian-data]
^{:key (str "right-" id)}
[react/text {:flex 1
:flex-wrap :wrap
:style {:padding-vertical 8}}
value])]])
;; Component to render Key and Storage management screen
(defview actions-base [{:keys [next-title next-event]}]
(letsubs [{:keys [name] :as multiaccount} [:multiaccounts/login]
{:keys [move-keystore-checked?]} [:multiaccounts/key-storage]]
[react/view {:flex 1}
[local-topbar (i18n/label :t/choose-actions)]
[accordion/section {:title name
:icon [chat-icon.screen/contact-icon-contacts-tab
(multiaccounts/displayed-photo multiaccount)]
:count 0
:content [accordion-content]}]
[react/view {:flex 1
:flex-direction :column
:justify-content :space-between}
[react/view
[quo/list-header (i18n/label :t/actions)]
[quo/list-item {:title (i18n/label :t/move-keystore-file)
:subtitle (i18n/label :t/select-new-location-for-keys)
:subtitle-max-lines 4
:accessory :checkbox
:active move-keystore-checked?
:on-press #(re-frame/dispatch [::multiaccounts.key-storage/move-keystore-checked (not move-keystore-checked?)])}]
[quo/list-item {:title (i18n/label :t/reset-database)
:subtitle (i18n/label :t/reset-database-warning)
:subtitle-max-lines 4
:disabled true
:active move-keystore-checked?
:accessory :checkbox}]]
(when (and next-title next-event)
[toolbar/toolbar {:show-border? true
:right [quo/button
{:type :secondary
:disabled (not move-keystore-checked?)
:on-press #(re-frame/dispatch next-event)
:after :main-icons/next}
next-title]}])]]))
(defn actions-not-logged-in
"To be used when the flow is accessed before login, will enter seed phrase next"
[]
[actions-base {:next-title (i18n/label :t/enter-seed-phrase)
:next-event [::multiaccounts.key-storage/enter-seed-pressed]}])
(defn actions-logged-in
"To be used when the flow is accessed from profile, will choose storage next"
[]
[actions-base {:next-title (i18n/label :t/choose-storage)
:next-event [::multiaccounts.key-storage/choose-storage-pressed]}])
(defview seed-phrase []
(letsubs
[{:keys [seed-word-count seed-shape-invalid?]} [:multiaccounts/key-storage]]
[react/keyboard-avoiding-view {:flex 1}
[local-topbar (i18n/label :t/enter-seed-phrase)]
[multiaccounts.views/seed-phrase-input
{:on-change-event [::multiaccounts.key-storage/seed-phrase-input-changed]
:seed-word-count seed-word-count
:seed-shape-invalid? seed-shape-invalid?}]
[react/text {:style {:color colors/gray
:font-size 14
:margin-bottom 8
:text-align :center}}
(i18n/label :t/multiaccounts-recover-enter-phrase-text)]
[toolbar/toolbar {:show-border? true
:right [quo/button
{:type :secondary
:disabled (or seed-shape-invalid?
(nil? seed-shape-invalid?))
:on-press #(re-frame/dispatch [::multiaccounts.key-storage/choose-storage-pressed])
:after :main-icons/next}
(i18n/label :t/choose-storage)]}]]))
(defn keycard-subtitle []
[react/view
[react/text {:style {:color colors/gray}} (i18n/label :t/empty-keycard-required)]
[react/view {:flex-direction :row
:align-items :center}
[react/text {:style {:color colors/blue}
:accessibility-label :learn-more
:on-press #(js/alert :press)}
(i18n/label :learn-more)]
[vector-icons/icon :main-icons/tiny-external {:color colors/blue
:width 16
:height 16}]]])
(defn keycard-upsell-banner []
[react/touchable-highlight {:on-press #(.openURL ^js react/linking "https://keycard.tech/")}
[react/view {:background-color (if (= :dark @colors/theme) "#2C5955" "#DDF8F4")
:border-radius 16
:margin 16
:padding-horizontal 12
:padding-vertical 8
:flex-direction :row}
[react/view
[react/image {:source (resources/get-theme-image :keycard)
:resize-mode :contain
:style {:width 48
:height 48}}]]
[react/view {:flex 1
:margin-left 12}
[react/text {:style {:font-size 20
:font-weight "700"}}
(i18n/label :t/get-a-keycard)]
[react/text {:style {:color (colors/alpha colors/text 0.8)}}
(i18n/label :t/keycard-upsell-subtitle)]]]])
(defview storage []
(letsubs
[{:keys [keycard-storage-selected?]} [:multiaccounts/key-storage]]
[react/view {:flex 1}
[local-topbar (i18n/label :t/choose-storage)]
[react/view {:style styles/help-text-container}
[react/text {:style styles/help-text}
(i18n/label :t/choose-new-location-for-keystore)]]
[react/view
[quo/list-header (i18n/label :t/current)]
[quo/list-item {:title (i18n/label :t/this-device)
:text-size :base
:icon :main-icons/mobile
:disabled true}]
[quo/list-header (i18n/label :t/new)]
[quo/list-item {:title (i18n/label :t/keycard)
:subtitle (i18n/label :t/empty-keycard-required)
:subtitle-max-lines 4
:icon :main-icons/keycard
:active keycard-storage-selected?
:on-press #(re-frame/dispatch [::multiaccounts.key-storage/keycard-storage-pressed (not keycard-storage-selected?)])
:accessory :radio}]]
[react/view {:flex 1
:justify-content :flex-end}
(when-not keycard-storage-selected?
[keycard-upsell-banner])
[toolbar/toolbar {:show-border? true
:right [quo/button
{:type :secondary
:disabled (not keycard-storage-selected?)
:on-press #(re-frame/dispatch [::multiaccounts.key-storage/show-transfer-warning-popup])}
(i18n/label :t/confirm)]}]]]))
(defview seed-key-uid-mismatch-popover []
(letsubs [{:keys [name]} [:multiaccounts/login]]
[react/view {:margin-top 24
:margin-horizontal 24
:align-items :center}
[react/view {:width 32
:height 32
:border-radius 16
:align-items :center
:justify-content :center}
[vector-icons/icon :main-icons/warning {:color colors/blue}]]
[react/text {:style {:typography :title-bold
:margin-top 8
:margin-bottom 24}}
(i18n/label :t/seed-key-uid-mismatch)]
[react/view styles/popover-body-container
[react/view
[react/text {:style (into styles/popover-text
{:margin-bottom 16})}
(i18n/label :t/seed-key-uid-mismatch-desc-1 {:multiaccount-name name})]
[react/text {:style styles/popover-text}
(i18n/label :t/seed-key-uid-mismatch-desc-2)]]]
[react/view {:margin-vertical 24
:align-items :center}
[quo/button {:on-press #(re-frame/dispatch [:hide-popover])
:accessibility-label :cancel-custom-seed-phrase
:type :secondary}
(i18n/label :t/try-again)]]]))
(defview transfer-multiaccount-warning-popover []
[react/view {:margin-top 24
:margin-horizontal 24
:align-items :center}
[react/view {:width 32
:height 32
:border-radius 16
:align-items :center
:justify-content :center}
[vector-icons/icon :main-icons/tiny-warning-background {:color colors/red}]]
[react/text {:style styles/popover-title}
(i18n/label :t/move-keystore-file-to-keycard)]
[react/view styles/popover-body-container
[react/text {:style styles/popover-text}
(i18n/label :t/database-reset-warning)]]
[react/view {:margin-vertical 24
:align-items :center}
[quo/button {:on-press #(re-frame/dispatch [::multiaccounts.key-storage/delete-multiaccount-and-init-keycard-onboarding])
:accessibility-label :cancel-custom-seed-phrase
:type :primary
:theme :negative}
(i18n/label :t/move-and-reset)]
[quo/button {:on-press #(re-frame/dispatch [:hide-popover])
:accessibility-label :cancel-custom-seed-phrase
:type :secondary}
(i18n/label :t/cancel)]]])
(defview unknown-error-popover []
[react/view {:margin-top 24
:margin-horizontal 24
:align-items :center}
[react/view {:width 32
:height 32
:border-radius 16
:align-items :center
:justify-content :center}
[vector-icons/icon :main-icons/close {:color colors/red}]]
[react/text {:style {:typography :title-bold
:margin-top 8
:margin-bottom 24}}
(i18n/label :t/something-went-wrong)]
[react/view styles/popover-body-container
[react/view
[react/text {:style (into styles/popover-text
{:margin-bottom 16})}
(i18n/label :t/transfer-ma-unknown-error-desc-1)]
[react/text {:style styles/popover-text}
(i18n/label :t/transfer-ma-unknown-error-desc-2)]]]
[react/view {:margin-vertical 24
:align-items :center}
[quo/button {:on-press #(re-frame/dispatch [::multiaccounts.key-storage/hide-popover-and-goto-multiaccounts-screen])
:type :secondary}
(i18n/label :t/okay)]]])
(comment
;; UI flow
(do
;; Goto key management actions screen
(re-frame/dispatch [::multiaccounts.key-storage/key-and-storage-management-pressed])
;; Check move key store checkbox
(re-frame/dispatch [::multiaccounts.key-storage/move-keystore-checked true])
;; Goto enter seed screen
(re-frame/dispatch [::multiaccounts.key-storage/enter-seed-pressed])
;; Enter seed phrase
;; invalid seed shape
#_(re-frame/dispatch [::multiaccounts.key-storage/seed-phrase-input-changed (status-im.utils.security/mask-data "h h h h h h h h h h h h")])
;; valid seed for Trusty Candid Bighornedsheep
;; If you try to select Dim Venerated Yaffle, but use this seed instead, validate-seed-against-key-uid will fail miserably
#_(re-frame/dispatch [::multiaccounts.key-storage/seed-phrase-input-changed
(status-im.utils.security/mask-data "disease behave roof exile ghost head carry item tumble census rocket champion")])
;; valid seed for Swiffy Warlike Seagull
#_(re-frame/dispatch [::multiaccounts.key-storage/seed-phrase-input-changed
(status-im.utils.security/mask-data "dirt agent garlic merge tuna leaf congress hedgehog absent dish pizza scrap")])
;; valid seed for Dim Venerated Yaffle (this is just a test account, okay to leak seed)
(re-frame/dispatch [::multiaccounts.key-storage/seed-phrase-input-changed
(status-im.utils.security/mask-data "rocket mixed rebel affair umbrella legal resemble scene virus park deposit cargo")])
;; Click choose storage
(re-frame/dispatch [::multiaccounts.key-storage/choose-storage-pressed])
;; Choose Keycard from storage options
(re-frame/dispatch [::multiaccounts.key-storage/keycard-storage-pressed true])
;; Confirm migration popup
(re-frame/dispatch [::multiaccounts.key-storage/show-transfer-warning-popup])
;; Delete multiaccount and init keycard onboarding
(re-frame/dispatch [::multiaccounts.key-storage/delete-multiaccount-and-init-keycard-onboarding]))
;; Show error popup
(re-frame/dispatch [::multiaccounts.key-storage/show-seed-key-uid-mismatch-error-popup])
(re-frame/dispatch [::multiaccounts.key-storage/show-transfer-warning-popup])
(re-frame/dispatch [::multiaccounts.key-storage/delete-multiaccount-error])
(re-frame/dispatch [:hide-popover])
;; Flow to populate state after multiaccount is deleted
(do
;; set seed phrase for Dim Venerated Yaffle
(re-frame/dispatch [:set-in [:multiaccounts/key-storage :seed-phrase] "rocket mixed rebel affair umbrella legal resemble scene virus park deposit cargo"])
;; set seed for Trusty Candid Bighornedsheep
#_(re-frame/dispatch [:set-in [:multiaccounts/key-storage :seed-phrase] "disease behave roof exile ghost head carry item tumble census rocket champion"])
;; simulate delete multiaccount success
(re-frame/dispatch [::multiaccounts.key-storage/delete-multiaccount-success])))

View File

@ -45,7 +45,11 @@
view-id [:view-id]
supported-biometric-auth [:supported-biometric-auth]]
[react/keyboard-avoiding-view {:style ast/multiaccounts-view}
[topbar/topbar {:border-bottom false}]
[topbar/topbar {:border-bottom false
:right-accessories [{:icon :more
:on-press #(do
(react/dismiss-keyboard!)
(re-frame/dispatch [:multiaccounts.recover.ui/recover-multiaccount-button-pressed]))}]}]
[react/scroll-view {:keyboardShouldPersistTaps :always
:style styles/login-view}
[react/view styles/login-badge-container
@ -93,18 +97,10 @@
[react/i18n-text {:style styles/processing :key :processing}]])
[toolbar/toolbar
{:show-border? true
:size :large
:left
[quo/button
{:type :secondary
:on-press #(do
(react/dismiss-keyboard!)
(re-frame/dispatch [:multiaccounts.recover.ui/recover-multiaccount-button-pressed]))}
(i18n/label :t/access-existing-keys)]
:right
{:size :large
:center
[react/view {:padding-horizontal 8}
[quo/button
{:disabled (or (not sign-in-enabled?) processing)
:on-press #(login-multiaccount @password-text-input)}
(i18n/label :t/submit)]]}]]))
(i18n/label :t/sign-in)]]}]]))

View File

@ -1,11 +1,13 @@
(ns status-im.ui.screens.multiaccounts.recover.views
(:require-macros [status-im.utils.views :refer [defview]])
(:require-macros [status-im.utils.views :refer [defview letsubs]])
(:require [re-frame.core :as re-frame]
[status-im.ui.components.react :as react]
[status-im.multiaccounts.recover.core :as multiaccounts.recover]
[status-im.multiaccounts.key-storage.core :as multiaccounts.key-storage]
[status-im.keycard.recovery :as keycard]
[status-im.i18n :as i18n]
[status-im.utils.config :as config]
[status-im.utils.security]
[status-im.ui.components.colors :as colors]
[quo.core :as quo]
[status-im.utils.platform :as platform]
@ -42,32 +44,71 @@
:type :secondary}
(i18n/label :t/cancel)]]]])
(defn bottom-sheet-view []
[react/view {:flex 1 :flex-direction :row}
[react/view {:flex 1}
[quo/list-item
{:theme :accent
:title (i18n/label :t/enter-seed-phrase)
:accessibility-label :enter-seed-phrase-button
:icon :main-icons/text
:on-press #(hide-sheet-and-dispatch [::multiaccounts.recover/enter-phrase-pressed])}]
(when (or platform/android?
config/keycard-test-menu-enabled?)
(defview bottom-sheet-view []
(letsubs [view-id [:view-id]
acc-to-login-keycard-pairing [:intro-wizard/acc-to-login-keycard-pairing]]
[react/view {:flex 1 :flex-direction :row}
[react/view {:flex 1}
;; Show manage storage link when on login screen, only on android devices
;; and the selected account is not paired with keycard
(when (and (= view-id :login)
platform/android?
(not acc-to-login-keycard-pairing))
[quo/list-item
{:theme :accent
:title (i18n/label :t/manage-keys-and-storage)
:accessibility-label :enter-seed-phrase-button
:icon :main-icons/key
:on-press #(hide-sheet-and-dispatch [::multiaccounts.key-storage/key-and-storage-management-pressed])}])
[quo/list-item
{:theme :accent
:title (i18n/label :t/recover-with-keycard)
:accessibility-label :recover-with-keycard-button
:icon [react/view {:border-width 1
:border-radius 20
:border-color colors/blue-light
:background-color colors/blue-light
:justify-content :center
:align-items :center
:width 40
:height 40}
[react/image {:source (resources/get-image :keycard-logo-blue)
:style {:width 24 :height 24}}]]
:on-press #(hide-sheet-and-dispatch [::keycard/recover-with-keycard-pressed])}])]])
:title (i18n/label :t/recover-with-seed-phrase)
:accessibility-label :enter-seed-phrase-button
:icon :main-icons/text
:on-press #(hide-sheet-and-dispatch [::multiaccounts.recover/enter-phrase-pressed])}]
(when (or platform/android?
config/keycard-test-menu-enabled?)
[quo/list-item
{:theme :accent
:title (i18n/label :t/recover-with-keycard)
:accessibility-label :recover-with-keycard-button
:icon [react/view {:border-width 1
:border-radius 20
:border-color colors/blue-light
:background-color colors/blue-light
:justify-content :center
:align-items :center
:width 40
:height 40}
[react/image {:source (resources/get-image :keycard-logo-blue)
:style {:width 24 :height 24}}]]
:on-press #(hide-sheet-and-dispatch [::keycard/recover-with-keycard-pressed])}])]]))
(def bottom-sheet
{:content bottom-sheet-view})
(comment
;; Recover with seed to device UI flow
(do
;; Press get-started on welcome screen
(re-frame/dispatch [:multiaccounts.create.ui/intro-wizard])
;; Goto seed screen
(re-frame/dispatch [::multiaccounts.recover/enter-phrase-pressed])
;; Enter seed phrase for Dim Venerated Yaffle
(re-frame/dispatch [:multiaccounts.recover/enter-phrase-input-changed
(status-im.utils.security/mask-data "rocket mixed rebel affair umbrella legal resemble scene virus park deposit cargo")])
;; Recover multiaccount
(re-frame/dispatch [:multiaccounts.recover/enter-phrase-next-pressed])
;; Press Re-encrypt
(re-frame/dispatch [:multiaccounts.recover/re-encrypt-pressed])
;; Press next on default storage (ie store on device)
(re-frame/dispatch [:multiaccounts.recover/select-storage-next-pressed])
;; Enter password (need to wait for a moment for this to finish)
(re-frame/dispatch [:multiaccounts.recover/enter-password-next-pressed {:key-code "111111"}])))

View File

@ -6,6 +6,7 @@
[status-im.ui.screens.multiaccounts.styles :as styles]
[status-im.ui.components.list.views :as list]
[status-im.ui.components.react :as react]
[status-im.utils.security :as security]
[status-im.i18n :as i18n]
[status-im.ui.components.colors :as colors]
[status-im.ui.components.topbar :as topbar]
@ -56,3 +57,29 @@
{:on-press #(re-frame/dispatch [:multiaccounts.recover.ui/recover-multiaccount-button-pressed])
:type :secondary}
(i18n/label :t/access-existing-keys)]}]]))
(defn seed-phrase-input [{:keys [on-change-event
seed-word-count
seed-shape-invalid?]}]
[react/view {:flex 1
:justify-content :center
:padding-horizontal 16}
[quo/text-input
{:show-cancel false
:auto-correct false
:placeholder (i18n/label :t/seed-phrase-placeholder)
:monospace true
:multiline true
:auto-focus true
:accessibility-label :passphrase-input
:on-change-text #(re-frame/dispatch (conj on-change-event (security/mask-data %)))}]
;; word counter view
[react/view {:align-items :flex-end}
[react/view {:flex-direction :row
:align-items :center
:padding-vertical 8
:opacity (if seed-word-count 1 0)}
[quo/text {:color (if seed-shape-invalid? :secondary :main)
:size :small}
(when-not seed-shape-invalid? "✓ ")
(i18n/label-pluralize seed-word-count :t/words-n)]]]])

View File

@ -13,6 +13,7 @@
[status-im.ui.components.invite.advertiser :as advertiser.invite]
[status-im.ui.components.invite.dapp :as dapp.invite]
[status-im.ui.screens.multiaccounts.recover.views :as multiaccounts.recover]
[status-im.ui.screens.multiaccounts.key-storage.views :as multiaccounts.key-storage]
[status-im.ui.screens.signing.views :as signing]
[status-im.ui.screens.biometric.views :as biometric]
[status-im.ui.components.colors :as colors]
@ -154,6 +155,15 @@
(= :dapp-invite view)
[dapp.invite/accept-popover]
(= :seed-key-uid-mismatch view)
[multiaccounts.key-storage/seed-key-uid-mismatch-popover]
(= :transfer-multiaccount-to-keycard-warning view)
[multiaccounts.key-storage/transfer-multiaccount-warning-popover]
(= :transfer-multiaccount-unknown-error view)
[multiaccounts.key-storage/unknown-error-popover]
:else
[view])]]]]])))})))

View File

@ -55,10 +55,14 @@
(fn []
(log/debug :on-screen-focus name)
(let [on-back-press (fn []
(when (and back-handler
(not= back-handler :noop))
(re-frame/dispatch back-handler))
(boolean back-handler))]
(if (fn? back-handler)
(back-handler)
(do
(when (and back-handler
(vector? back-handler)
(not= back-handler :noop))
(re-frame/dispatch back-handler))
(boolean back-handler))))]
(when on-focus (re-frame/dispatch on-focus))
(add-back-handler-listener on-back-press)
(fn []

View File

@ -4,6 +4,7 @@
[status-im.ui.screens.progress.views :as progress]
[status-im.ui.screens.multiaccounts.views :as multiaccounts]
[status-im.ui.screens.intro.views :as intro]
[status-im.keycard.core :as keycard.core]
[status-im.ui.screens.keycard.onboarding.views :as keycard.onboarding]
[status-im.ui.screens.keycard.recovery.views :as keycard.recovery]
[status-im.ui.screens.keycard.views :as keycard]
@ -61,7 +62,7 @@
:back-handler :noop
:component intro/wizard-recovery-success}
{:name :keycard-onboarding-intro
:back-handler :noop
:back-handler keycard.core/onboarding-intro-back-handler
:component keycard.onboarding/intro}
{:name :keycard-onboarding-puk-code
:back-handler :noop

View File

@ -0,0 +1,18 @@
(ns status-im.ui.screens.routing.key-storage-stack
"Manage flow required to change key-storage location"
(:require [status-im.ui.screens.routing.core :as navigation]
[status-im.ui.screens.multiaccounts.key-storage.views :as key-storage.views]))
(defonce stack (navigation/create-stack))
(defn key-storage-stack []
[stack {:initial-route-name :actions-not-logged-in
:header-mode :none}
[{:name :actions-not-logged-in
:component key-storage.views/actions-not-logged-in}
{:name :actions-logged-in
:component key-storage.views/actions-logged-in}
{:name :seed-phrase
:component key-storage.views/seed-phrase}
{:name :storage
:component key-storage.views/storage}]])

View File

@ -11,6 +11,7 @@
[status-im.ui.screens.routing.intro-login-stack :as intro-login-stack]
[status-im.ui.screens.routing.chat-stack :as chat-stack]
[status-im.ui.screens.routing.wallet-stack :as wallet-stack]
[status-im.ui.screens.routing.key-storage-stack :as key-storage-stack]
[status-im.ui.screens.group.views :as group-chat]
[status-im.ui.screens.group.events :as group.events]
[status-im.ui.screens.routing.profile-stack :as profile-stack]
@ -153,7 +154,10 @@
{:name :profile
:transition :presentation-ios
:insets {:bottom true}
:component contact/profile}]
:component contact/profile}
{:name :key-storage-stack
:component key-storage-stack/key-storage-stack}]
(when config/quo-preview-enabled?
[{:name :quo-preview
:insets {:top false :bottom false}

View File

@ -16,7 +16,7 @@
[status-im.wallet.core :as wallet]
[clojure.string :as string]
[status-im.utils.security :as security]
[status-im.multiaccounts.recover.core :as recover]
[status-im.multiaccounts.core :as multiaccounts]
[status-im.ethereum.mnemonic :as mnemonic]
[taoensso.timbre :as log]
[status-im.wallet.prices :as prices]
@ -151,10 +151,10 @@
(fx/defn import-new-account-seed
[{:keys [db]} passphrase hashed-password]
{:db (assoc-in db [:add-account :step] :generating)
::recover/validate-mnemonic [(security/safe-unmask-data passphrase)
#(re-frame/dispatch [:wallet.accounts/seed-validated
% passphrase hashed-password])]})
{:db (assoc-in db [:add-account :step] :generating)
::multiaccounts/validate-mnemonic [(security/safe-unmask-data passphrase)
#(re-frame/dispatch [:wallet.accounts/seed-validated
% passphrase hashed-password])]})
(fx/defn new-account-seed-validated
{:events [:wallet.accounts/seed-validated]}

View File

@ -737,6 +737,7 @@
"main-wallet": "Main Wallet",
"mainnet-network": "Main network",
"make-admin": "Make admin",
"manage-keys-and-storage": "Manage keys and storage",
"mark-all-read": "Mark all read",
"members": {
"one": "1 member",
@ -1033,7 +1034,7 @@
"show-qr": "Show QR code",
"show-transaction-data": "Show transaction data",
"sign-and-send": "Sign and send",
"sign-in": "Unlock",
"sign-in": "Sign in",
"sign-message": "Sign Message",
"sign-out": "Sign out",
"sign-with": "Sign with",
@ -1373,5 +1374,30 @@
"connect-wallet": "Connect wallet",
"open-chat": "Open chat",
"favourite-description": "Your favourite websites will appear here",
"transfers-fetching-failure": "Transfers history could not be updated. Check your connection and pull down to try again"
"transfers-fetching-failure": "Transfers history could not be updated. Check your connection and pull down to try again",
"move-and-reset": "Move and Reset",
"move-keystore-file-to-keycard": "Move keystore file to keycard?",
"database-reset-warning": "Database will be reset. Chats, contacts and settings will be deleted",
"empty-keycard-required": "Requires an empty Keycard",
"current": "Current",
"choose-storage": "Choose storage",
"choose-new-location-for-keystore": "Choose a new location to save your keystore file",
"get-a-keycard": "Get a Keycard",
"keycard-upsell-subtitle": "Your portable, easy to use hardware wallet",
"actions": "Actions",
"move-keystore-file": "Move keystore file",
"select-new-location-for-keys": "Select a new location to save your private key(s)",
"reset-database": "Reset database",
"reset-database-warning": "Delete chats, contacts and settings. Required when youve lost your password",
"key-managment": "Key management",
"choose-actions": "Choose actions",
"master-account": "Master account",
"back-up": "Back up",
"key-on-device": "Private key is saved on this device",
"seed-key-uid-mismatch": "Seed doesn't match",
"seed-key-uid-mismatch-desc-1": "The seed phrase you entered does not match {{multiaccount-name}}",
"seed-key-uid-mismatch-desc-2": "To manage keys for this account verify your seed phrase and try again.",
"recover-with-seed-phrase": "Recover with seed phrase",
"transfer-ma-unknown-error-desc-1": "It looks like your multiaccount was not deleted. Database may have been reset",
"transfer-ma-unknown-error-desc-2": "Please check your account list and try again. If the account is not listed go to Access existing keys to recover with seed phrase"
}