[#21318] Keycard - Allow user to initiate Profile key pair migration on an empty Keycard (#21359)

This commit is contained in:
flexsurfer 2024-10-07 20:15:19 +02:00 committed by GitHub
parent a8bc93eb17
commit 80ad2b8fca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 406 additions and 94 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -65,7 +65,7 @@
:type type :type type
:include-button? include-button?}) :include-button? include-button?})
style)} style)}
[icons/icon icon [icons/icon (or icon :i/info)
{:color (style/get-color-by-type theme type :icon) {:color (style/get-color-by-type theme type :icon)
:no-color no-icon-color? :no-color no-icon-color?
:size (or icon-size 16) :size (or icon-size 16)

View File

@ -4,12 +4,10 @@
[container-style] [container-style]
(merge container-style (merge container-style
{:flex-direction :row {:flex-direction :row
:flex 1
:align-items :flex-start})) :align-items :flex-start}))
(def index (def index
{:margin-left 5}) {:margin-left 5})
(def text-container (def text-container
{:margin-left 8 {:margin-left 8})
:flex 1})

View File

@ -17,6 +17,7 @@
:generate-keys1 (js/require "../resources/images/ui2/generating-keys-1.png") :generate-keys1 (js/require "../resources/images/ui2/generating-keys-1.png")
:ethereum-address (js/require "../resources/images/ui2/ethereum-address.png") :ethereum-address (js/require "../resources/images/ui2/ethereum-address.png")
:use-keycard (js/require "../resources/images/ui2/keycard.png") :use-keycard (js/require "../resources/images/ui2/keycard.png")
:check-your-keycard (js/require "../resources/images/ui2/check-your-keycard.png")
:onboarding-illustration (js/require "../resources/images/ui2/onboarding_illustration.png") :onboarding-illustration (js/require "../resources/images/ui2/onboarding_illustration.png")
:qr-code (js/require "../resources/images/ui2/qr-code.png") :qr-code (js/require "../resources/images/ui2/qr-code.png")
:keycard-logo (js/require "../resources/images/ui2/keycard-logo.png") :keycard-logo (js/require "../resources/images/ui2/keycard-logo.png")

View File

@ -0,0 +1,36 @@
(ns status-im.contexts.keycard.check.view
(:require [quo.core :as quo]
[react-native.core :as rn]
[status-im.common.events-helper :as events-helper]
[status-im.common.resources :as resources]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn view
[]
[:<>
[quo/page-nav
{:key :header
:background :blur
:icon-name :i/arrow-left
:on-press events-helper/navigate-back}]
[quo/page-top
{:title (i18n/label :t/check-keycard)
:description :text
:description-text (i18n/label :t/see-keycard-ready)}]
[rn/view {:style {:flex 1 :align-items :center :justify-content :center}}
[rn/image
{:resize-mode :contain
:source (resources/get-image :check-your-keycard)}]]
[quo/divider-label (i18n/label :t/tips-scan-keycard)]
[rn/view {:style {:padding-horizontal 10}}
[quo/markdown-list
{:container-style {:padding-vertical 10}
:description (i18n/label :t/remove-phone-case)}]
[quo/markdown-list
{:container-style {:padding-bottom 25}
:description (i18n/label :t/keep-card-steady)}]]
[quo/button
{:on-press #(rf/dispatch [:keycard/connect])
:container-style {:margin-horizontal 20}}
(i18n/label :t/ready-to-scan)]])

View File

@ -10,10 +10,15 @@
(defonce ^:private active-listeners (atom [])) (defonce ^:private active-listeners (atom []))
(defn register-card-events (defn unregister-card-events
[] []
(doseq [listener @active-listeners] (doseq [listener @active-listeners]
(keycard/remove-event-listener listener)) (keycard/remove-event-listener listener))
(reset! active-listeners nil))
(defn register-card-events
[]
(unregister-card-events)
(reset! active-listeners (reset! active-listeners
[(keycard/on-card-connected #(rf/dispatch [:keycard/on-card-connected])) [(keycard/on-card-connected #(rf/dispatch [:keycard/on-card-connected]))
(keycard/on-card-disconnected #(rf/dispatch [:keycard/on-card-disconnected])) (keycard/on-card-disconnected #(rf/dispatch [:keycard/on-card-disconnected]))
@ -23,7 +28,9 @@
(keycard/on-nfc-timeout #(rf/dispatch [:keycard.ios/on-nfc-timeout]))) (keycard/on-nfc-timeout #(rf/dispatch [:keycard.ios/on-nfc-timeout])))
(keycard/on-nfc-enabled #(rf/dispatch [:keycard/on-check-nfc-enabled-success true])) (keycard/on-nfc-enabled #(rf/dispatch [:keycard/on-check-nfc-enabled-success true]))
(keycard/on-nfc-disabled #(rf/dispatch [:keycard/on-check-nfc-enabled-success false]))])) (keycard/on-nfc-disabled #(rf/dispatch [:keycard/on-check-nfc-enabled-success false]))]))
(rf/reg-fx :effects.keycard/register-card-events register-card-events) (rf/reg-fx :effects.keycard/register-card-events register-card-events)
(rf/reg-fx :effects.keycard/unregister-card-events unregister-card-events)
(defn check-nfc-enabled (defn check-nfc-enabled
[] []

View File

@ -0,0 +1,36 @@
(ns status-im.contexts.keycard.empty.view
(:require [quo.core :as quo]
[react-native.core :as rn]
[status-im.common.events-helper :as events-helper]
[status-im.common.resources :as resources]
[status-im.contexts.keycard.sheets.migrate.view :as sheets.migrate]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn view
[]
[:<>
[quo/page-nav
{:key :header
:background :blur
:icon-name :i/arrow-left
:on-press events-helper/navigate-back}]
[quo/page-top
{:title (i18n/label :t/keycard-empty)
:description :text
:description-text (i18n/label :t/what-to-do)}]
[rn/view {:style {:padding-horizontal 28 :padding-top 20}}
[quo/small-option-card
{:variant :main
:title (i18n/label :t/import-key-pair-keycard)
:subtitle (i18n/label :t/use-keycard-login-sign)
:button-label (i18n/label :t/import-profile-key-pair)
:accessibility-label :get-keycard
:image (resources/get-image :generate-keys)
:on-press #(rf/dispatch [:show-bottom-sheet
{:theme :dark
:content (fn [] [sheets.migrate/view])}])}]]
[quo/information-box
{:type :default
:style {:margin-top 32 :margin-horizontal 28}}
(i18n/label :t/empty-card-info)]])

View File

@ -0,0 +1,42 @@
(ns status-im.contexts.keycard.error.view
(:require [quo.core :as quo]
[react-native.core :as rn]
[react-native.safe-area :as safe-area]
[status-im.common.events-helper :as events-helper]
[utils.re-frame :as rf]))
(def titles
{:keycard/error.keycard-wrong {:title "Keycard is not empty"
:description "You cant use it to store new keys right now"}
:keycard/error.keycard-unpaired {:title "Keycard is full"
:description "All pairing slots are occupied"}
:keycard/error.keycard-frozen {:title "Keycard is locked"
:description "You cant use it right now"}
:keycard/error.keycard-locked {:title "Keycard is locked"
:description "You cant use it right now"}})
(defn view
[]
(let [{:keys [top bottom]} (safe-area/get-insets)
error (rf/sub [:keycard/application-info-error])
{:keys [title description]} (get titles error)]
[quo/overlay
{:type :shell
:container-style {:padding-top top
:padding-bottom bottom}}
[quo/page-nav
{:key :header
:background :blur
:icon-name :i/arrow-left
:on-press events-helper/navigate-back}]
[quo/page-top
{:title title
:description :text
:description-text description}]
[rn/view {:height 226}]
[rn/view {:padding-horizontal 20}
[quo/info-message
{:container-style {:padding-top 15}
:icon :i/info
:size :default}
"To unlock or factory reset the Keycard, please use the Status desktop app. If you'd like this features on mobile, feel free to upvote them and discuss in the Status community."]]]))

View File

@ -1,8 +1,8 @@
(ns status-im.contexts.keycard.events (ns status-im.contexts.keycard.events
(:require [re-frame.core :as rf] (:require [re-frame.core :as rf]
status-im.contexts.keycard.login.events status-im.contexts.keycard.login.events
status-im.contexts.keycard.nfc-sheet.events
status-im.contexts.keycard.pin.events status-im.contexts.keycard.pin.events
status-im.contexts.keycard.sheet.events
status-im.contexts.keycard.sign.events status-im.contexts.keycard.sign.events
[status-im.contexts.keycard.utils :as keycard.utils] [status-im.contexts.keycard.utils :as keycard.utils]
[taoensso.timbre :as log])) [taoensso.timbre :as log]))
@ -14,7 +14,9 @@
(rf/reg-event-fx :keycard.ios/on-nfc-user-cancelled (rf/reg-event-fx :keycard.ios/on-nfc-user-cancelled
(fn [{:keys [db]}] (fn [{:keys [db]}]
(log/debug "[keycard] nfc user cancelled") (log/debug "[keycard] nfc user cancelled")
{:db (assoc-in db [:keycard :pin :status] nil) {:db (-> db
(assoc-in [:keycard :pin :status] nil)
(assoc-in [:keycard :on-nfc-cancelled-event-vector] nil))
:fx [(when-let [on-nfc-cancelled-event-vector (get-in db [:keycard :on-nfc-cancelled-event-vector])] :fx [(when-let [on-nfc-cancelled-event-vector (get-in db [:keycard :on-nfc-cancelled-event-vector])]
[:dispatch on-nfc-cancelled-event-vector])]})) [:dispatch on-nfc-cancelled-event-vector])]}))
@ -42,12 +44,6 @@
{:db (assoc-in db [:keycard :card-connected?] false) {:db (assoc-in db [:keycard :card-connected?] false)
:fx [[:dispatch-later [{:ms 500 :dispatch [:keycard.ios/start-nfc]}]]]})) :fx [[:dispatch-later [{:ms 500 :dispatch [:keycard.ios/start-nfc]}]]]}))
(rf/reg-event-fx :keycard/get-application-info
(fn [_ [{:keys [on-success on-failure]}]]
(log/debug "[keycard] get-application-info")
{:effects.keycard/get-application-info {:on-success on-success
:on-failure on-failure}}))
(rf/reg-event-fx :keycard/on-retrieve-pairings-success (rf/reg-event-fx :keycard/on-retrieve-pairings-success
(fn [{:keys [db]} [pairings]] (fn [{:keys [db]} [pairings]]
{:db (assoc-in db [:keycard :pairings] pairings) {:db (assoc-in db [:keycard :pairings] pairings)
@ -60,7 +56,7 @@
(rf/reg-event-fx :keycard/on-action-with-pin-error (rf/reg-event-fx :keycard/on-action-with-pin-error
(fn [{:keys [db]} [error]] (fn [{:keys [db]} [error]]
(log/debug "[keycard] get keys error: " error) (log/debug "[keycard] on-action-with-pin-error: " error)
(let [tag-was-lost? (keycard.utils/tag-lost? (:error error)) (let [tag-was-lost? (keycard.utils/tag-lost? (:error error))
pin-retries-count (keycard.utils/pin-retries (:error error))] pin-retries-count (keycard.utils/pin-retries (:error error))]
(if tag-was-lost? (if tag-was-lost?
@ -69,7 +65,53 @@
{:effects.utils/show-popup {:title "wrong-keycard"}} {:effects.utils/show-popup {:title "wrong-keycard"}}
{:db (-> db {:db (-> db
(assoc-in [:keycard :application-info :pin-retry-counter] pin-retries-count) (assoc-in [:keycard :application-info :pin-retry-counter] pin-retries-count)
(update-in [:keycard :pin] assoc :status :error)) (assoc-in [:keycard :pin :status] :error))
:fx [[:dispatch [:keycard/hide-connection-sheet]] :fx [[:dispatch [:keycard/disconnect]]
(when (zero? pin-retries-count) (when (zero? pin-retries-count)
[:effects.utils/show-popup {:title "frozen-keycard"}])]}))))) [:effects.utils/show-popup {:title "frozen-keycard"}])]})))))
(rf/reg-event-fx :keycard/on-get-application-info-success
(fn [{:keys [db]} [application-info {:keys [key-uid on-success-fx]}]]
(let [error (keycard.utils/validate-application-info key-uid application-info)]
(if error
(case error
:keycard/error.not-keycard
{:fx [[:dispatch [:keycard/disconnect]]
[:dispatch [:open-modal :screen/keycard.not-keycard]]]}
:keycard/error.keycard-blank
{:fx [[:dispatch [:keycard/disconnect]]
[:dispatch [:open-modal :screen/keycard.empty]]]}
{:db (assoc-in db [:keycard :application-info-error] error)
:fx [[:dispatch [:keycard/disconnect]]
[:dispatch [:open-modal :screen/keycard.error]]]})
{:db (-> db
(assoc-in [:keycard :application-info] application-info)
(assoc-in [:keycard :pin :status] :verifying))
:fx on-success-fx}))))
(rf/reg-event-fx :keycard/get-application-info
(fn [_ [{:keys [on-success on-failure]}]]
{:effects.keycard/get-application-info {:on-success on-success :on-failure on-failure}}))
(rf/reg-event-fx :keycard/cancel-connection
(fn [{:keys [db]}]
{:db (-> db
(assoc-in [:keycard :on-card-connected-event-vector] nil)
(assoc-in [:keycard :on-nfc-cancelled-event-vector] nil))}))
(rf/reg-event-fx :keycard/disconnect
(fn [_ _]
{:fx [[:dispatch [:keycard/cancel-connection]]
[:dispatch [:keycard/hide-connection-sheet]]]}))
(rf/reg-event-fx :keycard/connect
(fn [{:keys [db]} [args]]
(let [connected? (get-in db [:keycard :card-connected?])
event-vector [:keycard/get-application-info
{:on-success #(rf/dispatch [:keycard/on-get-application-info-success % args])}]]
{:db (assoc-in db [:keycard :on-card-connected-event-vector] event-vector)
:fx [[:dispatch
[:keycard/show-connection-sheet
{:on-cancel-event-vector [:keycard/cancel-connection]}]]
(when connected?
[:dispatch event-vector])]})))

View File

@ -1,7 +1,5 @@
(ns status-im.contexts.keycard.login.events (ns status-im.contexts.keycard.login.events
(:require [status-im.contexts.keycard.utils :as keycard.utils] (:require [utils.re-frame :as rf]))
[taoensso.timbre :as log]
[utils.re-frame :as rf]))
(rf/reg-event-fx :keycard.login/on-get-keys-success (rf/reg-event-fx :keycard.login/on-get-keys-success
(fn [{:keys [db]} [data]] (fn [{:keys [db]} [data]]
@ -16,7 +14,7 @@
:password encryption-public-key :password encryption-public-key
:key-uid key-uid :key-uid key-uid
:name (:name profile))) :name (:name profile)))
:fx [[:dispatch [:keycard/hide-connection-sheet]] :fx [[:dispatch [:keycard/disconnect]]
[:effects.keycard/login-with-keycard [:effects.keycard/login-with-keycard
{:password encryption-public-key {:password encryption-public-key
:whisper-private-key whisper-private-key :whisper-private-key whisper-private-key
@ -33,36 +31,8 @@
:password encryption-public-key :password encryption-public-key
:key-uid key-uid :key-uid key-uid
:name (:name profile))) :name (:name profile)))
:fx [[:dispatch [:keycard/hide-connection-sheet]] :fx [[:dispatch [:keycard/disconnect]]
[:effects.keycard/login-with-keycard [:effects.keycard/login-with-keycard
{:password encryption-public-key {:password encryption-public-key
:whisper-private-key whisper-private-key :whisper-private-key whisper-private-key
:key-uid key-uid}]]})))) :key-uid key-uid}]]}))))
(rf/reg-event-fx :keycard.login/on-get-application-info-success
(fn [{:keys [db]} [application-info {:keys [key-uid on-read-fx]}]]
(let [error (keycard.utils/validate-application-info key-uid application-info)]
(if error
{:effects.utils/show-popup {:title (str error)}}
{:db (-> db
(assoc-in [:keycard :application-info] application-info)
(assoc-in [:keycard :pin :status] :verifying))
:fx on-read-fx}))))
(rf/reg-event-fx :keycard.login/cancel-reading-card
(fn [{:keys [db]}]
{:db (assoc-in db [:keycard :on-card-connected-event-vector] nil)}))
(rf/reg-event-fx :keycard/read-card
(fn [{:keys [db]} [args]]
(let [connected? (get-in db [:keycard :card-connected?])
event-vector [:keycard/get-application-info
{:on-success #(rf/dispatch [:keycard.login/on-get-application-info-success %
args])}]]
(log/debug "[keycard] proceed-to-login")
{:db (assoc-in db [:keycard :on-card-connected-event-vector] event-vector)
:fx [[:dispatch
[:keycard/show-connection-sheet
{:on-cancel-event-vector [:keycard.login/cancel-reading-card]}]]
(when connected?
[:dispatch event-vector])]})))

View File

@ -1,18 +1,16 @@
(ns status-im.contexts.keycard.sheet.events (ns status-im.contexts.keycard.nfc-sheet.events
(:require [re-frame.core :as rf] (:require [re-frame.core :as rf]
[react-native.platform :as platform] [react-native.platform :as platform]
[taoensso.timbre :as log])) [taoensso.timbre :as log]))
(rf/reg-event-fx :keycard/show-connection-sheet-component
(fn [{:keys [db]} [{:keys [on-cancel-event-vector]}]]
{:db (assoc-in db [:keycard :connection-sheet-opts] {:on-close #(rf/dispatch on-cancel-event-vector)})
:fx [[:dismiss-keyboard true]
[:show-nfc-sheet nil]]}))
(rf/reg-event-fx :keycard/show-connection-sheet (rf/reg-event-fx :keycard/show-connection-sheet
(fn [_ [args]] (fn [{:keys [db]} [{:keys [on-cancel-event-vector]} :as args]]
(if platform/android? (if platform/android?
{:dispatch [:keycard/show-connection-sheet-component args]} {:db (assoc-in db
[:keycard :connection-sheet-opts]
{:on-close #(rf/dispatch on-cancel-event-vector)})
:fx [[:dismiss-keyboard true]
[:show-nfc-sheet nil]]}
{:effects.keycard.ios/start-nfc {:effects.keycard.ios/start-nfc
{:on-success {:on-success
(fn [] (fn []

View File

@ -1,8 +1,9 @@
(ns status-im.contexts.keycard.sheet.view (ns status-im.contexts.keycard.nfc-sheet.view
(:require [quo.foundations.colors :as colors] (:require [quo.foundations.colors :as colors]
quo.theme quo.theme
[react-native.core :as rn] [react-native.core :as rn]
[status-im.common.resources :as resources] [status-im.common.resources :as resources]
[utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(defn connect-keycard (defn connect-keycard
@ -18,13 +19,13 @@
:padding-vertical 30 :padding-vertical 30
:background-color (colors/theme-colors colors/white colors/neutral-95 theme)}} :background-color (colors/theme-colors colors/white colors/neutral-95 theme)}}
[rn/text {:style {:font-size 26 :color "#9F9FA5" :margin-bottom 36}} [rn/text {:style {:font-size 26 :color "#9F9FA5" :margin-bottom 36}}
"Ready to Scan"] (i18n/label :t/ready-to-scan)]
[rn/image [rn/image
{:source (resources/get-image :nfc-prompt)}] {:source (resources/get-image :nfc-prompt)}]
[rn/text {:style {:font-size 16 :color :white :margin-vertical 36}} [rn/text {:style {:font-size 16 :color :white :margin-vertical 36}}
(if connected? (if connected?
"Connected. Dont move your card." (i18n/label :t/connected-dont-move)
"Hold your phone near a Status Keycard")] (i18n/label :t/hold-phone-near-keycard))]
[rn/pressable [rn/pressable
{:on-press (fn [] {:on-press (fn []
(when on-close (on-close)) (when on-close (on-close))
@ -33,4 +34,4 @@
[rn/view [rn/view
{:style {:background-color "#8E8E93" :flex 1 :align-items :center :padding 18 :border-radius 10}} {:style {:background-color "#8E8E93" :flex 1 :align-items :center :padding 18 :border-radius 10}}
[rn/text {:style {:color :white :font-size 16}} [rn/text {:style {:color :white :font-size 16}}
"Cancel"]]]]])) (i18n/label :t/cancel)]]]]]))

View File

@ -0,0 +1,28 @@
(ns status-im.contexts.keycard.not-keycard.view
(:require [quo.core :as quo]
[react-native.core :as rn]
[react-native.safe-area :as safe-area]
[status-im.common.events-helper :as events-helper]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn view
[]
(let [{:keys [top bottom]} (safe-area/get-insets)]
[quo/overlay
{:type :shell
:container-style {:padding-top top
:padding-bottom bottom}}
[quo/page-nav
{:key :header
:background :blur
:icon-name :i/arrow-left
:on-press events-helper/navigate-back}]
[quo/page-top
{:title (i18n/label :t/oops-not-keycard)
:description :text
:description-text (i18n/label :t/make-sure-keycard)}]
[rn/view {:flex 1}]
[rn/view {:padding-horizontal 20}
[quo/button {:on-press #(rf/dispatch [:keycard/connect])}
(i18n/label :t/try-again)]]]))

View File

@ -0,0 +1,33 @@
(ns status-im.contexts.keycard.sheets.migrate.view
(:require [quo.core :as quo]
[react-native.core :as rn]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn view
[]
(let [profile-name (rf/sub [:profile/name])
profile-picture (rf/sub [:profile/image])
customization-color (rf/sub [:profile/customization-color])]
[:<>
[quo/drawer-top
{:type :context-tag
:context-tag-type :default
:title (i18n/label :t/migrate-key-pair-keycard)
:full-name profile-name
:profile-picture profile-picture
:customization-color customization-color}]
[rn/view {:style {:padding-horizontal 20}}
[quo/text {}
(i18n/label :t/migrate-key-pair-keycard-default-key {:name profile-name})]
[quo/information-box
{:type :default
:style {:margin-top 20 :margin-bottom 12}}
(i18n/label :t/migrate-key-pair-keycard-info)]]
[quo/bottom-actions
{:actions :two-actions
:button-one-label (i18n/label :t/continue)
:button-one-props {:on-press #()}
:button-two-label (i18n/label :t/cancel)
:button-two-props {:type :grey
:on-press #(rf/dispatch [:hide-bottom-sheet])}}]]))

View File

@ -14,16 +14,17 @@
path (get-in db [:wallet :accounts current-address :path]) path (get-in db [:wallet :accounts current-address :path])
key-uid (get-in db [:profile/profile :key-uid])] key-uid (get-in db [:profile/profile :key-uid])]
{:fx [[:dispatch {:fx [[:dispatch
[:keycard/read-card [:keycard/connect
{:key-uid key-uid {:key-uid key-uid
:on-read-fx [[:effects.keycard/sign :on-success-fx [[:effects.keycard/sign
{:pin pin-text {:pin pin-text
:path path :path path
:hash (utils.address/naked-address tx-hash) :hash (utils.address/naked-address tx-hash)
:on-success :on-success
#(do #(do
(rf/dispatch [:keycard/hide-connection-sheet]) (rf/dispatch [:keycard/disconnect])
(rf/dispatch (rf/dispatch
[:wallet/proceed-with-transactions-signatures [:wallet/proceed-with-transactions-signatures
(get-signature-map tx-hash %)])) (get-signature-map tx-hash %)]))
:on-failure #(rf/dispatch [:keycard/on-action-with-pin-error %])}]]}]]]}))) :on-failure #(rf/dispatch [:keycard/on-action-with-pin-error
%])}]]}]]]})))

View File

@ -18,28 +18,31 @@
(defn validate-application-info (defn validate-application-info
[profile-key-uid {:keys [key-uid paired? pin-retry-counter puk-retry-counter] :as application-info}] [profile-key-uid {:keys [key-uid paired? pin-retry-counter puk-retry-counter] :as application-info}]
(let [profile-mismatch? (or (nil? profile-key-uid) (not= profile-key-uid key-uid))] (let [profile-mismatch? (or (nil? profile-key-uid) (not= profile-key-uid key-uid))]
(log/debug "[keycard] login-with-keycard" (log/debug "[keycard]" "login-with-keycard"
"empty application info" (empty? application-info) "empty application info" (empty? application-info)
"no key-uid" (empty? key-uid) "no key-uid" (empty? key-uid)
"profile-mismatch?" profile-mismatch? "profile-mismatch?" profile-mismatch?
"no pairing" paired?) "no pairing" paired?)
(cond (cond
(empty? application-info) (empty? application-info)
:not-keycard :keycard/error.not-keycard
(empty? (:key-uid application-info)) (empty? (:key-uid application-info))
:keycard-blank :keycard/error.keycard-blank
profile-mismatch? profile-mismatch?
:keycard-wrong :keycard/error.keycard-wrong
(not paired?) (not paired?)
:keycard-unpaired :keycard/error.keycard-unpaired
(and (zero? pin-retry-counter) (and (zero? pin-retry-counter)
(or (nil? puk-retry-counter) (or (nil? puk-retry-counter)
(pos? puk-retry-counter))) (pos? puk-retry-counter)))
nil :keycard/error.keycard-frozen
(zero? puk-retry-counter)
:keycard/error.keycard-locked
:else :else
nil))) nil)))

View File

@ -242,12 +242,13 @@
{:on-complete {:on-complete
(fn [pin-text] (fn [pin-text]
(rf/dispatch (rf/dispatch
[:keycard/read-card [:keycard/connect
{:key-uid key-uid {:key-uid key-uid
:on-read-fx [[:effects.keycard/get-keys :on-success-fx [[:effects.keycard/get-keys
{:pin pin-text {:pin pin-text
:on-success #(rf/dispatch [:keycard.login/on-get-keys-success %]) :on-success #(rf/dispatch [:keycard.login/on-get-keys-success %])
:on-failure #(rf/dispatch [:keycard/on-action-with-pin-error %])}]]}]))}] :on-failure #(rf/dispatch [:keycard/on-action-with-pin-error
%])}]]}]))}]
[password-input])] [password-input])]
(when-not keycard-pairing (when-not keycard-pairing
[quo/button [quo/button

View File

@ -1,6 +1,7 @@
(ns status-im.contexts.profile.settings.list-items (ns status-im.contexts.profile.settings.list-items
(:require [status-im.common.not-implemented :as not-implemented] (:require [status-im.common.not-implemented :as not-implemented]
[status-im.config :as config] [status-im.config :as config]
[status-im.feature-flags :as ff]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
@ -54,9 +55,9 @@
:image :icon :image :icon
:blur? true :blur? true
:action :arrow}) :action :arrow})
(when config/show-not-implemented-features? (when (ff/enabled? ::ff/keycard.migrate-profile)
{:title (i18n/label :t/keycard) {:title (i18n/label :t/keycard)
:on-press not-implemented/alert :on-press #(rf/dispatch [:open-modal :screen/settings.keycard])
:image-props :i/keycard :image-props :i/keycard
:image :icon :image :icon
:blur? true :blur? true

View File

@ -0,0 +1,45 @@
(ns status-im.contexts.settings.keycard.view
(:require [quo.core :as quo]
[quo.foundations.colors :as colors]
[react-native.core :as rn]
[status-im.common.events-helper :as events-helper]
[status-im.common.resources :as resources]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
(defn view
[]
(let [keycard-profile? (rf/sub [:keycard/keycard-profile?])]
[:<>
[quo/page-nav
{:key :header
:background :blur
:icon-name :i/arrow-left
:on-press events-helper/navigate-back}]
[quo/page-top
{:title (i18n/label :t/keycard)}]
(if keycard-profile?
[:<>]
[rn/view {:style {:padding-horizontal 28 :padding-top 20}}
[quo/small-option-card
{:variant :main
:title (i18n/label :t/get-keycard)
:subtitle (i18n/label :t/secure-wallet-card)
:button-label (i18n/label :t/buy-keycard)
:accessibility-label :get-keycard
:image (resources/get-image :generate-keys)
:on-press #()}]
[rn/view {:style {:margin-top 24}}
[quo/text
{:style {:margin-bottom 12
:color colors/white-opa-70}
:size :paragraph-2
:weight :medium}
(i18n/label :t/own-keycard)]]
[quo/small-option-card
{:variant :icon
:title (i18n/label :t/setup-keycard)
:subtitle (i18n/label :t/ready-keycard)
:accessibility-label :setup-keycard
:image (resources/get-image :use-keycard)
:on-press #(rf/dispatch [:open-modal :screen/keycard.check])}]])]))

View File

@ -32,7 +32,8 @@
::wallet.long-press-watch-only-asset (enabled-in-env? :FLAG_LONG_PRESS_WATCH_ONLY_ASSET_ENABLED) ::wallet.long-press-watch-only-asset (enabled-in-env? :FLAG_LONG_PRESS_WATCH_ONLY_ASSET_ENABLED)
::wallet.saved-addresses (enabled-in-env? :WALLET_SAVED_ADDRESSES) ::wallet.saved-addresses (enabled-in-env? :WALLET_SAVED_ADDRESSES)
::wallet.wallet-connect (enabled-in-env? :FLAG_WALLET_CONNECT_ENABLED) ::wallet.wallet-connect (enabled-in-env? :FLAG_WALLET_CONNECT_ENABLED)
::wallet.custom-network-amounts (enabled-in-env? :FLAG_WALLET_CUSTOM_NETWORK_AMOUNTS_ENABLED)}) ::wallet.custom-network-amounts (enabled-in-env? :FLAG_WALLET_CUSTOM_NETWORK_AMOUNTS_ENABLED)
::keycard.migrate-profile false})
(defonce ^:private feature-flags-config (defonce ^:private feature-flags-config
(reagent/atom initial-flags)) (reagent/atom initial-flags))

View File

@ -69,6 +69,12 @@
transitions/new-to-status-modal-animations transitions/new-to-status-modal-animations
transitions/push-animations-for-transparent-background)})) transitions/push-animations-for-transparent-background)}))
(def keycard-modal-screen-options
(assoc transparent-modal-screen-options
:layout nil
:theme :dark
:insets {:top? true :bottom? true}))
(def sheet-options (def sheet-options
{:layout {:componentBackgroundColor :transparent {:layout {:componentBackgroundColor :transparent
:orientation ["portrait"] :orientation ["portrait"]

View File

@ -26,6 +26,10 @@
[status-im.contexts.communities.actions.share-community.view :as share-community] [status-im.contexts.communities.actions.share-community.view :as share-community]
[status-im.contexts.communities.discover.view :as communities.discover] [status-im.contexts.communities.discover.view :as communities.discover]
[status-im.contexts.communities.overview.view :as communities.overview] [status-im.contexts.communities.overview.view :as communities.overview]
[status-im.contexts.keycard.check.view :as keycard.check]
[status-im.contexts.keycard.empty.view :as keycard.empty]
[status-im.contexts.keycard.error.view :as keycard.error]
[status-im.contexts.keycard.not-keycard.view :as keycard.not-keycard]
[status-im.contexts.onboarding.create-or-sync-profile.view :as create-or-sync-profile] [status-im.contexts.onboarding.create-or-sync-profile.view :as create-or-sync-profile]
[status-im.contexts.onboarding.create-password.view :as create-password] [status-im.contexts.onboarding.create-password.view :as create-password]
[status-im.contexts.onboarding.create-profile.view :as create-profile] [status-im.contexts.onboarding.create-profile.view :as create-profile]
@ -58,6 +62,7 @@
[status-im.contexts.profile.settings.screens.password.view :as settings-password] [status-im.contexts.profile.settings.screens.password.view :as settings-password]
[status-im.contexts.profile.settings.screens.syncing.view :as settings.syncing] [status-im.contexts.profile.settings.screens.syncing.view :as settings.syncing]
[status-im.contexts.profile.settings.view :as settings] [status-im.contexts.profile.settings.view :as settings]
[status-im.contexts.settings.keycard.view :as settings.keycard]
[status-im.contexts.settings.language-and-currency.currency.view :as settings.currency-selection] [status-im.contexts.settings.language-and-currency.currency.view :as settings.currency-selection]
[status-im.contexts.settings.language-and-currency.view :as settings.language-and-currency] [status-im.contexts.settings.language-and-currency.view :as settings.language-and-currency]
[status-im.contexts.settings.privacy-and-security.share-usage.view :as settings.share-usage] [status-im.contexts.settings.privacy-and-security.share-usage.view :as settings.share-usage]
@ -584,6 +589,10 @@
:options options/transparent-modal-screen-options :options options/transparent-modal-screen-options
:component wallet-options/view} :component wallet-options/view}
{:name :screen/settings.keycard
:options options/keycard-modal-screen-options
:component settings.keycard/view}
{:name :screen/settings.rename-keypair {:name :screen/settings.rename-keypair
:options options/transparent-screen-options :options options/transparent-screen-options
:component keypair-rename/view} :component keypair-rename/view}
@ -671,7 +680,24 @@
:popGesture false :popGesture false
:hardwareBackButton {:dismissModalOnPress false :hardwareBackButton {:dismissModalOnPress false
:popStackOnPress false}) :popStackOnPress false})
:component change-password-loading/view}] :component change-password-loading/view}
;; Keycard
{:name :screen/keycard.check
:options options/keycard-modal-screen-options
:component keycard.check/view}
{:name :screen/keycard.empty
:options options/keycard-modal-screen-options
:component keycard.empty/view}
{:name :screen/keycard.error
:options options/keycard-modal-screen-options
:component keycard.error/view}
{:name :screen/keycard.not-keycard
:options options/keycard-modal-screen-options
:component keycard.not-keycard/view}]
[{:name :shell [{:name :shell
:options {:theme :dark}}] :options {:theme :dark}}]

View File

@ -11,7 +11,7 @@
[status-im.common.bottom-sheet-screen.view :as bottom-sheet-screen] [status-im.common.bottom-sheet-screen.view :as bottom-sheet-screen]
[status-im.common.bottom-sheet.view :as bottom-sheet] [status-im.common.bottom-sheet.view :as bottom-sheet]
[status-im.common.toasts.view :as toasts] [status-im.common.toasts.view :as toasts]
[status-im.contexts.keycard.sheet.view :as keycard.sheet] [status-im.contexts.keycard.nfc-sheet.view :as keycard.sheet]
[status-im.navigation.screens :as screens] [status-im.navigation.screens :as screens]
[status-im.setup.hot-reload :as reloader] [status-im.setup.hot-reload :as reloader]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
@ -47,7 +47,7 @@
(merge (merge
{:flex 1 {:flex 1
:margin-top alert-banners-top-margin :margin-top alert-banners-top-margin
:background-color (or background-color (colors/theme-colors colors/white colors/neutral-100 theme))} :background-color (or background-color (colors/theme-colors colors/white colors/neutral-95 theme))}
(when bottom? (when bottom?
{:padding-bottom (safe-area/get-bottom)}) {:padding-bottom (safe-area/get-bottom)})
(when top? (when top?

View File

@ -36,3 +36,8 @@
(fn [keycard] (fn [keycard]
(:connection-sheet-opts keycard))) (:connection-sheet-opts keycard)))
(rf/reg-sub
:keycard/application-info-error
:<- [:keycard]
(fn [keycard]
(:application-info-error keycard)))

View File

@ -326,6 +326,12 @@
(fn [profile] (fn [profile]
(profile.utils/photo profile))) (profile.utils/photo profile)))
(re-frame/reg-sub
:profile/name
:<- [:profile/profile]
(fn [profile]
(profile.utils/displayed-name profile)))
(re-frame/reg-sub (re-frame/reg-sub
:profile/login-profile :profile/login-profile
:<- [:profile/login] :<- [:profile/login]

View File

@ -274,6 +274,7 @@
"buy-crypto-title": "Looks like your wallet is empty", "buy-crypto-title": "Looks like your wallet is empty",
"buy-eth": "Buy ETH", "buy-eth": "Buy ETH",
"buy-ethereum": "Buy Ethereum", "buy-ethereum": "Buy Ethereum",
"buy-keycard": "Buy Keycard",
"by-continuing-you-accept": "By continuing you accept our ", "by-continuing-you-accept": "By continuing you accept our ",
"camera-access-error": "To grant the required camera permission, please go to your system settings and make sure that Status > Camera is selected.", "camera-access-error": "To grant the required camera permission, please go to your system settings and make sure that Status > Camera is selected.",
"camera-permission-denied": "Permission denied", "camera-permission-denied": "Permission denied",
@ -362,6 +363,7 @@
"check-before-syncing-doc-checkbox-2": "Make sure you are logged in on the other device", "check-before-syncing-doc-checkbox-2": "Make sure you are logged in on the other device",
"check-before-syncing-doc-checkbox-3": "Disable the firewall and VPN on your devices", "check-before-syncing-doc-checkbox-3": "Disable the firewall and VPN on your devices",
"check-before-syncing-doc-description": "To sync your devices successfully, make sure to check and complete these steps:", "check-before-syncing-doc-description": "To sync your devices successfully, make sure to check and complete these steps:",
"check-keycard": "Check your Keycard",
"check-on-block-explorer": "Check on block explorer", "check-on-block-explorer": "Check on block explorer",
"check-on-opensea": "Check on opensea", "check-on-opensea": "Check on opensea",
"check-other-device-for-pairing": "Check your other device for a pairing request.", "check-other-device-for-pairing": "Check your other device for a pairing request.",
@ -470,6 +472,7 @@
"connect-with-users": "Connect with users", "connect-with-users": "Connect with users",
"connected": "Connected", "connected": "Connected",
"connected-dapps": "Connected dApps", "connected-dapps": "Connected dApps",
"connected-dont-move": "Connected. Dont move your card.",
"connected-peers": "Connected and discovered peers", "connected-peers": "Connected and discovered peers",
"connected-to": "Connected to", "connected-to": "Connected to",
"connecting": "Connecting...", "connecting": "Connecting...",
@ -827,6 +830,7 @@
"emojihash-description": "A visual representation of your chat key. It will help other users recognize your profile.", "emojihash-description": "A visual representation of your chat key. It will help other users recognize your profile.",
"emojis": "Emojis", "emojis": "Emojis",
"empty-activity-center": "Your chat notifications\nwill appear here", "empty-activity-center": "Your chat notifications\nwill appear here",
"empty-card-info": "To generate new key pair, or import existent non-profile keys to the Keycard, please use the Status desktop app. If you'd like these features on mobile, feel free to upvote them and discuss them in Status community.",
"empty-chat-description": "There are no messages \nin this chat yet", "empty-chat-description": "There are no messages \nin this chat yet",
"empty-chat-description-community": "It's been quiet here for the last {{quiet-hours}}.", "empty-chat-description-community": "It's been quiet here for the last {{quiet-hours}}.",
"empty-chat-description-one-to-one": "Any messages you send here are encrypted and can only be read by you and ", "empty-chat-description-one-to-one": "Any messages you send here are encrypted and can only be read by you and ",
@ -1073,6 +1077,7 @@
"generating-mnemonic": "Generating seed phrase", "generating-mnemonic": "Generating seed phrase",
"generic-error": "Error: {{generic-error}}", "generic-error": "Error: {{generic-error}}",
"get-a-keycard": "Get a Keycard", "get-a-keycard": "Get a Keycard",
"get-keycard": "Get Keycard",
"get-started": "Get started", "get-started": "Get started",
"get-status-at": "Get Status at http://status.im", "get-status-at": "Get Status at http://status.im",
"get-stickers": "Get Stickers", "get-stickers": "Get Stickers",
@ -1132,6 +1137,7 @@
"history-nodes": "Status nodes", "history-nodes": "Status nodes",
"hit-photos-limit": "You can only add {{max-photos}} photos to your message", "hit-photos-limit": "You can only add {{max-photos}} photos to your message",
"hold-card": "Hold card to the back\n of your phone", "hold-card": "Hold card to the back\n of your phone",
"hold-phone-near-keycard": "Hold your phone near a Status Keycard",
"hold-to-post-1": "Hold", "hold-to-post-1": "Hold",
"hold-to-post-2": "to post", "hold-to-post-2": "to post",
"home": "Home", "home": "Home",
@ -1167,10 +1173,12 @@
"import-community-title": "Import a community", "import-community-title": "Import a community",
"import-from-keycard": "Import from Keycard", "import-from-keycard": "Import from Keycard",
"import-key-pair": "Import key pair", "import-key-pair": "Import key pair",
"import-key-pair-keycard": "Import profile key pair to Keycard",
"import-keypair-steps": "{{account-name}} was derived from your {{keypair-name}} key pair, which has not yet been imported to this device. To transact using this account, you will need to import the {{keypair-name}} key pair first.", "import-keypair-steps": "{{account-name}} was derived from your {{keypair-name}} key pair, which has not yet been imported to this device. To transact using this account, you will need to import the {{keypair-name}} key pair first.",
"import-keypair-to-use-account": "Import key pair to use this account", "import-keypair-to-use-account": "Import key pair to use this account",
"import-private-key": "Import private key", "import-private-key": "Import private key",
"import-private-key-info": "New addresses cannot be derived from an account imported from a private key. Import using a seed phrase if you wish to derive addresses.", "import-private-key-info": "New addresses cannot be derived from an account imported from a private key. Import using a seed phrase if you wish to derive addresses.",
"import-profile-key-pair": "Import profile key pair",
"import-to-use-derived-accounts": "Import to use derived accounts", "import-to-use-derived-accounts": "Import to use derived accounts",
"import-using-phrase": "Import using recovery phrase", "import-using-phrase": "Import using recovery phrase",
"in": "in", "in": "in",
@ -1295,6 +1303,7 @@
"jump-to": "Jump to", "jump-to": "Jump to",
"jun": "Jun", "jun": "Jun",
"K": "K", "K": "K",
"keep-card-steady": "Keep the card steady under the phone",
"keep-key": "KeepKey", "keep-key": "KeepKey",
"key": "Key", "key": "Key",
"key-managment": "Key management", "key-managment": "Key management",
@ -1324,6 +1333,7 @@
"keycard-connected-title": "Connected", "keycard-connected-title": "Connected",
"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-dont-ask-card": "Don't ask for card to sign in", "keycard-dont-ask-card": "Don't ask for card to sign in",
"keycard-empty": "Keycard is empty",
"keycard-enter-new-passcode": "Enter new passcode {{step}}/2", "keycard-enter-new-passcode": "Enter new passcode {{step}}/2",
"keycard-error-description": "Connect the card again to continue", "keycard-error-description": "Connect the card again to continue",
"keycard-error-title": "Connection lost", "keycard-error-title": "Connection lost",
@ -1478,6 +1488,7 @@
"make-admin": "Make admin", "make-admin": "Make admin",
"make-moderator": "Make moderator", "make-moderator": "Make moderator",
"make-one-it-is-easy-we-promise": "Make one, its easy, we promise!", "make-one-it-is-easy-we-promise": "Make one, its easy, we promise!",
"make-sure-keycard": "Make sure the card you scanned is a Keycard.",
"make-sure-no-camera-warning": "Make sure no camera or person can see this screen before revealing", "make-sure-no-camera-warning": "Make sure no camera or person can see this screen before revealing",
"manage-connections": "Manage connections from within Application Connections", "manage-connections": "Manage connections from within Application Connections",
"manage-keys-and-storage": "Manage keys and storage", "manage-keys-and-storage": "Manage keys and storage",
@ -1557,6 +1568,9 @@
"messages-from-contacts-only-subtitle": "Only people you added as contacts can start a new chat with you or invite you to a group", "messages-from-contacts-only-subtitle": "Only people you added as contacts can start a new chat with you or invite you to a group",
"messages-gap-warning": "Some messages might be missing", "messages-gap-warning": "Some messages might be missing",
"might-break": "Might break some ÐApps", "might-break": "Might break some ÐApps",
"migrate-key-pair-keycard": "Migrate profile key pair to Keycard",
"migrate-key-pair-keycard-default-key": "{{name}} is your default Status key pair. Migrating this key pair to Keycard will require you to use your Keycard to login to Status and to transact with the key pairs derived accounts on all synced devices.\n\nKey pair will be removed from device and stored on Keycard.",
"migrate-key-pair-keycard-info": "Re-encrypting your data with your new Keycard login method may take some time, during which you wont be able to use the app.",
"migration-successful": "Migration successful", "migration-successful": "Migration successful",
"migration-successful-text": "Account succesfully migrated to Keycard", "migration-successful-text": "Account succesfully migrated to Keycard",
"migrations-failed-content": "{{message}}\nschema version: initial {{initial-version}}, current {{current-version}}, last {{last-version}}\n\nA database error occured. Your funds and chat key are safe. Other data, like your chats and contacts, cannot be restored. \"{{erase-multiaccounts-data-button-text}}\" button, will remove all other data and allows you to access your funds and send messages.", "migrations-failed-content": "{{message}}\nschema version: initial {{initial-version}}, current {{current-version}}, last {{last-version}}\n\nA database error occured. Your funds and chat key are safe. Other data, like your chats and contacts, cannot be restored. \"{{erase-multiaccounts-data-button-text}}\" button, will remove all other data and allows you to access your funds and send messages.",
@ -1794,6 +1808,7 @@
"online-community-member": "Online", "online-community-member": "Online",
"online-now": "Online now", "online-now": "Online now",
"only-mentions": "Only @mentions", "only-mentions": "Only @mentions",
"oops-not-keycard": "Oops, this isnt a Keycard",
"oops-this-qr-does-not-contain-an-address": "Oops! This QR does not contain an address", "oops-this-qr-does-not-contain-an-address": "Oops! This QR does not contain an address",
"oops-wrong-password": "Oops, wrong password!", "oops-wrong-password": "Oops, wrong password!",
"oops-wrong-word": "Oops! Wrong word", "oops-wrong-word": "Oops! Wrong word",
@ -1824,6 +1839,7 @@
"outgoing": "Outgoing", "outgoing": "Outgoing",
"outgoing-transaction": "Outgoing transaction", "outgoing-transaction": "Outgoing transaction",
"overview": "Overview", "overview": "Overview",
"own-keycard": "Already own a Keycard?",
"own-your-crypto": "Own your crypto", "own-your-crypto": "Own your crypto",
"owner": "Owner", "owner": "Owner",
"page-camera-request-blocked": "camera requests blocked. To enable camera requests go to Settings", "page-camera-request-blocked": "camera requests blocked. To enable camera requests go to Settings",
@ -2004,6 +2020,8 @@
"re-encrypt-key": "Re-encrypt your keys", "re-encrypt-key": "Re-encrypt your keys",
"read": "Read", "read": "Read",
"read-more": "Read more", "read-more": "Read more",
"ready-keycard": "Get your Keycard ready",
"ready-to-scan": "Ready to Scan",
"rearrange-categories": "Rearrange Categories", "rearrange-categories": "Rearrange Categories",
"receive": "Receive", "receive": "Receive",
"receive-at-least": "Receive at least", "receive-at-least": "Receive at least",
@ -2058,6 +2076,7 @@
"remove-network": "Remove network", "remove-network": "Remove network",
"remove-nickname": "Remove nickname", "remove-nickname": "Remove nickname",
"remove-nickname-toast": "You have removed {{secondary-name}}'s nickname", "remove-nickname-toast": "You have removed {{secondary-name}}'s nickname",
"remove-phone-case": "Remove your phone case if you have one",
"remove-private-key-address-desc": "The account will be removed from all of your synced devices. Make sure you have a backup of your key pair or recovery phrase.", "remove-private-key-address-desc": "The account will be removed from all of your synced devices. Make sure you have a backup of your key pair or recovery phrase.",
"remove-profile-confirm-message": "All profile data will removed from device.", "remove-profile-confirm-message": "All profile data will removed from device.",
"remove-profile-message": "Remove profile from this device", "remove-profile-message": "Remove profile from this device",
@ -2166,9 +2185,11 @@
"searching-for-activity": "Searching for activity...", "searching-for-activity": "Searching for activity...",
"secret-keys-confirmation-text": "You will need them to continue to use your Keycard in case you ever lose your phone.", "secret-keys-confirmation-text": "You will need them to continue to use your Keycard in case you ever lose your phone.",
"secret-keys-confirmation-title": "Written the codes down?", "secret-keys-confirmation-title": "Written the codes down?",
"secure-wallet-card": "A secure and private cold wallet in a card format",
"security": "Security", "security": "Security",
"see-details": "See details", "see-details": "See details",
"see-it-again": "SEE IT AGAIN", "see-it-again": "SEE IT AGAIN",
"see-keycard-ready": "Lets see whats on your Keycard. Ready?",
"see-recovery-phrase-again": "See recovery phrase again", "see-recovery-phrase-again": "See recovery phrase again",
"see-sticker-set": "See the full sticker set", "see-sticker-set": "See the full sticker set",
"see-suggestions": "See suggestions", "see-suggestions": "See suggestions",
@ -2248,6 +2269,7 @@
"set-up-sync": "Set up sync", "set-up-sync": "Set up sync",
"settings": "Settings", "settings": "Settings",
"setup-group-chat": "Setup group chat", "setup-group-chat": "Setup group chat",
"setup-keycard": "Setup Keycard",
"setup-syncing": "Pair devices to sync", "setup-syncing": "Pair devices to sync",
"share": "Share", "share": "Share",
"share-account": "Share account", "share-account": "Share account",
@ -2472,6 +2494,7 @@
"time-in-mins": "{{minutes}} min", "time-in-mins": "{{minutes}} min",
"timeline": "Timeline", "timeline": "Timeline",
"tip-cap": "Tip cap", "tip-cap": "Tip cap",
"tips-scan-keycard": "Tips to scan your Keycard",
"to": "to", "to": "to",
"to-block": "Block", "to-block": "Block",
"to-capitalized": "To", "to-capitalized": "To",
@ -2626,6 +2649,7 @@
"use-as-profile-picture": "Use as profile picture", "use-as-profile-picture": "Use as profile picture",
"use-biometrics": "Use biometrics to fill in your password", "use-biometrics": "Use biometrics to fill in your password",
"use-keycard": "Use Keycard", "use-keycard": "Use Keycard",
"use-keycard-login-sign": "Use this Keycard to login and sign transactions",
"use-keycard-subtitle": "Keys will be stored on your Keycard", "use-keycard-subtitle": "Keys will be stored on your Keycard",
"use-photo": "Use Photo", "use-photo": "Use Photo",
"use-recovery-phrase": "Use recovery phrase", "use-recovery-phrase": "Use recovery phrase",
@ -2770,6 +2794,7 @@
"what-are-you-waiting-for": "What are you waiting for?", "what-are-you-waiting-for": "What are you waiting for?",
"what-changed": "What changed", "what-changed": "What changed",
"what-is-shared": "What is shared", "what-is-shared": "What is shared",
"what-to-do": "What you would like to do?",
"what-we-will-receive": "What we will receive:", "what-we-will-receive": "What we will receive:",
"what-we-wont-receive": "What we won't receive:", "what-we-wont-receive": "What we won't receive:",
"whats-on-your-mind": "Whats on your mind…", "whats-on-your-mind": "Whats on your mind…",