[#10452] Keycard: updated PUK flow

This commit is contained in:
Roman Volosovskyi 2020-05-14 17:35:43 +03:00
parent 4021e957ea
commit a1027cdfcc
No known key found for this signature in database
GPG Key ID: 0238A4B5ECEE70DE
28 changed files with 950 additions and 335 deletions

View File

@ -348,7 +348,8 @@
response])) response]))
:on-failure :on-failure
(fn [response] (fn [response]
(log/info "[keycard response fail] get-keys") (log/info "[keycard response fail] get-keys"
(error-object->map response))
(re-frame/dispatch [:hardwallet.callback/on-get-keys-error (re-frame/dispatch [:hardwallet.callback/on-get-keys-error
(error-object->map response)]))}))) (error-object->map response)]))})))

View File

@ -4,13 +4,16 @@
[status-im.hardwallet.onboarding :as onboarding] [status-im.hardwallet.onboarding :as onboarding]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[status-im.hardwallet.common :as common])) [status-im.hardwallet.common :as common]
[status-im.hardwallet.login :as hardwallet.login]))
(fx/defn change-pin-pressed (fx/defn change-pin-pressed
{:events [:keycard-settings.ui/change-pin-pressed]} {:events [:keycard-settings.ui/change-pin-pressed]}
[{:keys [db] :as cofx}] [{:keys [db] :as cofx}]
(let [pin-retry-counter (get-in db [:hardwallet :application-info :pin-retry-counter]) (let [pin-retry-counter (get-in db [:hardwallet :application-info :pin-retry-counter])
enter-step (if (zero? pin-retry-counter) :puk :current)] enter-step (if (zero? pin-retry-counter) :puk :current)]
(if (= enter-step :puk)
(hardwallet.login/reset-pin cofx)
(fx/merge cofx (fx/merge cofx
{:db {:db
(assoc-in db [:hardwallet :pin] {:enter-step enter-step (assoc-in db [:hardwallet :pin] {:enter-step enter-step
@ -21,7 +24,7 @@
:status nil :status nil
:error-label nil :error-label nil
:on-verified :hardwallet/proceed-to-change-pin})} :on-verified :hardwallet/proceed-to-change-pin})}
(common/navigate-to-enter-pin-screen)))) (common/navigate-to-enter-pin-screen)))))
(fx/defn proceed-to-change-pin (fx/defn proceed-to-change-pin
{:events [:hardwallet/proceed-to-change-pin]} {:events [:hardwallet/proceed-to-change-pin]}

View File

@ -204,10 +204,10 @@
(fx/defn clear-pin (fx/defn clear-pin
[{:keys [db] :as cofx}] [{:keys [db] :as cofx}]
(fx/merge cofx (fx/merge
{:db (update-in db cofx
{:db (assoc-in db
[:hardwallet :pin] [:hardwallet :pin]
merge
{:status nil {:status nil
:login (get-in db [:hardwallet :pin :original]) :login (get-in db [:hardwallet :pin :original])
:export-key [] :export-key []
@ -216,15 +216,18 @@
:current [] :current []
:original [] :original []
:confirmation [] :confirmation []
:error-label nil})})) :error-label nil
:on-verified (get-in db [:hardwallet :pin :on-verified])
:on-verified-failure (get-in db [:hardwallet :pin :on-verified])})}))
(fx/defn cancel-sheet-confirm (fx/defn cancel-sheet-confirm
{:events [::cancel-sheet-confirm {:events [::cancel-sheet-confirm
:hardwallet/back-button-pressed]} :hardwallet/back-button-pressed]}
[cofx] [{:keys [db] :as cofx}]
(when-not (get-in db [:hardwallet :card-connected?])
(fx/merge cofx (fx/merge cofx
(hide-connection-sheet) (hide-connection-sheet)
(clear-pin))) (clear-pin))))
(fx/defn cancel-sheet (fx/defn cancel-sheet
{:events [::cancel-sheet]} {:events [::cancel-sheet]}
@ -293,6 +296,9 @@
(let [key-uid (get-in db [:multiaccounts/login :key-uid]) (let [key-uid (get-in db [:multiaccounts/login :key-uid])
pairing (get-in db [:multiaccounts/multiaccounts key-uid :keycard-pairing]) pairing (get-in db [:multiaccounts/multiaccounts key-uid :keycard-pairing])
pin (string/join (get-in db [:hardwallet :pin :login]))] pin (string/join (get-in db [:hardwallet :pin :login]))]
(log/debug "[keycard] get-keys-from-keycard"
"not nil pairing:" (boolean pairing)
", not empty pin:" (boolean (seq pin)))
(when (and pairing (when (and pairing
(seq pin)) (seq pin))
{:db (assoc-in db [:hardwallet :pin :status] :verifying) {:db (assoc-in db [:hardwallet :pin :status] :verifying)
@ -344,9 +350,14 @@
(if tag-was-lost? (if tag-was-lost?
{:db (assoc-in db [:hardwallet :pin :status] nil)} {:db (assoc-in db [:hardwallet :pin :status] nil)}
(if (re-matches pin-mismatch-error (:error error)) (if (re-matches pin-mismatch-error (:error error))
(fx/merge cofx (fx/merge
{:hardwallet/get-application-info {:pairing (get-pairing db key-uid)} cofx
:db (update-in db [:hardwallet :pin] merge {:status :error {:hardwallet/get-application-info
{:pairing (get-pairing db key-uid)}
:db
(update-in db [:hardwallet :pin] merge
{:status :error
:login [] :login []
:import-multiaccount [] :import-multiaccount []
:error-label :t/pin-mismatch})} :error-label :t/pin-mismatch})}
@ -360,44 +371,57 @@
(fx/defn get-application-info (fx/defn get-application-info
{:events [:hardwallet/get-application-info]} {:events [:hardwallet/get-application-info]}
[{:keys [db]} pairing on-card-read] [{:keys [db]} pairing on-card-read]
(let [key-uid (get-in db [:hardwallet :application-info :key-uid]) (let [key-uid (get-in
db [:hardwallet :application-info :key-uid]
(get-in db [:multiaccounts/login :key-uid]))
pairing' (or pairing (some->> key-uid (get-pairing db)))] pairing' (or pairing (some->> key-uid (get-pairing db)))]
(log/debug "[hardwallet] get-application-info" (log/debug "[hardwallet] get-application-info"
"pairing" pairing') "pairing" pairing')
{:hardwallet/get-application-info {:pairing pairing' {:hardwallet/get-application-info {:pairing pairing'
:on-success on-card-read}})) :on-success on-card-read}}))
(fx/defn frozen-keycard-popup
[{:keys [db] :as cofx}]
(if (:multiaccounts/login db)
(fx/merge
cofx
{:db (assoc-in db [:hardwallet :pin :status] :frozen-card)}
hide-connection-sheet)
{:db (assoc db :popover/popover {:view :frozen-card})}))
(fx/defn on-get-application-info-success (fx/defn on-get-application-info-success
{:events [:hardwallet.callback/on-get-application-info-success]} {:events [:hardwallet.callback/on-get-application-info-success]}
[{:keys [db] :as cofx} info on-success] [{:keys [db] :as cofx} info on-success]
(let [info' (-> info (let [{:keys [pin-retry-counter puk-retry-counter]} info
(js->clj :keywordize-keys true)
(update :key-uid ethereum/normalized-hex))
{:keys [pin-retry-counter puk-retry-counter]} info'
view-id (:view-id db) view-id (:view-id db)
{:keys [on-card-read]} (:hardwallet db) {:keys [on-card-read]} (:hardwallet db)
on-success' (or on-success on-card-read) on-success' (or on-success on-card-read)
enter-step (if (zero? pin-retry-counter) enter-step (get-in db [:hardwallet :pin :enter-step])]
:puk
(get-in db [:hardwallet :pin :enter-step]))]
(log/debug "[hardwallet] on-get-application-info-success" (log/debug "[hardwallet] on-get-application-info-success"
"on-success" on-success') "on-success" on-success'
(fx/merge cofx "pin-retry-counter" pin-retry-counter
"puk-retry-counter" puk-retry-counter)
(fx/merge
cofx
{:db (-> db {:db (-> db
(assoc-in [:hardwallet :pin :enter-step] enter-step) (assoc-in [:hardwallet :pin :enter-step] enter-step)
(update-in [:hardwallet :pin :error-label] #(if (= :puk enter-step) (assoc-in [:hardwallet :application-info] info)
:t/enter-puk-code-description
%))
(assoc-in [:hardwallet :application-info] info')
(assoc-in [:hardwallet :application-info :applet-installed?] true) (assoc-in [:hardwallet :application-info :applet-installed?] true)
(assoc-in [:hardwallet :application-info-error] nil))} (assoc-in [:hardwallet :application-info-error] nil))}
(stash-on-card-read) (stash-on-card-read)
(when (and (zero? pin-retry-counter)
(pos? puk-retry-counter)
(not= enter-step :puk))
(frozen-keycard-popup))
(fn [{:keys [db] :as cofx}]
(if (zero? puk-retry-counter) (if (zero? puk-retry-counter)
{:utils/show-popup {:title (i18n/label :t/error) (fx/merge
:content (i18n/label :t/keycard-blocked)}} cofx
{:db (assoc-in db [:hardwallet :pin :status] :blocked-card)}
hide-connection-sheet)
(when on-success' (when on-success'
(dispatch-event on-success')))))) (dispatch-event cofx on-success')))))))
(fx/defn on-get-application-info-error (fx/defn on-get-application-info-error
{:events [:hardwallet.callback/on-get-application-info-error]} {:events [:hardwallet.callback/on-get-application-info-error]}
@ -484,3 +508,12 @@
{:db (assoc-in db [:hardwallet :pin :status] :verifying) {:db (assoc-in db [:hardwallet :pin :status] :verifying)
:hardwallet/verify-pin {:pin pin :hardwallet/verify-pin {:pin pin
:pairing pairing}})))})))) :pairing pairing}})))}))))
(fx/defn navigete-to-keycard-settings
{:events [::navigate-to-keycard-settings]}
[cofx]
(navigation/navigate-reset
cofx
{:index 1
:routes [{:name :my-profile}
{:name :keycard-settings}]}))

View File

@ -120,22 +120,35 @@
(defn- proceed-to-pin-confirmation [fx] (defn- proceed-to-pin-confirmation [fx]
(assoc-in fx [:db :hardwallet :pin :enter-step] :confirmation)) (assoc-in fx [:db :hardwallet :pin :enter-step] :confirmation))
(defn- proceed-to-pin-reset-confirmation [fx]
(-> fx
(update-in [:db :hardwallet :pin] dissoc :reset-confirmation)
(assoc-in [:db :hardwallet :pin :enter-step] :reset-confirmation)))
(defn- proceed-to-puk-confirmation [fx]
(assoc-in fx [:db :hardwallet :pin :enter-step] :puk))
(fx/defn on-unblock-pin-success (fx/defn on-unblock-pin-success
{:events [:hardwallet.callback/on-unblock-pin-success]} {:events [:hardwallet.callback/on-unblock-pin-success]}
[{:keys [db] :as cofx}] [{:keys [db] :as cofx}]
(let [pairing (common/get-pairing db)] (let [pairing (common/get-pairing db)
reset-pin (get-in db [:hardwallet :pin :reset])]
(fx/merge cofx (fx/merge cofx
{:hardwallet/get-application-info {:pairing pairing} {:hardwallet/get-application-info
:db (-> db {:pairing pairing}
(update-in [:hardwallet :pin] merge {:status nil
:enter-step :original :db
:current [0 0 0 0 0 0] (update-in db [:hardwallet :pin] merge
{:status :after-unblocking
:enter-step :login
:login reset-pin
:confirmation [] :confirmation []
:puk [] :puk []
:puk-restore? true :puk-restore? true
:error-label nil}))} :error-label nil})}
(common/hide-connection-sheet) (common/hide-connection-sheet)
(navigation/navigate-to-cofx :enter-pin-settings nil)))) (common/clear-on-card-connected)
(common/clear-on-card-read))))
(fx/defn on-unblock-pin-error (fx/defn on-unblock-pin-error
{:events [:hardwallet.callback/on-unblock-pin-error]} {:events [:hardwallet.callback/on-unblock-pin-error]}
@ -145,8 +158,12 @@
(log/debug "[hardwallet] unblock pin error" error) (log/debug "[hardwallet] unblock pin error" error)
(when-not tag-was-lost? (when-not tag-was-lost?
(fx/merge cofx (fx/merge cofx
{:hardwallet/get-application-info {:pairing pairing} {:hardwallet/get-application-info
:db (update-in db [:hardwallet :pin] merge {:status :error {:pairing pairing}
:db
(update-in db [:hardwallet :pin] merge
{:status :error
:error-label :t/puk-mismatch :error-label :t/puk-mismatch
:enter-step :puk :enter-step :puk
:puk []})} :puk []})}
@ -215,7 +232,7 @@
(fn [_] {:utils/dispatch-later (fn [_] {:utils/dispatch-later
[{:dispatch [on-verified-failure] [{:dispatch [on-verified-failure]
:ms 200}]})) :ms 200}]}))
(clear-on-verify-handlers)) #_(clear-on-verify-handlers))
(fx/merge cofx (fx/merge cofx
(common/hide-connection-sheet) (common/hide-connection-sheet)
@ -231,12 +248,13 @@
:handler :handler
(fn [{:keys [db]}] (fn [{:keys [db]}]
(let [puk (common/vector->string (get-in db [:hardwallet :pin :puk])) (let [puk (common/vector->string (get-in db [:hardwallet :pin :puk]))
pin (common/vector->string (get-in db [:hardwallet :pin :reset]))
key-uid (get-in db [:hardwallet :application-info :key-uid]) key-uid (get-in db [:hardwallet :application-info :key-uid])
pairing (common/get-pairing db key-uid)] pairing (common/get-pairing db key-uid)]
{:db (assoc-in db [:hardwallet :pin :status] :verifying) {:db (assoc-in db [:hardwallet :pin :status] :verifying)
:hardwallet/unblock-pin :hardwallet/unblock-pin
{:puk puk {:puk puk
:new-pin common/default-pin :new-pin pin
:pairing pairing}}))})) :pairing pairing}}))}))
(def pin-code-length 6) (def pin-code-length 6)
@ -252,12 +270,15 @@
(fx/defn update-pin (fx/defn update-pin
{:events [:hardwallet.ui/pin-numpad-button-pressed]} {:events [:hardwallet.ui/pin-numpad-button-pressed]}
[{:keys [db] :as cofx} number enter-step] [{:keys [db] :as cofx} number enter-step]
(log/debug "update-pin" enter-step)
(let [numbers-entered (count (get-in db [:hardwallet :pin enter-step])) (let [numbers-entered (count (get-in db [:hardwallet :pin enter-step]))
need-update? (if (= enter-step :puk) need-update? (if (= enter-step :puk)
(< numbers-entered puk-code-length) (< numbers-entered puk-code-length)
(< numbers-entered pin-code-length))] (< numbers-entered pin-code-length))]
(fx/merge cofx (fx/merge cofx
{:db (cond-> (assoc-in db [:hardwallet :pin :status] nil) {:db (cond-> (-> db
(assoc-in [:hardwallet :pin :enter-step] enter-step)
(assoc-in [:hardwallet :pin :status] nil))
need-update? (update-in [:hardwallet :pin enter-step] (fnil conj []) number))} need-update? (update-in [:hardwallet :pin enter-step] (fnil conj []) number))}
(when need-update? (when need-update?
(handle-pin-input enter-step))))) (handle-pin-input enter-step)))))
@ -269,6 +290,13 @@
:original [] :original []
:confirmation []})) :confirmation []}))
(defn- pin-reset-error [fx error-label]
(update-in fx [:db :hardwallet :pin] merge {:status :error
:error-label error-label
:enter-step :reset
:reset []
:reset-confirmation []}))
; PIN enter steps: ; PIN enter steps:
; login - PIN is used to login ; login - PIN is used to login
; sign - PIN for transaction sign ; sign - PIN for transaction sign
@ -309,7 +337,7 @@
(and (= enter-step :export-key) (and (= enter-step :export-key)
(= pin-code-length numbers-entered)) (= pin-code-length numbers-entered))
(wallet/hide-pin-sheet) (wallet/verify-pin-with-delay)
(and (= enter-step :sign) (and (= enter-step :sign)
(= pin-code-length numbers-entered)) (= pin-code-length numbers-entered))
@ -328,7 +356,21 @@
(= pin-code-length numbers-entered) (= pin-code-length numbers-entered)
(not= (get-in db [:hardwallet :pin :original]) (not= (get-in db [:hardwallet :pin :original])
(get-in db [:hardwallet :pin :confirmation]))) (get-in db [:hardwallet :pin :confirmation])))
(pin-enter-error :t/pin-mismatch)))) (pin-enter-error :t/pin-mismatch)
(= enter-step :reset)
(proceed-to-pin-reset-confirmation)
(and (= enter-step :reset-confirmation)
(= (get-in db [:hardwallet :pin :reset])
(get-in db [:hardwallet :pin :reset-confirmation])))
(proceed-to-puk-confirmation)
(and (= enter-step :reset-confirmation)
(= pin-code-length numbers-entered)
(not= (get-in db [:hardwallet :pin :reset])
(get-in db [:hardwallet :pin :reset-confirmation])))
(pin-reset-error :t/pin-mismatch))))
(fx/defn set-multiaccount-pairing (fx/defn set-multiaccount-pairing
[cofx _ pairing paired-on] [cofx _ pairing paired-on]

View File

@ -1,6 +1,7 @@
(ns status-im.hardwallet.export-key (ns status-im.hardwallet.export-key
(:require [status-im.utils.fx :as fx] (:require [status-im.utils.fx :as fx]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[status-im.hardwallet.wallet :as wallet]
[status-im.hardwallet.common :as common])) [status-im.hardwallet.common :as common]))
(fx/defn on-export-key-error (fx/defn on-export-key-error
@ -39,5 +40,6 @@
(let [callback-fn (get-in db [:hardwallet :on-export-success])] (let [callback-fn (get-in db [:hardwallet :on-export-success])]
(fx/merge cofx (fx/merge cofx
{:dispatch (callback-fn pubkey)} {:dispatch (callback-fn pubkey)}
(wallet/hide-pin-sheet)
(common/clear-pin) (common/clear-pin)
(common/hide-connection-sheet)))) (common/hide-connection-sheet))))

View File

@ -8,7 +8,8 @@
[status-im.hardwallet.recovery :as recovery] [status-im.hardwallet.recovery :as recovery]
[status-im.hardwallet.onboarding :as onboarding] [status-im.hardwallet.onboarding :as onboarding]
status-im.hardwallet.fx status-im.hardwallet.fx
[status-im.ui.components.bottom-sheet.core :as bottom-sheet])) [status-im.ui.components.bottom-sheet.core :as bottom-sheet]
[status-im.signing.core :as signing.core]))
(fx/defn login-got-it-pressed (fx/defn login-got-it-pressed
{:events [:keycard.login.pin.ui/got-it-pressed {:events [:keycard.login.pin.ui/got-it-pressed
@ -49,16 +50,56 @@
{:db (assoc-in db [:hardwallet :flow] :login)} {:db (assoc-in db [:hardwallet :flow] :login)}
(navigation/navigate-to-cofx :keycard-recovery-pair nil))) (navigation/navigate-to-cofx :keycard-recovery-pair nil)))
(fx/defn frozen-keycard-popup
[{:keys [db]}]
{:db (assoc db :popover/popover {:view :frozen-card})})
(fx/defn reset-pin
{:events [::reset-pin]}
[{:keys [db] :as cofx}]
(fx/merge
cofx
{:db (assoc db :hardwallet/new-account-sheet? false)}
(signing.core/discard)
(fn [{:keys [db]}]
{:db (-> db
(dissoc :popover/popover)
(update-in [:hardwallet :pin] dissoc
:reset :puk)
(update-in [:hardwallet :pin] assoc
:enter-step :reset
:error nil
:status nil))})
(when-not (:multiaccounts/login db)
(navigation/navigate-to-cofx
:profile-stack
{:screen :keycard-pin}))))
(fx/defn dismiss-frozen-keycard-popover
{:events [::frozen-keycard-popover-dismissed]}
[{:keys [db]}]
{:db (-> db
(dissoc :popover/popover)
(update :hardwallet dissoc :setup-step))})
(fx/defn login-with-keycard (fx/defn login-with-keycard
{:events [:hardwallet/login-with-keycard]} {:events [:hardwallet/login-with-keycard]}
[{:keys [db] :as cofx}] [{:keys [db] :as cofx}]
(let [application-info (get-in db [:hardwallet :application-info]) (let [{:keys [:pin-retry-counter :puk-retry-counter]
:as application-info}
(get-in db [:hardwallet :application-info])
key-uid (get-in db [:hardwallet :application-info :key-uid]) key-uid (get-in db [:hardwallet :application-info :key-uid])
multiaccount (get-in db [:multiaccounts/multiaccounts (get-in db [:multiaccounts/login :key-uid])]) multiaccount (get-in db [:multiaccounts/multiaccounts (get-in db [:multiaccounts/login :key-uid])])
multiaccount-key-uid (get multiaccount :key-uid) multiaccount-key-uid (get multiaccount :key-uid)
multiaccount-mismatch? (or (nil? multiaccount) multiaccount-mismatch? (or (nil? multiaccount)
(not= multiaccount-key-uid key-uid)) (not= multiaccount-key-uid key-uid))
pairing (:keycard-pairing multiaccount)] pairing (:keycard-pairing multiaccount)]
(log/debug "[keycard] login-with-keycard"
"empty application info" (empty? application-info)
"no key-uid" (empty? key-uid)
"multiaccount-mismatch?" multiaccount-mismatch?
"no pairing" (empty? pairing))
(cond (cond
(empty? application-info) (empty? application-info)
(fx/merge cofx (fx/merge cofx
@ -80,10 +121,16 @@
(common/hide-connection-sheet) (common/hide-connection-sheet)
(navigation/navigate-to-cofx :keycard-unpaired nil)) (navigation/navigate-to-cofx :keycard-unpaired nil))
(and (zero? pin-retry-counter)
(or (nil? puk-retry-counter)
(= 5 puk-retry-counter)))
nil #_(frozen-keycard-popup cofx)
:else :else
(common/get-keys-from-keycard cofx)))) (common/get-keys-from-keycard cofx))))
(fx/defn proceed-to-login (fx/defn proceed-to-login
{:events [::login-after-reset]}
[cofx] [cofx]
(log/debug "[hardwallet] proceed-to-login") (log/debug "[hardwallet] proceed-to-login")
(common/show-connection-sheet (common/show-connection-sheet

View File

@ -3,6 +3,7 @@
["react-native" :as rn] ["react-native" :as rn]
[status-im.utils.types :as types] [status-im.utils.types :as types]
[status-im.native-module.core :as status] [status-im.native-module.core :as status]
[status-im.ethereum.core :as ethereum]
[status-im.hardwallet.keycard :as keycard])) [status-im.hardwallet.keycard :as keycard]))
(defonce event-emitter (.-DeviceEventEmitter rn)) (defonce event-emitter (.-DeviceEventEmitter rn))
@ -57,9 +58,20 @@
(defn get-application-info (defn get-application-info
[{:keys [pairing on-success on-failure]}] [{:keys [pairing on-success on-failure]}]
;; NOTE: if the card fails to get application info in the middle of the call
;; it doesn't returns a Tar was lost. error properly
;; https://github.com/status-im/react-native-status-keycard/blob/master/android/src/main/java/im/status/ethereum/keycard/SmartCard.java#L235
(.. status-keycard (.. status-keycard
(getApplicationInfo (str pairing)) (getApplicationInfo (str pairing))
(then on-success) (then (fn [response]
(let [info (-> response
(js->clj :keywordize-keys true)
(update :key-uid ethereum/normalized-hex))]
(if (and pairing (nil? (:pin-retry-counter info)))
(on-failure (clj->js {:message "Tag was lost."
:code "android.nfc.TagLostException"}))
(on-success info)))))
(catch on-failure))) (catch on-failure)))
(defn install-applet [{:keys [on-success on-failure]}] (defn install-applet [{:keys [on-success on-failure]}]

View File

@ -197,17 +197,21 @@
{:events [:hardwallet.callback/on-sign-error]} {:events [:hardwallet.callback/on-sign-error]}
[{:keys [db] :as cofx} error] [{:keys [db] :as cofx} error]
(log/debug "[hardwallet] sign error: " error) (log/debug "[hardwallet] sign error: " error)
(let [tag-was-lost? (common/tag-lost? (:error error))] (let [tag-was-lost? (common/tag-lost? (:error error))
pin-retries (get-in db [:hardwallet :application-info :pin-retry-counter])]
(when-not tag-was-lost? (when-not tag-was-lost?
(if (re-matches common/pin-mismatch-error (:error error)) (if (re-matches common/pin-mismatch-error (:error error))
(fx/merge cofx (fx/merge cofx
{:db (-> db {:db (-> db
(assoc-in [:hardwallet :application-info :pin-retry-counter] (dec pin-retries))
(update-in [:hardwallet :pin] merge {:status :error (update-in [:hardwallet :pin] merge {:status :error
:sign [] :sign []
:error-label :t/pin-mismatch}) :error-label :t/pin-mismatch})
(assoc-in [:signing/sign :keycard-step] :pin))} (assoc-in [:signing/sign :keycard-step] :pin))}
(common/hide-connection-sheet) (common/hide-connection-sheet)
(common/get-application-info (common/get-pairing db) nil)) (common/get-application-info (common/get-pairing db) nil)
(when (zero? (dec pin-retries))
(common/frozen-keycard-popup)))
(fx/merge cofx (fx/merge cofx
(common/hide-connection-sheet) (common/hide-connection-sheet)
(common/show-wrong-keycard-alert true)))))) (common/show-wrong-keycard-alert true))))))

View File

@ -31,6 +31,8 @@
:paired? true :paired? true
:has-master-key? true :has-master-key? true
:initialized? true :initialized? true
:pin-retry-counter 3
:puk-retry-counter 5
:key-uid (get-in @re-frame.db/app-db [:multiaccounts/login :key-uid])}) :key-uid (get-in @re-frame.db/app-db [:multiaccounts/login :key-uid])})
(connect-card)) (connect-card))
@ -84,7 +86,7 @@
(defn install-cash-applet [_]) (defn install-cash-applet [_])
(def kk1-password "000000") (def kk1-password "000000")
(def default-puk "000000000000")
(defn init-card [{:keys [pin on-success]}] (defn init-card [{:keys [pin on-success]}]
(swap! state assoc :application-info (swap! state assoc :application-info
{:free-pairing-slots 5 {:free-pairing-slots 5
@ -98,7 +100,7 @@
(swap! state assoc :pin pin) (swap! state assoc :pin pin)
(later (later
#(on-success {:password kk1-password #(on-success {:password kk1-password
:puk "000000000000" :puk default-puk
:pin pin}))) :pin pin})))
(defn install-applet-and-init-card [_]) (defn install-applet-and-init-card [_])
@ -141,12 +143,34 @@
password password
#(on-success response))))) #(on-success response)))))
(defn unblock-pin [_]) (defn unblock-pin
[{:keys [puk on-success on-failure]}]
(if (= puk default-puk)
(do
(swap! state update :application-info assoc
:pin-retry-counter 3
:puk-retry-counter 5)
(later #(on-success true)))
(do
(swap! state update-in
[:application-info :puk-retry-counter]
(fnil dec 5))
(later
#(on-failure
#js {:code "EUNSPECIFIED"
:message "Unexpected error SW, 0x63C2"})))))
(defn verify-pin [{:keys [pin pairing on-success]}] (defn verify-pin [{:keys [pin pairing on-success on-failure]}]
(when (and (= pairing kk1-pair) (if (and (= pairing kk1-pair)
(= pin (get @state :pin))) (= pin (get @state :pin)))
(later #(on-success 3)))) (later #(on-success 3))
(do
(swap! state update-in
[:application-info :pin-retry-counter]
(fnil dec 3))
(later #(on-failure
#js {:code "EUNSPECIFIED"
:message "Unexpected error SW, 0x63C2"})))))
(defn change-pin [args] (defn change-pin [args]
(log/warn "change-pin not implemented" args)) (log/warn "change-pin not implemented" args))
@ -199,16 +223,42 @@
(on-success publicKey))))))))))))))) (on-success publicKey)))))))))))))))
(defn unpair-and-delete [_]) (defn unpair-and-delete [_])
(defn get-keys [{:keys [on-success pin]}]
;; It is a bit complicated to verify password before we have multiaccs main
;; wallet address, so we just define a set of "allowed" pins
(def allowed-pins
#{"121212" "111111" "222222" "123123"})
(defn get-keys [{:keys [on-success on-failure pin]}]
(if (contains? allowed-pins pin)
(do
(swap! state assoc :pin pin) (swap! state assoc :pin pin)
;;TODO(rasom): verify password before callback
(later (later
#(on-success #(on-success
{:key-uid (get-in @state [:application-info :key-uid]) {:key-uid (get-in @state [:application-info :key-uid])
:encryption-public-key (ethereum/sha3 pin)}))) :encryption-public-key (ethereum/sha3 pin)})))
(do
(log/debug "Incorrect PIN" pin)
(swap! state update-in
[:application-info :pin-retry-counter]
(fnil dec 3))
(later
#(on-failure
#js {:code "EUNSPECIFIED"
:message "Unexpected error SW, 0x63C2"})))))
(defn sign [{:keys [on-success]}] (defn sign [{:keys [pin on-success on-failure]}]
(on-success "123")) (if (= pin (get @state :pin))
(later
#(on-success "123"))
(do
(swap! state update-in
[:application-info :pin-retry-counter]
(fnil dec 3))
(later
#(on-failure
#js {:code "EUNSPECIFIED"
:message "Unexpected error SW, 0x63C2"})))))
(defn sign-typed-data [args] (defn sign-typed-data [args]
(log/warn "sign-typed-data not implemented" args)) (log/warn "sign-typed-data not implemented" args))

View File

@ -1,11 +1,9 @@
(ns status-im.hardwallet.wallet (ns status-im.hardwallet.wallet
(:require [status-im.ethereum.core :as ethereum] (:require [status-im.ethereum.core :as ethereum]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[status-im.ui.screens.wallet.add-new.views :as add-new.views]
[status-im.hardwallet.common :as common] [status-im.hardwallet.common :as common]
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.ethereum.eip55 :as eip55] [status-im.ethereum.eip55 :as eip55]
[status-im.ui.components.bottom-sheet.core :as bottom-sheet]
[status-im.utils.hex :as utils.hex])) [status-im.utils.hex :as utils.hex]))
(fx/defn show-pin-sheet (fx/defn show-pin-sheet
@ -15,21 +13,20 @@
cofx cofx
{:db (-> db {:db (-> db
(assoc-in [:hardwallet :pin :enter-step] :export-key) (assoc-in [:hardwallet :pin :enter-step] :export-key)
(update-in [:hardwallet :pin] dissoc :export-key))} (update-in [:hardwallet :pin] dissoc :export-key)
(bottom-sheet/show-bottom-sheet (assoc :hardwallet/new-account-sheet? true))}))
{:view {:content add-new.views/pin
:height 256}})))
(fx/defn hide-pin-sheet (fx/defn verify-pin-with-delay
{:events [:hardwallet/hide-new-account-pin-sheet]}
[cofx] [cofx]
(fx/merge
cofx
{:utils/dispatch-later {:utils/dispatch-later
;; We need to give previous sheet some time to be fully hidden ;; We need to give previous sheet some time to be fully hidden
[{:ms 200 [{:ms 200
:dispatch [:wallet.accounts/verify-pin]}]} :dispatch [:wallet.accounts/verify-pin]}]})
(bottom-sheet/hide-bottom-sheet)))
(fx/defn hide-pin-sheet
{:events [:hardwallet/new-account-pin-sheet-hide]}
[{:keys [db]}]
{:db (assoc db :hardwallet/new-account-sheet? false)})
(fx/defn generate-new-keycard-account (fx/defn generate-new-keycard-account
{:events [:wallet.accounts/generate-new-keycard-account]} {:events [:wallet.accounts/generate-new-keycard-account]}
@ -59,6 +56,6 @@
(common/verify-pin (common/verify-pin
cofx cofx
{:pin-step :export-key {:pin-step :export-key
:on-card-connected :wallet.accounts/generate-new-keycard-account :on-card-connected :wallet.accounts/verify-pin
:on-success :wallet.accounts/generate-new-keycard-account :on-success :wallet.accounts/generate-new-keycard-account
:on-failure :hardwallet/new-account-pin-sheet})) :on-failure :hardwallet/new-account-pin-sheet}))

View File

@ -716,7 +716,6 @@
:pin-changed :pin-changed
:pin-code :pin-code
:pin-mismatch :pin-mismatch
:pin-retries-left
:preview-privacy :preview-privacy
:privacy :privacy
:privacy-and-security :privacy-and-security

View File

@ -7,6 +7,7 @@
[status-im.ethereum.core :as ethereum] [status-im.ethereum.core :as ethereum]
[status-im.ethereum.eip55 :as eip55] [status-im.ethereum.eip55 :as eip55]
[status-im.ethereum.json-rpc :as json-rpc] [status-im.ethereum.json-rpc :as json-rpc]
[status-im.hardwallet.common :as hardwallet.common]
[status-im.fleet.core :as fleet] [status-im.fleet.core :as fleet]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.multiaccounts.biometric.core :as biometric] [status-im.multiaccounts.biometric.core :as biometric]
@ -367,15 +368,17 @@
(log/debug "[login] get-auth-method-success" (log/debug "[login] get-auth-method-success"
"auth-method" auth-method "auth-method" auth-method
"keycard-multiacc?" keycard-multiaccount?) "keycard-multiacc?" keycard-multiaccount?)
(fx/merge cofx (fx/merge
cofx
{:db (assoc db :auth-method auth-method)} {:db (assoc db :auth-method auth-method)}
#(cond #(cond
(= auth-method (= auth-method keychain/auth-method-biometric)
keychain/auth-method-biometric)
(biometric/biometric-auth %) (biometric/biometric-auth %)
(= auth-method (= auth-method keychain/auth-method-password)
keychain/auth-method-password) (get-credentials % key-uid)
(get-credentials % key-uid)) (and keycard-multiaccount?
(get-in db [:hardwallet :card-connected?]))
(hardwallet.common/get-application-info % nil nil))
(open-login-callback nil)))) (open-login-callback nil))))
(fx/defn biometric-auth-done (fx/defn biometric-auth-done

View File

@ -60,6 +60,7 @@
(navigate-to-cofx cofx view-id params)) (navigate-to-cofx cofx view-id params))
(fx/defn navigate-replace (fx/defn navigate-replace
{:events [:navigate-replace]}
[{:keys [db]} go-to-view-id screen-params] [{:keys [db]} go-to-view-id screen-params]
(let [db (cond-> (assoc db :view-id go-to-view-id) (let [db (cond-> (assoc db :view-id go-to-view-id)
(seq screen-params) (seq screen-params)

View File

@ -188,6 +188,9 @@
(reg-root-key-sub ::message-lists :message-lists) (reg-root-key-sub ::message-lists :message-lists)
(reg-root-key-sub ::pagination-info :pagination-info) (reg-root-key-sub ::pagination-info :pagination-info)
;; keycard
(reg-root-key-sub :hardwallet/new-account-sheet? :hardwallet/new-account-sheet?)
;;GENERAL ============================================================================================================== ;;GENERAL ==============================================================================================================
(re-frame/reg-sub (re-frame/reg-sub

View File

@ -49,7 +49,28 @@
update? (atom nil) update? (atom nil)
current-obj (reagent/atom nil)] current-obj (reagent/atom nil)]
(reagent/create-class (reagent/create-class
{:component-will-update (fn [_ [_ obj _ _]] {:component-will-mount (fn [args]
(let [[_ obj _ _] (.-argv (.-props args))]
(when @clear-timeout (js/clearTimeout @clear-timeout))
(when (or (not= obj @current-obj) @update?)
(cond
@update?
(do (reset! update? false)
(show-panel-anim bottom-anim-value alpha-value))
(and @current-obj obj)
(do (reset! update? true)
(js/setTimeout #(reset! current-obj obj) 600)
(hide-panel-anim bottom-anim-value alpha-value (- window-height)))
obj
(do (reset! current-obj obj)
(show-panel-anim bottom-anim-value alpha-value))
:else
(do (reset! clear-timeout (js/setTimeout #(reset! current-obj nil) 600))
(hide-panel-anim bottom-anim-value alpha-value (- window-height)))))))
:component-will-update (fn [_ [_ obj _ _]]
(when @clear-timeout (js/clearTimeout @clear-timeout)) (when @clear-timeout (js/clearTimeout @clear-timeout))
(when (or (not= obj @current-obj) @update?) (when (or (not= obj @current-obj) @update?)
(cond (cond

View File

@ -0,0 +1,44 @@
(ns status-im.ui.screens.hardwallet.frozen-card.view
(:require-macros [status-im.utils.views :as views])
(:require [status-im.ui.components.react :as react]
[status-im.ui.components.colors :as colors]
[status-im.ui.components.icons.vector-icons :as icons]
[status-im.ui.components.common.common :as components.common]
[status-im.hardwallet.login :as login]
[status-im.i18n :as i18n]
[re-frame.core :as re-frame]))
(views/defview frozen-card
[{:keys [show-dismiss-button?]
:or {show-dismiss-button? true}}]
[react/view {:style (when-not show-dismiss-button?
{:flex 1})}
[react/view {:margin-top 24
:margin-horizontal 24
:align-items :center}
[react/view {:background-color colors/blue-light
:width 32 :height 32
:border-radius 16
:align-items :center
:justify-content :center}
[icons/icon :main-icons/warning {:color colors/blue}]]
[react/text {:style {:typography :title-bold
:margin-top 16
:margin-bottom 8}}
(i18n/label :t/keycard-is-frozen-title)]
[react/text {:style {:color colors/gray
:text-align :center}}
(i18n/label :t/keycard-is-frozen-details)]]
[react/view {:margin-bottom 24
:margin-horizontal 24
:align-items :center}
[components.common/button
{:on-press #(re-frame/dispatch [::login/reset-pin])
:button-style {:margin-top 24}
:label (i18n/label :t/keycard-is-frozen-reset)}]
(when show-dismiss-button?
[components.common/button
{:on-press #(re-frame/dispatch [::login/frozen-keycard-popover-dismissed])
:button-style {:margin-top 24}
:background? false
:label (i18n/label :t/dismiss)}])]])

View File

@ -1,22 +1,41 @@
(ns status-im.ui.screens.hardwallet.pin.styles (ns status-im.ui.screens.hardwallet.pin.styles
(:require [status-im.ui.components.colors :as colors] (:require [status-im.ui.components.colors :as colors]))
[status-im.utils.styles :as styles]))
(styles/def pin-container (def pin-container
{:flex 1 {:flex 1
:flex-direction :column :flex-direction :column
:justify-content :space-between}) :justify-content :space-between})
(defn error-container [small-screen?] (defn info-container [small-screen?]
{:height (when small-screen? 18) {:height 44
:margin-top (if small-screen? 14 10) :width "100%"
:margin-bottom (if small-screen? 10 0)}) :justify-content :center
:margin-top (if small-screen? 14 10)})
(defn error-container [y-translation opacity]
{:left 0
:right 0
:align-items :center
:position :absolute
:transform [{:translateY y-translation}]
:opacity opacity
:justify-content :center})
(defn error-text [small-screen?] (defn error-text [small-screen?]
{:color colors/red {:position :absolute
:color colors/red
:font-size (if small-screen? 12 15) :font-size (if small-screen? 12 15)
:text-align :center}) :text-align :center})
(defn retry-container [y-translation opacity]
{:left 0
:right 0
:align-items :center
:position :absolute
:transform [{:translateY y-translation}]
:opacity opacity
:justify-content :center})
(defn center-container [title] (defn center-container [title]
{:flex-direction :column {:flex-direction :column
:align-items :center :align-items :center
@ -34,16 +53,18 @@
(def pin-indicator-container (def pin-indicator-container
{:flex-direction :row {:flex-direction :row
:justify-content :space-between :justify-content :space-between
:margin-top 16}) :align-items :center
:height 22
:margin-top 5})
(def pin-indicator-group-container (def pin-indicator-group-container
{:flex-direction :row {:flex-direction :row
:justify-content :space-between}) :justify-content :space-between})
(defn pin-indicator [pressed? status] (defn pin-indicator [pressed? error?]
{:width 8 {:width 8
:height 8 :height 8
:background-color (if (= status :error) :background-color (if error?
colors/red colors/red
(if pressed? (if pressed?
colors/blue colors/blue
@ -51,6 +72,15 @@
:border-radius 50 :border-radius 50
:margin-horizontal 5}) :margin-horizontal 5})
(defn puk-indicator [error?]
{:width 8
:height 8
:background-color (if error?
colors/red
colors/black-transparent)
:border-radius 50
:margin-horizontal 5})
(def waiting-indicator-container (def waiting-indicator-container
{:margin-top 26}) {:margin-top 26})

View File

@ -19,7 +19,7 @@
(re-frame/reg-sub (re-frame/reg-sub
:hardwallet/pin-enter-step :hardwallet/pin-enter-step
(fn [db] (fn [db]
(get-in db [:hardwallet :pin :enter-step] :original))) (get-in db [:hardwallet :pin :enter-step])))
(re-frame/reg-sub (re-frame/reg-sub
:hardwallet/pin-operation :hardwallet/pin-operation
@ -47,3 +47,10 @@
:hardwallet/pin-error-label :hardwallet/pin-error-label
(fn [db] (fn [db]
(get-in db [:hardwallet :pin :error-label]))) (get-in db [:hardwallet :pin :error-label])))
(re-frame/reg-sub
:hardwallet/frozen-card?
(fn [db]
(let [{:keys [pin-retry-counter]}
(get-in db [:hardwallet :application-info])]
(zero? pin-retry-counter))))

View File

@ -1,6 +1,8 @@
(ns status-im.ui.screens.hardwallet.pin.views (ns status-im.ui.screens.hardwallet.pin.views
(:require-macros [status-im.utils.views :refer [defview letsubs]]) (:require-macros [status-im.utils.views :refer [defview letsubs]])
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as re-frame]
[reagent.core :as reagent]
[status-im.ui.components.animation :as animation]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.ui.components.colors :as colors] [status-im.ui.components.colors :as colors]
[status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.icons.vector-icons :as vector-icons]
@ -8,9 +10,11 @@
[status-im.ui.screens.hardwallet.pin.styles :as styles] [status-im.ui.screens.hardwallet.pin.styles :as styles]
[status-im.ui.components.checkbox.view :as checkbox] [status-im.ui.components.checkbox.view :as checkbox]
[status-im.utils.platform :as platform] [status-im.utils.platform :as platform]
[status-im.utils.utils :as utils]
[status-im.ui.components.topbar :as topbar])) [status-im.ui.components.topbar :as topbar]))
(def default-pin-retries-number 3) (def default-pin-retries-number 3)
(def default-puk-retries-number 5)
(defn numpad-button [n step enabled? small-screen?] (defn numpad-button [n step enabled? small-screen?]
[react/touchable-highlight [react/touchable-highlight
@ -40,32 +44,36 @@
[react/view (styles/numpad-delete-button small-screen?) [react/view (styles/numpad-delete-button small-screen?)
[vector-icons/icon :main-icons/backspace {:color colors/blue}]]]]]) [vector-icons/icon :main-icons/backspace {:color colors/blue}]]]]])
(defn pin-indicator [pressed? status] (defn pin-indicators [pin error?]
[react/view (styles/pin-indicator pressed? status)]) [react/view styles/pin-indicator-container
(defn pin-indicators [pin status style]
[react/view (merge styles/pin-indicator-container style)
(map-indexed
(fn [i group]
^{:key i}
[react/view styles/pin-indicator-group-container
group])
(partition 3
(map-indexed (map-indexed
(fn [i n] (fn [i n]
^{:key i} (let [pressed? (number? n)]
[pin-indicator (number? n) status]) ^{:key i} [react/view (styles/pin-indicator pressed? error?)]))
(concat pin (concat pin (repeat (- 6 (count pin)) nil)))])
(repeat (- 6 (count pin))
nil)))))])
(defn puk-indicators [puk status] (defn puk-indicators [puk error?]
[react/view {:margin-top 28} [react/view {:margin-top 28
:flex-direction :row
:justify-content :space-between}
(map-indexed (map-indexed
(fn [i puk-group] (fn [i puk-group]
^{:key i} ^{:key i}
[pin-indicators puk-group status {:margin-top 8}]) [react/view (merge styles/pin-indicator-container
(partition 6 {:margin-top 8
:margin 12})
(map-indexed
(fn [j n]
(if (number? n)
^{:key j} [react/text {:style {:font-size 20
:width 18
:color (if error?
colors/red
colors/black)}}
n]
^{:key j} [react/view (styles/puk-indicator error?)]))
puk-group)])
(partition 4
(concat puk (concat puk
(repeat (- 12 (count puk)) (repeat (- 12 (count puk))
nil))))]) nil))))])
@ -82,10 +90,98 @@
:on-value-change #(re-frame/dispatch [:multiaccounts/save-password %])}] :on-value-change #(re-frame/dispatch [:multiaccounts/save-password %])}]
[react/text (i18n/label :t/hardwallet-dont-ask-card)]]))) [react/text (i18n/label :t/hardwallet-dont-ask-card)]])))
(defn bezier-easing []
(.bezier ^js animation/easing 0.77, 0.000, 0.175, 1))
(defn animate-info-in
"animation that makes the error message appear for a few seconds, then
replaces it with the number of attempts left"
[error-y-translation error-opacity retries-y-translation retries-opacity]
(animation/start
(animation/anim-sequence
[(animation/parallel
[(animation/timing error-opacity
{:toValue 1
:easing (bezier-easing)
:duration 400
:useNativeDriver true})
(animation/timing error-y-translation
{:toValue 0
:easing (bezier-easing)
:duration 400
:useNativeDriver true})])
(animation/anim-delay 2200)
(animation/parallel
[(animation/timing error-opacity
{:toValue 0
:easing (bezier-easing)
:duration 400
:useNativeDriver true})
(animation/timing error-y-translation
{:toValue 8
:easing (bezier-easing)
:duration 400
:useNativeDriver true})
(animation/timing retries-opacity
{:toValue 1
:easing (bezier-easing)
:duration 400
:useNativeDriver true})
(animation/timing retries-y-translation
{:toValue 0
:easing (bezier-easing)
:duration 400
:useNativeDriver true})])])))
(defn animate-info-out
[retries-y-translation retries-opacity]
(animation/start
(animation/parallel
[(animation/timing retries-opacity
{:toValue 0
:easing (bezier-easing)
:duration 400
:useNativeDriver true})
(animation/timing retries-y-translation
{:toValue -8
:easing (bezier-easing)
:duration 400
:useNativeDriver true})])))
(defn pin-view (defn pin-view
[{:keys [pin title-label description-label step status error-label [{:keys [retry-counter]}]
(let [error-y-translation (animation/create-value -8)
error-opacity (animation/create-value 0)
retries-y-translation (animation/create-value (if retry-counter 8 0))
retries-opacity (animation/create-value (if retry-counter 0 1))
!error? (reagent/atom false)]
(reagent/create-class
{:component-did-update
(fn [this [_ previous-props]]
(let [[_ props] (.-argv (.-props ^js this))
previous-status (:status previous-props)
new-status (:status props)]
(case new-status
:error (when (or (nil? previous-status)
(= :verifying previous-status))
(utils/vibrate)
(reset! !error? true)
(animate-info-in error-y-translation
error-opacity
retries-y-translation
retries-opacity)
(js/setTimeout (fn [] (reset! !error? false)) 3000))
:verifying (do
(animation/set-value error-y-translation -8)
(animate-info-out retries-y-translation
retries-opacity))
nil)))
:reagent-render
(fn [{:keys [pin title-label description-label step error-label status
retry-counter small-screen? save-password-checkbox?]}] retry-counter small-screen? save-password-checkbox?]}]
(let [enabled? (not= status :verifying)] (let [enabled? (and (not= status :verifying)
(not @!error?))
puk? (= step :puk)]
[react/scroll-view [react/scroll-view
[react/view styles/pin-container [react/view styles/pin-container
[react/view (styles/center-container title-label) [react/view (styles/center-container title-label)
@ -96,24 +192,40 @@
[react/text {:style styles/create-pin-text [react/text {:style styles/create-pin-text
:number-of-lines 2} :number-of-lines 2}
(i18n/label description-label)]) (i18n/label description-label)])
[react/view {:flex 1}
(case status
:verifying [react/view styles/waiting-indicator-container
[react/activity-indicator {:animating true
:size :small}]]
:error [react/view (styles/error-container small-screen?)
[react/text {:style (styles/error-text small-screen?)}
(i18n/label error-label)]]
(when (and retry-counter (< retry-counter default-pin-retries-number))
[react/view {:margin-top (if (= step :puk) 24 8)}
[react/text {:style {:text-align :center}}
(i18n/label :t/pin-retries-left {:number retry-counter})]]))]
(when save-password-checkbox? (when save-password-checkbox?
[save-password]) [save-password])
(if (= step :puk) [react/view {:style (styles/info-container small-screen?)}
[puk-indicators pin status] (when error-label
[pin-indicators pin status nil]) [react/animated-view {:style (styles/error-container error-y-translation error-opacity)}
[numpad step enabled? small-screen?]]]])) [react/text {:style (styles/error-text small-screen?)}
(i18n/label error-label)]])
[react/animated-view {:style (styles/retry-container retries-y-translation retries-opacity)}
(cond
(and retry-counter (= retry-counter 1))
[react/nested-text {:style {:text-align :center
:color colors/gray}}
(i18n/label (if puk?
:t/pin-one-attempt-blocked-before
:t/pin-one-attempt-frozen-before))
[{:style {:color colors/black
:font-weight "700"}}
(i18n/label :t/pin-one-attempt)]
(i18n/label (if puk?
:t/pin-one-attempt-blocked-after
:t/pin-one-attempt-frozen-after))]
(and retry-counter (< retry-counter (if puk?
default-puk-retries-number
default-pin-retries-number)))
[react/text {:style {:text-align :center
:color colors/gray}}
(i18n/label :t/pin-retries-left {:number retry-counter})]
:else
nil)]]
(if puk?
[puk-indicators pin @!error?]
[pin-indicators pin @!error?])
[numpad step enabled? small-screen?]]]]))})))
(def pin-retries 3) (def pin-retries 3)
(def puk-retries 5) (def puk-retries 5)
@ -125,6 +237,11 @@
pin-retry-counter [:hardwallet/pin-retry-counter] pin-retry-counter [:hardwallet/pin-retry-counter]
puk-retry-counter [:hardwallet/puk-retry-counter] puk-retry-counter [:hardwallet/puk-retry-counter]
error-label [:hardwallet/pin-error-label]] error-label [:hardwallet/pin-error-label]]
(let [;; TODO(rasom): retarded hack to prevent state mess on opening pin
;; sheet on another tab and returning back to this screen. Should be
;; properly rewritten so that different instances of pin-view do not
;; mess with state unrelated to them.
step (or step :current)]
[react/view {:flex 1 [react/view {:flex 1
:background-color colors/white} :background-color colors/white}
[topbar/topbar {}] [topbar/topbar {}]
@ -153,4 +270,4 @@
:t/new-pin-description) :t/new-pin-description)
:step step :step step
:status status :status status
:error-label error-label}])])) :error-label error-label}])])))

View File

@ -8,7 +8,9 @@
[status-im.ui.components.colors :as colors] [status-im.ui.components.colors :as colors]
[status-im.ui.components.common.common :as components.common] [status-im.ui.components.common.common :as components.common]
[status-im.ui.components.topbar :as topbar] [status-im.ui.components.topbar :as topbar]
[status-im.constants :as constants])) [status-im.constants :as constants]
[status-im.ui.screens.keycard.views :as keycard.views]
[status-im.hardwallet.common :as hardwallet.common]))
(defn- action-row [{:keys [icon label on-press color-theme]}] (defn- action-row [{:keys [icon label on-press color-theme]}]
[react/touchable-highlight [react/touchable-highlight
@ -131,3 +133,10 @@
:color-theme :red :color-theme :red
:label :t/reset-card :label :t/reset-card
:on-press #(re-frame/dispatch [:keycard-settings.ui/reset-card-pressed])}]])]])) :on-press #(re-frame/dispatch [:keycard-settings.ui/reset-card-pressed])}]])]]))
(defn reset-pin []
[keycard.views/login-pin
{:back-button-handler
::hardwallet.common/navigate-to-keycard-settings
:hide-login-actions? true
:default-enter-step :reset}])

View File

@ -15,8 +15,9 @@
[status-im.ui.screens.keycard.styles :as styles] [status-im.ui.screens.keycard.styles :as styles]
[status-im.utils.core :as utils.core] [status-im.utils.core :as utils.core]
[status-im.utils.gfycat.core :as gfy] [status-im.utils.gfycat.core :as gfy]
[status-im.utils.identicon :as identicon] [status-im.constants :as constants]
[status-im.constants :as constants]) [status-im.ui.screens.keycard.views :as keycard.views]
[status-im.utils.identicon :as identicon])
(:require-macros [status-im.utils.views :refer [defview letsubs]])) (:require-macros [status-im.utils.views :refer [defview letsubs]]))
(defn intro [] (defn intro []
@ -83,9 +84,17 @@
{:handler #(re-frame/dispatch [::hardwallet.recovery/cancel-pressed]) {:handler #(re-frame/dispatch [::hardwallet.recovery/cancel-pressed])
:style {:padding-left 21}} :style {:padding-left 21}}
(i18n/label :t/cancel)] (i18n/label :t/cancel)]
(when-not (#{:frozen-card :blocked-card} status)
[react/text {:style {:color colors/gray}} [react/text {:style {:color colors/gray}}
(i18n/label :t/step-i-of-n {:number 2 (i18n/label :t/step-i-of-n {:number 2
:step 2})]] :step 2})])]
(case status
:frozen-card
[keycard.views/frozen-card]
:blocked-card
[keycard.views/blocked-card]
[react/view {:flex 1 [react/view {:flex 1
:flex-direction :column :flex-direction :column
:justify-content :space-between :justify-content :space-between
@ -102,7 +111,7 @@
:small-screen? small-screen? :small-screen? small-screen?
:status status :status status
:error-label error-label :error-label error-label
:step :import-multiaccount}]]])) :step :import-multiaccount}]])]))
(defview pair [] (defview pair []
(letsubs [pair-code [:hardwallet-pair-code] (letsubs [pair-code [:hardwallet-pair-code]

View File

@ -12,7 +12,10 @@
[status-im.ui.screens.chat.photos :as photos] [status-im.ui.screens.chat.photos :as photos]
[status-im.ui.screens.hardwallet.pin.views :as pin.views] [status-im.ui.screens.hardwallet.pin.views :as pin.views]
[status-im.ui.screens.keycard.styles :as styles] [status-im.ui.screens.keycard.styles :as styles]
[status-im.constants :as constants]) [status-im.constants :as constants]
[status-im.ui.components.button :as button]
[status-im.hardwallet.login :as hardwallet.login]
[status-im.ui.screens.hardwallet.frozen-card.view :as frozen-card.view])
(:require-macros [status-im.utils.views :refer [defview letsubs]])) (:require-macros [status-im.utils.views :refer [defview letsubs]]))
;; NOTE(Ferossgp): Seems like it should be in popover ;; NOTE(Ferossgp): Seems like it should be in popover
@ -208,22 +211,132 @@
[photos/photo (multiaccounts/displayed-photo account) [photos/photo (multiaccounts/displayed-photo account)
{:size (if small-screen? 45 61)}])})) {:size (if small-screen? 45 61)}])}))
(defview login-pin [] (defn access-is-reset [{:keys [hide-login-actions?]}]
[react/view
{:style {:flex 1
:align-items :center}}
[react/view
{:style {:flex 1
:align-items :center
:justify-content :center}}
[react/view
{:style
{:background-color colors/green-transparent-10
:margin-bottom 32
:width 40
:height 40
:align-items :center
:justify-content :center
:border-radius 20}}
[vector-icons/icon
:main-icons/check
{:color colors/green}]]
[react/text {:style {:typography :header}}
(i18n/label :t/keycard-access-reset)]
[react/text (i18n/label :t/keycard-can-use-with-new-passcode)]]
(when-not hide-login-actions?
[react/view
{:style {:width 160
:margin-bottom 15}}
[button/button
{:type :main
:style {:align-self :stretch}
:container-style {:height 52}
:label (i18n/label :t/open)
:on-press #(re-frame/dispatch
[::hardwallet.login/login-after-reset])}]])])
(defn frozen-card []
[frozen-card.view/frozen-card
{:show-dismiss-button? false}])
(defn blocked-card []
[react/view {:style {:flex 1
:align-items :center}}
[react/view {:margin-top 24
:margin-horizontal 24
:align-items :center}
[react/view {:background-color colors/red-transparent-10
:width 32
:height 32
:border-radius 16
:align-items :center
:justify-content :center}
[vector-icons/icon
:main-icons/cancel
{:color colors/red
:width 20
:height 20}]]
[react/text {:style {:typography :title-bold
:margin-top 16
:margin-bottom 8}}
(i18n/label :t/keycard-is-blocked-title)]
[react/text {:style {:color colors/gray
:text-align :center}}
(i18n/label :t/keycard-is-blocked-details)]
[react/text "\n"]
[react/nested-text
{:style {:color colors/gray
:text-align :center}}
(i18n/label :t/keycard-is-blocked-instructions)
[{} " "]
[{:style {:color colors/blue}
:on-press #(.openURL ^js react/linking "https://status.im/faq/#keycard")}
(i18n/label :t/learn-more)]]]])
(defn- step-view [step]
[react/view
{:style {:flex 1
:justify-content :center
:align-items :center}}
[react/text {:style {:typography :title-bold :text-align :center}}
(i18n/label :t/keycard-reset-passcode)]
[react/text {:style {:color colors/gray}}
(i18n/label :t/keycard-enter-new-passcode {:step step})]])
(defview login-pin [{:keys [back-button-handler
hide-login-actions?
default-enter-step]
:or {default-enter-step :login}}]
(letsubs [pin [:hardwallet/pin] (letsubs [pin [:hardwallet/pin]
enter-step [:hardwallet/pin-enter-step] enter-step [:hardwallet/pin-enter-step]
status [:hardwallet/pin-status] status [:hardwallet/pin-status]
error-label [:hardwallet/pin-error-label] error-label [:hardwallet/pin-error-label]
{:keys [name] :as account} [:multiaccounts/login] login-multiaccount [:multiaccounts/login]
multiaccount [:multiaccount]
small-screen? [:dimensions/small-screen?] small-screen? [:dimensions/small-screen?]
retry-counter [:hardwallet/retry-counter]] retry-counter [:hardwallet/retry-counter]]
(let [{:keys [name] :as account} (or login-multiaccount multiaccount)
;; TODO(rasom): this hack fixes state mess when more then two
;; pin-view instances are used at the same time. Should be properly
;; refactored instead
enter-step (or enter-step default-enter-step)]
[react/view styles/container [react/view styles/container
[topbar/topbar [topbar/topbar
{:accessories [{:icon :main-icons/more {:accessories [(when-not hide-login-actions?
:handler #(re-frame/dispatch [:keycard.login.pin.ui/more-icon-pressed])}] {:icon :main-icons/more
:handler #(re-frame/dispatch [:keycard.login.pin.ui/more-icon-pressed])})]
:content (cond
(= :reset enter-step)
[step-view 1]
(= :reset-confirmation enter-step)
[step-view 2]
(and (= :puk enter-step)
(not= :blocked-card status))
[react/view
{:style {:flex 1
:justify-content :center
:align-items :center}}
[react/text {:style {:color colors/gray}}
(i18n/label :t/enter-puk-code)]])
:navigation :navigation
{:icon :main-icons/back {:icon :main-icons/back
:accessibility-label :back-button :accessibility-label :back-button
:handler #(re-frame/dispatch [:keycard.login.pin.ui/cancel-pressed])}}] :handler #(re-frame/dispatch
[(or back-button-handler
:keycard.login.pin.ui/cancel-pressed)])}}]
[react/view {:flex 1 [react/view {:flex 1
:flex-direction :column :flex-direction :column
:justify-content :space-between :justify-content :space-between
@ -263,6 +376,18 @@
:number-of-lines 1 :number-of-lines 1
:ellipsize-mode :middle} :ellipsize-mode :middle}
name]]] name]]]
(cond
(= :after-unblocking status)
[access-is-reset
{:hide-login-actions? hide-login-actions?}]
(= :frozen-card status)
[frozen-card]
(= :blocked-card status)
[blocked-card]
:else
[pin.views/pin-view [pin.views/pin-view
{:pin pin {:pin pin
:retry-counter retry-counter :retry-counter retry-counter
@ -270,12 +395,15 @@
:status status :status status
:error-label error-label :error-label error-label
:step enter-step :step enter-step
:save-password-checkbox? true}] :save-password-checkbox? (not (contains?
#{:reset :reset-confirmation :puk}
enter-step))}])
(when-not hide-login-actions?
[react/view {:margin-bottom (if small-screen? 25 32)} [react/view {:margin-bottom (if small-screen? 25 32)}
[react/touchable-highlight [react/touchable-highlight
{:on-press #(re-frame/dispatch [:multiaccounts.recover.ui/recover-multiaccount-button-pressed])} {:on-press #(re-frame/dispatch [:multiaccounts.recover.ui/recover-multiaccount-button-pressed])}
[react/text {:style {:color colors/blue}} [react/text {:style {:color colors/blue}}
(i18n/label :t/recover-key)]]]]])) (i18n/label :t/recover-key)]]])]])))
(defn- more-sheet-content [] (defn- more-sheet-content []
[react/view {:flex 1} [react/view {:flex 1}

View File

@ -12,7 +12,8 @@
[status-im.ui.screens.multiaccounts.recover.views :as multiaccounts.recover] [status-im.ui.screens.multiaccounts.recover.views :as multiaccounts.recover]
[status-im.ui.screens.signing.views :as signing] [status-im.ui.screens.signing.views :as signing]
[status-im.ui.screens.biometric.views :as biometric] [status-im.ui.screens.biometric.views :as biometric]
[status-im.ui.components.colors :as colors])) [status-im.ui.components.colors :as colors]
[status-im.ui.screens.hardwallet.frozen-card.view :as frozen-card]))
(defn hide-panel-anim (defn hide-panel-anim
[bottom-anim-value alpha-value window-height] [bottom-anim-value alpha-value window-height]
@ -137,6 +138,9 @@
(= :transaction-data view) (= :transaction-data view)
[signing/transaction-data] [signing/transaction-data]
(= :frozen-card view)
[frozen-card/frozen-card]
:else :else
[view])]]]]])))}))) [view])]]]]])))})))

View File

@ -126,5 +126,7 @@
:component hardwallet.settings/keycard-settings} :component hardwallet.settings/keycard-settings}
{:name :reset-card {:name :reset-card
:component hardwallet.settings/reset-card} :component hardwallet.settings/reset-card}
{:name :keycard-pin
:component hardwallet.settings/reset-pin}
{:name :enter-pin-settings {:name :enter-pin-settings
:component hardwallet.pin/enter-pin}]]) :component hardwallet.pin/enter-pin}]])

View File

@ -96,6 +96,7 @@
enter-step [:hardwallet/pin-enter-step] enter-step [:hardwallet/pin-enter-step]
status [:hardwallet/pin-status] status [:hardwallet/pin-status]
retry-counter [:hardwallet/retry-counter]] retry-counter [:hardwallet/retry-counter]]
(let [enter-step (or enter-step :sign)]
[react/view [react/view
[pin.views/pin-view [pin.views/pin-view
{:pin pin {:pin pin
@ -103,7 +104,7 @@
:step enter-step :step enter-step
:small-screen? small-screen? :small-screen? small-screen?
:status status :status status
:error-label error-label}]])) :error-label error-label}]])))
(defn sign-with-keycard-button (defn sign-with-keycard-button
[amount-error gas-error] [amount-error gas-error]

View File

@ -12,7 +12,8 @@
[status-im.ui.screens.wallet.accounts.sheets :as sheets] [status-im.ui.screens.wallet.accounts.sheets :as sheets]
[status-im.ui.screens.wallet.accounts.styles :as styles] [status-im.ui.screens.wallet.accounts.styles :as styles]
[status-im.utils.utils :as utils.utils] [status-im.utils.utils :as utils.utils]
[status-im.wallet.utils :as wallet.utils]) [status-im.wallet.utils :as wallet.utils]
[status-im.hardwallet.login :as hardwallet.login])
(:require-macros [status-im.utils.views :as views])) (:require-macros [status-im.utils.views :as views]))
(views/defview account-card [{:keys [name color address type] :as account}] (views/defview account-card [{:keys [name color address type] :as account}]
@ -130,14 +131,21 @@
(views/letsubs [currency [:wallet/currency] (views/letsubs [currency [:wallet/currency]
portfolio-value [:portfolio-value] portfolio-value [:portfolio-value]
empty-balances? [:empty-balances?] empty-balances? [:empty-balances?]
frozen-card? [:hardwallet/frozen-card?]
{:keys [mnemonic]} [:multiaccount]] {:keys [mnemonic]} [:multiaccount]]
[reanimated/view {:style (styles/container {:minimized minimized})} [reanimated/view {:style (styles/container {:minimized minimized})}
(when (and mnemonic minimized (not empty-balances?)) (when (or
(and frozen-card? minimized)
(and mnemonic minimized (not empty-balances?)))
[reanimated/view {:style (styles/accounts-mnemonic {:animation animation})} [reanimated/view {:style (styles/accounts-mnemonic {:animation animation})}
[react/touchable-highlight [react/touchable-highlight
{:on-press #(re-frame/dispatch [:navigate-to :profile-stack {:screen :backup-seed {:on-press #(re-frame/dispatch
:initial false}])} (if frozen-card?
[react/view {:flex-direction :row :align-items :center} [::hardwallet.login/reset-pin]
[:navigate-to :profile-stack {:screen :backup-seed
:initial false}]))}
[react/view {:flex-direction :row
:align-items :center}
[react/view {:width 14 [react/view {:width 14
:height 14 :height 14
:background-color colors/gray :background-color colors/gray
@ -151,7 +159,9 @@
"!"]] "!"]]
[react/text {:style {:color colors/gray} [react/text {:style {:color colors/gray}
:accessibility-label :back-up-your-seed-phrase-warning} :accessibility-label :back-up-your-seed-phrase-warning}
(i18n/label :t/back-up-your-seed-phrase)]]]]) (if frozen-card?
(i18n/label :t/your-card-is-frozen)
(i18n/label :t/back-up-your-seed-phrase))]]]])
[reanimated/view {:style (styles/value-container {:minimized minimized [reanimated/view {:style (styles/value-container {:minimized minimized
:animation animation}) :animation animation})

View File

@ -17,7 +17,8 @@
[status-im.ethereum.core :as ethereum] [status-im.ethereum.core :as ethereum]
[status-im.utils.security :as security] [status-im.utils.security :as security]
[clojure.string :as string] [clojure.string :as string]
[status-im.utils.platform :as platform])) [status-im.utils.platform :as platform]
[status-im.ui.components.bottom-panel.views :as bottom-panel]))
(defn- request-camera-permissions [] (defn- request-camera-permissions []
(let [options {:handler :wallet.add-new/qr-scanner-result}] (let [options {:handler :wallet.add-new/qr-scanner-result}]
@ -129,21 +130,38 @@
(defview pin [] (defview pin []
(letsubs [pin [:hardwallet/pin] (letsubs [pin [:hardwallet/pin]
status [:hardwallet/pin-status] status [:hardwallet/pin-status]
error-label [:hardwallet/pin-error-label]] error-label [:hardwallet/pin-error-label]
retry-counter [:hardwallet/retry-counter]]
[react/keyboard-avoiding-view {:style {:flex 1}} [react/keyboard-avoiding-view {:style {:flex 1}}
[topbar/topbar [topbar/topbar
{:navigation :none {:navigation :none
:accessories :accessories
[{:label :t/cancel [{:label :t/cancel
:handler #(re-frame/dispatch [:bottom-sheet/hide])}]}] :handler #(re-frame/dispatch [:hardwallet/new-account-pin-sheet-hide])}]}]
[pin.views/pin-view [pin.views/pin-view
{:pin pin {:pin pin
:status status :status status
:retry-counter retry-counter
:title-label :t/current-pin :title-label :t/current-pin
:description-label :t/current-pin-description :description-label :t/current-pin-description
:error-label error-label :error-label error-label
:step :export-key}]])) :step :export-key}]]))
(defn pin-sheet []
(let [show-sheet? @(re-frame/subscribe [:hardwallet/new-account-sheet?])
{window-height :height} @(re-frame/subscribe [:dimensions/window])]
[bottom-panel/bottom-panel
show-sheet?
(fn [_]
[react/view {:style
{:background-color colors/white
:border-top-right-radius 16
:border-top-left-radius 16
:padding-bottom 40
:flex 1}}
[pin]])
window-height]))
(defview add-account [] (defview add-account []
(letsubs [{:keys [type account] :as add-account} [:add-account] (letsubs [{:keys [type account] :as add-account} [:add-account]
add-account-disabled? [:add-account-disabled?] add-account-disabled? [:add-account-disabled?]
@ -164,7 +182,8 @@
:label :t/add-account :label :t/add-account
:accessibility-label :add-account-add-account-button :accessibility-label :add-account-add-account-button
:on-press :on-press
(if keycard? (if (and keycard?
(not= type :watch))
#(re-frame/dispatch [:hardwallet/new-account-pin-sheet #(re-frame/dispatch [:hardwallet/new-account-pin-sheet
{:view {:content pin {:view {:content pin
:height 256}}]) :height 256}}])
@ -177,5 +196,6 @@
(and (and
(not keycard?) (not keycard?)
(not (spec/valid? ::multiaccounts.db/password (not (spec/valid? ::multiaccounts.db/password
@entered-password)))))}}]])) @entered-password)))))}}]
[pin-sheet]]))

View File

@ -543,11 +543,15 @@
"joined-group-chat-description": "You've joined {{group-name}} from invitation by {{username}}", "joined-group-chat-description": "You've joined {{group-name}} from invitation by {{username}}",
"key": "Key", "key": "Key",
"keycard": "Keycard", "keycard": "Keycard",
"keycard-access-reset": "Keycard access is reset",
"keycard-can-use-with-new-passcode": "You can use this card with your new passcode",
"keycard-applet-install-instructions": "To install the applet please follow the instructions on https://github.com/status-im/keycard-cli#keycard-applet-installation", "keycard-applet-install-instructions": "To install the applet please follow the instructions on https://github.com/status-im/keycard-cli#keycard-applet-installation",
"keycard-blocked": "Keycard has been blocked.\nYou need to reset card to continue using it.", "keycard-blocked": "Keycard has been blocked.\nYou need to reset card to continue using it.",
"keycard-cancel-setup-text": "This will cancel keycard setup. It's highly recommended to finish the setup in order to use keycard. Do you really want to cancel?", "keycard-cancel-setup-text": "This will cancel keycard setup. It's highly recommended to finish the setup in order to use keycard. Do you really want to cancel?",
"keycard-cancel-setup-title": "Dangerous operation", "keycard-cancel-setup-title": "Dangerous operation",
"keycard-desc": "Own a Keycard? Store your keys on it; youll need it for transactions", "keycard-desc": "Own a Keycard? Store your keys on it; youll need it for transactions",
"keycard-reset-passcode": "Reset passcode",
"keycard-enter-new-passcode": "Enter new passcode {{step}}/2",
"keycard-has-multiaccount-on-it": "This card is full. Each card can hold one main keypair", "keycard-has-multiaccount-on-it": "This card is full. Each card can hold one main keypair",
"keycard-onboarding-finishing-header": "Finishing up", "keycard-onboarding-finishing-header": "Finishing up",
"keycard-onboarding-intro-header": "Store your keys on Keycard", "keycard-onboarding-intro-header": "Store your keys on Keycard",
@ -576,6 +580,13 @@
"keycard-recovery-phrase-confirmation-title": "Written the seed phrase down?", "keycard-recovery-phrase-confirmation-title": "Written the seed phrase down?",
"keycard-recovery-success-header": "Your keys have been\n successfully recovered", "keycard-recovery-success-header": "Your keys have been\n successfully recovered",
"keycard-unauthorized-operation": "You're unauthorized to perform this operation.\n Please tap valid card and try again.", "keycard-unauthorized-operation": "You're unauthorized to perform this operation.\n Please tap valid card and try again.",
"keycard-is-frozen-title": "Keycard is frozen",
"keycard-is-frozen-details": "To protect your assets, your card is frozen. Reset card access to unlock keys and send transactions. Create a new passcode and enter your PUK to access your account(s) on this card",
"keycard-is-frozen-reset": "Reset card access",
"your-card-is-frozen": "Your Keycard is frozen. Reset card access",
"keycard-is-blocked-title": "Keycard is blocked",
"keycard-is-blocked-details": "You can no longer use this card to access or sign for this account. There have been too many failed passcode and PUK attempts.",
"keycard-is-blocked-instructions": "To access your account reinstall Status and use a new Keycard, use a different wallet or reset Keycard manually.",
"language": "Language", "language": "Language",
"learn-more": "Learn more", "learn-more": "Learn more",
"learn-more-about-keycard": "Learn more about Keycard", "learn-more-about-keycard": "Learn more about Keycard",
@ -772,7 +783,12 @@
"pin-changed": "6-digit passcode has been changed", "pin-changed": "6-digit passcode has been changed",
"pin-code": "6-digit passcode", "pin-code": "6-digit passcode",
"pin-mismatch": "Wrong passcode", "pin-mismatch": "Wrong passcode",
"pin-retries-left": "You have {{number}} retries left", "pin-retries-left": "{{number}} attemps left",
"pin-one-attempt-blocked-before": "Be careful, you have only",
"pin-one-attempt-frozen-before": "Be careful, you have only",
"pin-one-attempt": " one attempt ",
"pin-one-attempt-blocked-after": "before your Keycard gets blocked",
"pin-one-attempt-frozen-after": "before your Keycard gets frozen",
"preview-privacy": "Preview privacy mode", "preview-privacy": "Preview privacy mode",
"privacy": "Privacy", "privacy": "Privacy",
"privacy-and-security": "Privacy and security", "privacy-and-security": "Privacy and security",
@ -789,7 +805,7 @@
"puk-and-pairing-codes-displayed": "PUK and pairing codes displayed", "puk-and-pairing-codes-displayed": "PUK and pairing codes displayed",
"puk-code": "PUK code", "puk-code": "PUK code",
"puk-code-explanation": "If you forget your 6-digit passcode or enter it incorrectly 3 times, you'll need this code to unlock your card.", "puk-code-explanation": "If you forget your 6-digit passcode or enter it incorrectly 3 times, you'll need this code to unlock your card.",
"puk-mismatch": "PUK code does not match", "puk-mismatch": "Wrong PUK code",
"quiet-days": "{{quiet-days}} days", "quiet-days": "{{quiet-days}} days",
"quiet-hours": "{{quiet-hours}} hours", "quiet-hours": "{{quiet-hours}} hours",
"re-encrypt-key": "Re-encrypt your keys", "re-encrypt-key": "Re-encrypt your keys",