diff --git a/resources/images/ui2/nfc-prompt@2x.png b/resources/images/ui2/nfc-prompt@2x.png new file mode 100644 index 0000000000..c2b224cc7f Binary files /dev/null and b/resources/images/ui2/nfc-prompt@2x.png differ diff --git a/resources/images/ui2/nfc-prompt@3x.png b/resources/images/ui2/nfc-prompt@3x.png new file mode 100644 index 0000000000..3dcf935825 Binary files /dev/null and b/resources/images/ui2/nfc-prompt@3x.png differ diff --git a/resources/images/ui2/nfc-success@2x.png b/resources/images/ui2/nfc-success@2x.png new file mode 100644 index 0000000000..8a24eae441 Binary files /dev/null and b/resources/images/ui2/nfc-success@2x.png differ diff --git a/resources/images/ui2/nfc-success@3x.png b/resources/images/ui2/nfc-success@3x.png new file mode 100644 index 0000000000..242157f61b Binary files /dev/null and b/resources/images/ui2/nfc-success@3x.png differ diff --git a/src/keycard/keycard.cljs b/src/keycard/keycard.cljs index 037e721e63..7ea3692fe5 100644 --- a/src/keycard/keycard.cljs +++ b/src/keycard/keycard.cljs @@ -1,38 +1,277 @@ -(ns keycard.keycard) +(ns keycard.keycard + (:require + ["react-native" :as rn] + ["react-native-status-keycard" :default status-keycard] + [react-native.platform :as platform] + [taoensso.timbre :as log] + [utils.address :as address])) -(defprotocol Keycard - (start-nfc [this args]) - (stop-nfc [this args]) - (set-nfc-message [this args]) - (check-nfc-support [this args]) - (check-nfc-enabled [this args]) - (open-nfc-settings [this]) - (register-card-events [this args]) - (set-pairings [this args]) - (on-card-disconnected [this callback]) - (on-card-connected [this callback]) - (remove-event-listener [this event]) - (remove-event-listeners [this]) - (get-application-info [this args]) - (factory-reset [this args]) - (install-applet [this args]) - (install-cash-applet [this args]) - (init-card [this args]) - (install-applet-and-init-card [this args]) - (pair [this args]) - (generate-and-load-key [this args]) - (unblock-pin [this args]) - (verify-pin [this args]) - (change-pin [this args]) - (change-puk [this args]) - (change-pairing [this args]) - (unpair [this args]) - (delete [this args]) - (remove-key [this args]) - (remove-key-with-unpair [this args]) - (export-key [this args]) - (unpair-and-delete [this args]) - (import-keys [this args]) - (get-keys [this args]) - (sign [this args]) - (sign-typed-data [this args])) +(defonce event-emitter + (if platform/ios? + (new (.-NativeEventEmitter rn) status-keycard) + (.-DeviceEventEmitter rn))) + +(defn start-nfc + [{:keys [on-success on-failure prompt-message]}] + (log/debug "start-nfc") + (.. status-keycard + (startNFC (str prompt-message)) + (then on-success) + (catch on-failure))) + +(defn stop-nfc + [{:keys [on-success on-failure error-message]}] + (log/debug "stop-nfc") + (.. status-keycard + (stopNFC (str error-message)) + (then on-success) + (catch on-failure))) + +(defn set-nfc-message + [{:keys [on-success on-failure status-message]}] + (log/debug "set-nfc-message") + (.. status-keycard + (setNFCMessage (str status-message)) + (then on-success) + (catch on-failure))) + +(defn check-nfc-support + [{:keys [on-success]}] + (.. status-keycard + nfcIsSupported + (then on-success))) + +(defn check-nfc-enabled + [{:keys [on-success]}] + (.. status-keycard + nfcIsEnabled + (then on-success))) + +(defn open-nfc-settings + [] + (.openNfcSettings status-keycard)) + +(defn remove-event-listeners + [] + (doseq [event ["keyCardOnConnected" "keyCardOnDisconnected" "keyCardOnNFCUserCancelled" + "keyCardOnNFCTimeout"]] + (.removeAllListeners ^js event-emitter event))) + +(defn remove-event-listener + [^js event] + (when event + (.remove event))) + +(defn on-card-connected + [callback] + (.addListener ^js event-emitter "keyCardOnConnected" callback)) + +(defn on-card-disconnected + [callback] + (.addListener ^js event-emitter "keyCardOnDisconnected" callback)) + +(defn on-nfc-user-cancelled + [callback] + (.addListener ^js event-emitter "keyCardOnNFCUserCancelled" callback)) + +(defn on-nfc-timeout + [callback] + (.addListener ^js event-emitter "keyCardOnNFCTimeout" callback)) + +(defn on-nfc-enabled + [callback] + (.addListener ^js event-emitter "keyCardOnNFCEnabled" callback)) + +(defn on-nfc-disabled + [callback] + (.addListener ^js event-emitter "keyCardOnNFCDisabled" callback)) + +(defn set-pairings + [pairings] + (.. status-keycard (setPairings (clj->js (or pairings {}))))) + +(defn get-application-info + [{:keys [on-success on-failure]}] + (.. status-keycard + (getApplicationInfo) + (then (fn [response] + (let [info (-> response + (js->clj :keywordize-keys true) + (update :key-uid address/normalized-hex))] + (on-success info)))) + (catch on-failure))) + +(defn factory-reset + [{:keys [on-success on-failure]}] + (.. status-keycard + (factoryReset) + (then (fn [response] + (let [info (-> response + (js->clj :keywordize-keys true) + (update :key-uid address/normalized-hex))] + (on-success info)))) + (catch on-failure))) + +(defn install-applet + [{:keys [on-success on-failure]}] + (.. status-keycard + installApplet + (then on-success) + (catch on-failure))) + +(defn install-cash-applet + [{:keys [on-success on-failure]}] + (.. status-keycard + installCashApplet + (then on-success) + (catch on-failure))) + +(defn init-card + [{:keys [pin on-success on-failure]}] + (.. status-keycard + (init pin) + (then on-success) + (catch on-failure))) + +(defn install-applet-and-init-card + [{:keys [pin on-success on-failure]}] + (.. status-keycard + (installAppletAndInitCard pin) + (then on-success) + (catch on-failure))) + +(defn pair + [{:keys [password on-success on-failure]}] + (when password + (.. status-keycard + (pair password) + (then on-success) + (catch on-failure)))) + +(defn generate-and-load-key + [{:keys [mnemonic pin on-success on-failure]}] + (.. status-keycard + (generateAndLoadKey mnemonic pin) + (then on-success) + (catch on-failure))) + +(defn unblock-pin + [{:keys [puk new-pin on-success on-failure]}] + (when (and new-pin puk) + (.. status-keycard + (unblockPin puk new-pin) + (then on-success) + (catch on-failure)))) + +(defn verify-pin + [{:keys [pin on-success on-failure]}] + (when (not-empty pin) + (.. status-keycard + (verifyPin pin) + (then on-success) + (catch on-failure)))) + +(defn change-pin + [{:keys [current-pin new-pin on-success on-failure]}] + (when (and current-pin new-pin) + (.. status-keycard + (changePin current-pin new-pin) + (then on-success) + (catch on-failure)))) + +(defn change-puk + [{:keys [pin puk on-success on-failure]}] + (when (and pin puk) + (.. status-keycard + (changePUK pin puk) + (then on-success) + (catch on-failure)))) + +(defn change-pairing + [{:keys [pin pairing on-success on-failure]}] + (when (and pin pairing) + (.. status-keycard + (changePairingPassword pin pairing) + (then on-success) + (catch on-failure)))) + +(defn unpair + [{:keys [pin on-success on-failure]}] + (when pin + (.. status-keycard + (unpair pin) + (then on-success) + (catch on-failure)))) + +(defn delete + [{:keys [on-success on-failure]}] + (.. status-keycard + (delete) + (then on-success) + (catch on-failure))) + +(defn remove-key + [{:keys [pin on-success on-failure]}] + (.. status-keycard + (removeKey pin) + (then on-success) + (catch on-failure))) + +(defn remove-key-with-unpair + [{:keys [pin on-success on-failure]}] + (.. status-keycard + (removeKeyWithUnpair pin) + (then on-success) + (catch on-failure))) + +(defn export-key + [{:keys [pin path on-success on-failure]}] + (.. status-keycard + (exportKeyWithPath pin path) + (then on-success) + (catch on-failure))) + +(defn unpair-and-delete + [{:keys [pin on-success on-failure]}] + (when (not-empty pin) + (.. status-keycard + (unpairAndDelete pin) + (then on-success) + (catch on-failure)))) + +(defn import-keys + [{:keys [pin on-success on-failure]}] + (when (not-empty pin) + (.. status-keycard + (importKeys pin) + (then on-success) + (catch on-failure)))) + +(defn get-keys + [{:keys [pin on-success on-failure]}] + (when (not-empty pin) + (.. status-keycard + (getKeys pin) + (then on-success) + (catch on-failure)))) + +(defn sign + [{pin :pin path :path card-hash :hash on-success :on-success on-failure :on-failure}] + (when (and pin card-hash) + (if path + (.. status-keycard + (signWithPath pin path card-hash) + (then on-success) + (catch on-failure)) + (.. status-keycard + (sign pin card-hash) + (then on-success) + (catch on-failure))))) + +(defn sign-typed-data + [{card-hash :hash on-success :on-success on-failure :on-failure}] + (when card-hash + (.. status-keycard + (signPinless card-hash) + (then on-success) + (catch on-failure)))) diff --git a/src/keycard/real_keycard.cljs b/src/keycard/real_keycard.cljs deleted file mode 100644 index 7250479e69..0000000000 --- a/src/keycard/real_keycard.cljs +++ /dev/null @@ -1,365 +0,0 @@ -(ns keycard.real-keycard - (:require - ["react-native" :as rn] - ["react-native-status-keycard" :default status-keycard] - [keycard.keycard :as keycard] - [react-native.platform :as platform] - [taoensso.timbre :as log] - [utils.address :as address])) - -(defonce event-emitter - (if platform/ios? - (new (.-NativeEventEmitter rn) status-keycard) - (.-DeviceEventEmitter rn))) - -(defonce active-listeners (atom [])) - -(defn start-nfc - [{:keys [on-success on-failure prompt-message]}] - (log/debug "start-nfc") - (.. status-keycard - (startNFC (str prompt-message)) - (then on-success) - (catch on-failure))) - -(defn stop-nfc - [{:keys [on-success on-failure error-message]}] - (log/debug "stop-nfc") - (.. status-keycard - (stopNFC (str error-message)) - (then on-success) - (catch on-failure))) - -(defn set-nfc-message - [{:keys [on-success on-failure status-message]}] - (log/debug "set-nfc-message") - (.. status-keycard - (setNFCMessage (str status-message)) - (then on-success) - (catch on-failure))) - -(defn check-nfc-support - [{:keys [on-success]}] - (.. status-keycard - nfcIsSupported - (then on-success))) - -(defn check-nfc-enabled - [{:keys [on-success]}] - (.. status-keycard - nfcIsEnabled - (then on-success))) - -(defn open-nfc-settings - [] - (.openNfcSettings status-keycard)) - -(defn remove-event-listeners - [] - (doseq [event ["keyCardOnConnected" "keyCardOnDisconnected" "keyCardOnNFCUserCancelled" - "keyCardOnNFCTimeout"]] - (.removeAllListeners ^js event-emitter event))) - -(defn remove-event-listener - [^js event] - (.remove event)) - -(defn on-card-connected - [callback] - (.addListener ^js event-emitter "keyCardOnConnected" callback)) - -(defn on-card-disconnected - [callback] - (.addListener ^js event-emitter "keyCardOnDisconnected" callback)) - -(defn on-nfc-user-cancelled - [callback] - (.addListener ^js event-emitter "keyCardOnNFCUserCancelled" callback)) - -(defn on-nfc-timeout - [callback] - (.addListener ^js event-emitter "keyCardOnNFCTimeout" callback)) - -(defn on-nfc-enabled - [callback] - (.addListener ^js event-emitter "keyCardOnNFCEnabled" callback)) - -(defn on-nfc-disabled - [callback] - (.addListener ^js event-emitter "keyCardOnNFCDisabled" callback)) - -(defn set-pairings - [{:keys [pairings]}] - (.. status-keycard (setPairings (clj->js (or pairings {}))))) - -(defn register-card-events - [args] - (doseq [listener @active-listeners] - (remove-event-listener listener)) - (reset! active-listeners - [(on-card-connected (:on-card-connected args)) - (on-card-disconnected (:on-card-disconnected args)) - (on-nfc-user-cancelled (:on-nfc-user-cancelled args)) - (on-nfc-timeout (:on-nfc-timeout args)) - (on-nfc-enabled (:on-nfc-enabled args)) - (on-nfc-disabled (:on-nfc-disabled args))])) - -(defn get-application-info - [{:keys [on-success on-failure]}] - - (.. status-keycard - (getApplicationInfo) - (then (fn [response] - (let [info (-> response - (js->clj :keywordize-keys true) - (update :key-uid address/normalized-hex))] - (on-success info)))) - (catch on-failure))) - -(defn factory-reset - [{:keys [on-success on-failure]}] - (.. status-keycard - (factoryReset) - (then (fn [response] - (let [info (-> response - (js->clj :keywordize-keys true) - (update :key-uid address/normalized-hex))] - (on-success info)))) - (catch on-failure))) - -(defn install-applet - [{:keys [on-success on-failure]}] - (.. status-keycard - installApplet - (then on-success) - (catch on-failure))) - -(defn install-cash-applet - [{:keys [on-success on-failure]}] - (.. status-keycard - installCashApplet - (then on-success) - (catch on-failure))) - -(defn init-card - [{:keys [pin on-success on-failure]}] - (.. status-keycard - (init pin) - (then on-success) - (catch on-failure))) - -(defn install-applet-and-init-card - [{:keys [pin on-success on-failure]}] - (.. status-keycard - (installAppletAndInitCard pin) - (then on-success) - (catch on-failure))) - -(defn pair - [{:keys [password on-success on-failure]}] - (when password - (.. status-keycard - (pair password) - (then on-success) - (catch on-failure)))) - -(defn generate-and-load-key - [{:keys [mnemonic pin on-success on-failure]}] - (.. status-keycard - (generateAndLoadKey mnemonic pin) - (then on-success) - (catch on-failure))) - -(defn unblock-pin - [{:keys [puk new-pin on-success on-failure]}] - (when (and new-pin puk) - (.. status-keycard - (unblockPin puk new-pin) - (then on-success) - (catch on-failure)))) - -(defn verify-pin - [{:keys [pin on-success on-failure]}] - (when (not-empty pin) - (.. status-keycard - (verifyPin pin) - (then on-success) - (catch on-failure)))) - -(defn change-pin - [{:keys [current-pin new-pin on-success on-failure]}] - (when (and current-pin new-pin) - (.. status-keycard - (changePin current-pin new-pin) - (then on-success) - (catch on-failure)))) - -(defn change-puk - [{:keys [pin puk on-success on-failure]}] - (when (and pin puk) - (.. status-keycard - (changePUK pin puk) - (then on-success) - (catch on-failure)))) - -(defn change-pairing - [{:keys [pin pairing on-success on-failure]}] - (when (and pin pairing) - (.. status-keycard - (changePairingPassword pin pairing) - (then on-success) - (catch on-failure)))) - -(defn unpair - [{:keys [pin on-success on-failure]}] - (when pin - (.. status-keycard - (unpair pin) - (then on-success) - (catch on-failure)))) - -(defn delete - [{:keys [on-success on-failure]}] - (.. status-keycard - (delete) - (then on-success) - (catch on-failure))) - -(defn remove-key - [{:keys [pin on-success on-failure]}] - (.. status-keycard - (removeKey pin) - (then on-success) - (catch on-failure))) - -(defn remove-key-with-unpair - [{:keys [pin on-success on-failure]}] - (.. status-keycard - (removeKeyWithUnpair pin) - (then on-success) - (catch on-failure))) - -(defn export-key - [{:keys [pin path on-success on-failure]}] - (.. status-keycard - (exportKeyWithPath pin path) - (then on-success) - (catch on-failure))) - -(defn unpair-and-delete - [{:keys [pin on-success on-failure]}] - (when (not-empty pin) - (.. status-keycard - (unpairAndDelete pin) - (then on-success) - (catch on-failure)))) - -(defn import-keys - [{:keys [pin on-success on-failure]}] - (when (not-empty pin) - (.. status-keycard - (importKeys pin) - (then on-success) - (catch on-failure)))) - -(defn get-keys - [{:keys [pin on-success on-failure]}] - (when (not-empty pin) - (.. status-keycard - (getKeys pin) - (then on-success) - (catch on-failure)))) - -(defn sign - [{pin :pin path :path card-hash :hash on-success :on-success on-failure :on-failure}] - (when (and pin card-hash) - (if path - (.. status-keycard - (signWithPath pin path card-hash) - (then on-success) - (catch on-failure)) - (.. status-keycard - (sign pin card-hash) - (then on-success) - (catch on-failure))))) - -(defn sign-typed-data - [{card-hash :hash on-success :on-success on-failure :on-failure}] - (when card-hash - (.. status-keycard - (signPinless card-hash) - (then on-success) - (catch on-failure)))) - -(defrecord RealKeycard [] - keycard/Keycard - (keycard/start-nfc [_this args] - (start-nfc args)) - (keycard/stop-nfc [_this args] - (stop-nfc args)) - (keycard/set-nfc-message [_this args] - (set-nfc-message args)) - (keycard/check-nfc-support [_this args] - (check-nfc-support args)) - (keycard/check-nfc-enabled [_this args] - (check-nfc-enabled args)) - (keycard/open-nfc-settings [_this] - (open-nfc-settings)) - (keycard/register-card-events [_this args] - (register-card-events args)) - (keycard/on-card-connected [_this callback] - (on-card-connected callback)) - (keycard/on-card-disconnected [_this callback] - (on-card-disconnected callback)) - (keycard/remove-event-listener [_this event] - (remove-event-listener event)) - (keycard/remove-event-listeners [_this] - (remove-event-listeners)) - (keycard/set-pairings [_this args] - (set-pairings args)) - (keycard/get-application-info [_this args] - (get-application-info args)) - (keycard/factory-reset [_this args] - (factory-reset args)) - (keycard/install-applet [_this args] - (install-applet args)) - (keycard/install-cash-applet [_this args] - (install-cash-applet args)) - (keycard/init-card [_this args] - (init-card args)) - (keycard/install-applet-and-init-card [_this args] - (install-applet-and-init-card args)) - (keycard/pair [_this args] - (pair args)) - (keycard/generate-and-load-key [_this args] - (generate-and-load-key args)) - (keycard/unblock-pin [_this args] - (unblock-pin args)) - (keycard/verify-pin [_this args] - (verify-pin args)) - (keycard/change-pin [_this args] - (change-pin args)) - (keycard/change-puk [_this args] - (change-puk args)) - (keycard/change-pairing [_this args] - (change-pairing args)) - (keycard/unpair [_this args] - (unpair args)) - (keycard/delete [_this args] - (delete args)) - (keycard/remove-key [_this args] - (remove-key args)) - (keycard/remove-key-with-unpair [_this args] - (remove-key-with-unpair args)) - (keycard/export-key [_this args] - (export-key args)) - (keycard/unpair-and-delete [_this args] - (unpair-and-delete args)) - (keycard/import-keys [_this args] - (import-keys args)) - (keycard/get-keys [_this args] - (get-keys args)) - (keycard/sign [_this args] - (sign args)) - (keycard/sign-typed-data [_this args] - (sign-typed-data args))) diff --git a/src/legacy/status_im/utils/keychain/core.cljs b/src/legacy/status_im/utils/keychain/core.cljs deleted file mode 100644 index 5d95de3c27..0000000000 --- a/src/legacy/status_im/utils/keychain/core.cljs +++ /dev/null @@ -1,58 +0,0 @@ -(ns legacy.status-im.utils.keychain.core - (:require - [oops.core :as oops] - [re-frame.core :as re-frame] - [react-native.keychain :as keychain] - [taoensso.timbre :as log] - [utils.re-frame :as rf])) - -(defn- whisper-key-name - [address] - (str address "-whisper")) - -(re-frame/reg-fx - :keychain/get-keycard-keys - (fn [[key-uid callback]] - (keychain/get-credentials - key-uid - (fn [encryption-key-data] - (if encryption-key-data - (keychain/get-credentials - (whisper-key-name key-uid) - (fn [whisper-key-data] - (if whisper-key-data - (callback [(oops/oget encryption-key-data "password") - (oops/oget whisper-key-data "password")]) - (callback nil)))) - (callback nil)))))) - -(re-frame/reg-fx - :keychain/save-keycard-keys - (fn [[key-uid encryption-public-key whisper-private-key]] - (keychain/save-credentials - key-uid - key-uid - encryption-public-key - #(when-not % - (log/error - (str "Error while saving encryption-public-key")))) - (keychain/save-credentials - (whisper-key-name key-uid) - key-uid - whisper-private-key - #(when-not % - (log/error - (str "Error while saving whisper-private-key")))))) - -(rf/defn get-keycard-keys - [_ key-uid] - {:keychain/get-keycard-keys - [key-uid - #(re-frame/dispatch - [:multiaccounts.login.callback/get-keycard-keys-success key-uid %])]}) - -(rf/defn save-keycard-keys - [_ key-uid encryption-public-key whisper-private-key] - {:keychain/save-keycard-keys [key-uid - encryption-public-key - whisper-private-key]}) diff --git a/src/mocks/js_dependencies.cljs b/src/mocks/js_dependencies.cljs index fb0a2ddf9a..a1b8aee2c0 100644 --- a/src/mocks/js_dependencies.cljs +++ b/src/mocks/js_dependencies.cljs @@ -110,8 +110,11 @@ (def status-keycard #js {:default #js - {:nfcIsSupported (fn [] #js {:then identity}) - :nfcIsEnabled (fn [] #js {:then identity})}}) + {:nfcIsSupported (fn [] #js {:then identity}) + :nfcIsEnabled (fn [] #js {:then identity}) + :getApplicationInfo (fn [] #js {:then identity}) + :getKeys (fn [] #js {:then identity}) + :setPairings (fn [] #js {:then identity})}}) (def snoopy #js {:default #js {}}) (def snoopy-filter #js {:default #js {}}) diff --git a/src/native_module/core.cljs b/src/native_module/core.cljs index 4f5edc4893..6587d2efc9 100644 --- a/src/native_module/core.cljs +++ b/src/native_module/core.cljs @@ -87,31 +87,6 @@ config #(callback (types/json->clj %)))) -(defn save-multiaccount-and-login-with-keycard - "NOTE: chat-key is a whisper private key sent from keycard" - [key-uid multiaccount-data password settings config accounts-data chat-key] - (log/debug "[native-module] save-account-and-login-with-keycard") - (init-keystore - key-uid - #(.saveAccountAndLoginWithKeycard - ^js (account-manager) - multiaccount-data - password - settings - config - accounts-data - chat-key))) - -(defn login-with-config - "NOTE: beware, the password has to be sha3 hashed" - [key-uid account-data hashed-password config] - (log/debug "[native-module] loginWithConfig") - (clear-web-data) - (let [config (if config (types/clj->json config) "")] - (init-keystore - key-uid - #(.loginWithConfig ^js (account-manager) account-data hashed-password config)))) - (defn login-account "NOTE: beware, the password has to be sha3 hashed" [{:keys [keyUid] :as request}] @@ -153,19 +128,6 @@ (clear-web-data) (.logout ^js (account-manager))) -(defn multiaccount-load-account - "NOTE: beware, the password has to be sha3 hashed - - this function is used after storing an account when you still want to - derive accounts from it, because saving an account flushes the loaded keys - from memory" - [address hashed-password callback] - (log/debug "[native-module] multiaccount-load-account") - (.multiAccountLoadAccount ^js (account-manager) - (types/clj->json {:address address - :password hashed-password}) - callback)) - (defn multiaccount-derive-addresses "NOTE: this should be named derive-accounts this only derive addresses, they still need to be stored @@ -179,38 +141,6 @@ :paths paths}) callback))) -(defn multiaccount-store-account - "NOTE: beware, the password has to be sha3 hashed - - this stores the account and flush keys in memory so - in order to also store derived accounts like initial wallet - and chat accounts, you need to load the account again with - `multiaccount-load-account` before using `multiaccount-store-derived` - and the id of the account stored will have changed" - [account-id key-uid hashed-password callback] - (log/debug "[native-module] multiaccount-store-account") - (when (status) - (init-keystore - key-uid - #(.multiAccountStoreAccount ^js (account-manager) - (types/clj->json {:accountID account-id - :password hashed-password}) - callback)))) - -(defn multiaccount-store-derived - "NOTE: beware, the password has to be sha3 hashed" - [account-id key-uid paths hashed-password callback] - (log/debug "[native-module] multiaccount-store-derived" - "account-id" - account-id) - (init-keystore - key-uid - #(.multiAccountStoreDerived ^js (account-manager) - (types/clj->json {:accountID account-id - :paths paths - :password hashed-password}) - callback))) - (defn multiaccount-generate-and-derive-addresses "used to generate multiple multiaccounts for onboarding NOTE: nothing is saved so you will need to use @@ -234,37 +164,12 @@ :Bip39Passphrase password}) callback)) -(defn multiaccount-import-private-key - [private-key callback] - (log/debug "[native-module] multiaccount-import-private-key") - (.multiAccountImportPrivateKey ^js (account-manager) - (types/clj->json {:privateKey private-key}) - callback)) - (defn verify "NOTE: beware, the password has to be sha3 hashed" [address hashed-password callback] (log/debug "[native-module] verify") (.verify ^js (account-manager) address hashed-password callback)) -(defn verify-database-password - "NOTE: beware, the password has to be sha3 hashed" - [key-uid hashed-password callback] - (log/debug "[native-module] verify-database-password") - (.verifyDatabasePassword ^js (account-manager) key-uid hashed-password callback)) - -(defn login-with-keycard - [{:keys [key-uid multiaccount-data password chat-key node-config]}] - (log/debug "[native-module] login-with-keycard") - (clear-web-data) - (init-keystore - key-uid - #(.loginWithKeycard ^js (account-manager) - multiaccount-data - password - chat-key - (types/clj->json node-config)))) - (defn set-soft-input-mode [mode] (log/debug "[native-module] set-soft-input-mode") diff --git a/src/quo/components/pin_input/pin/view.cljs b/src/quo/components/pin_input/pin/view.cljs new file mode 100644 index 0000000000..47c342a207 --- /dev/null +++ b/src/quo/components/pin_input/pin/view.cljs @@ -0,0 +1,43 @@ +(ns quo.components.pin-input.pin.view + (:require [quo.foundations.colors :as colors] + quo.theme + [react-native.core :as rn])) + +(defn view + [{:keys [theme state blur?]}] + (let [app-theme (quo.theme/use-theme) + theme (or theme app-theme)] + [rn/view {:style {:width 36 :height 36 :align-items :center :justify-content :center}} + (case state + :active + [rn/view + {:style {:width 16 + :height 16 + :border-radius 8 + :background-color (if blur? + colors/white-opa-20 + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))}}] + :filled + [rn/view + {:style {:width 16 + :height 16 + :border-radius 8 + :background-color (if blur? + colors/white + (colors/theme-colors colors/neutral-100 colors/white theme))}}] + :error + [rn/view + {:style {:width 16 + :height 16 + :border-radius 8 + :background-color (if blur? + colors/danger-60 + (colors/theme-colors colors/danger-50 colors/danger-60 theme))}}] + [rn/view + {:style + {:width 12 + :height 12 + :border-radius 6 + :background-color (if blur? + colors/white-opa-20 + (colors/theme-colors colors/neutral-40 colors/neutral-50 theme))}}])])) diff --git a/src/quo/components/pin_input/view.cljs b/src/quo/components/pin_input/view.cljs new file mode 100644 index 0000000000..0ae264007f --- /dev/null +++ b/src/quo/components/pin_input/view.cljs @@ -0,0 +1,27 @@ +(ns quo.components.pin-input.view + (:require [quo.components.markdown.text :as text] + [quo.components.pin-input.pin.view :as pin] + [quo.foundations.colors :as colors] + quo.theme + [react-native.core :as rn])) + +(defn view + [{:keys [number-of-pins number-of-filled-pins error? info] + :or {number-of-pins 6 number-of-filled-pins 0}}] + (let [theme (quo.theme/use-theme)] + [rn/view {:style {:align-items :center}} + [rn/view {:style {:flex-direction :row}} + (for [i (range 1 (inc number-of-pins))] + ^{:key i} + [pin/view + {:state (cond + error? :error + (<= i number-of-filled-pins) :filled + (= i (inc number-of-filled-pins)) :active)}])] + (when info + [text/text + {:style {:color (if error? + (colors/theme-colors colors/danger-50 colors/danger-60 theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))} + :size :paragraph-2} + info])])) diff --git a/src/quo/core.cljs b/src/quo/core.cljs index e04a5cca98..9ea0c9a71b 100644 --- a/src/quo/core.cljs +++ b/src/quo/core.cljs @@ -120,6 +120,7 @@ quo.components.overlay.view quo.components.password.password-tips.view quo.components.password.tips.view + quo.components.pin-input.view quo.components.profile.collectible-list-item.view quo.components.profile.collectible.view quo.components.profile.expanded-collectible.view @@ -320,6 +321,9 @@ (def keyboard-key quo.components.numbered-keyboard.keyboard-key.view/view) (def numbered-keyboard quo.components.numbered-keyboard.numbered-keyboard.view/view) +;;;; PIN input +(def pin-input quo.components.pin-input.view/view) + ;;;; Links (def internal-link-card quo.components.links.internal-link-card.view/view) (def link-preview quo.components.links.link-preview.view/view) diff --git a/src/status_im/common/bottom_sheet/view.cljs b/src/status_im/common/bottom_sheet/view.cljs index a941d9bad0..c4d1b1abee 100644 --- a/src/status_im/common/bottom_sheet/view.cljs +++ b/src/status_im/common/bottom_sheet/view.cljs @@ -63,8 +63,10 @@ (defn view [{:keys [hide? insets]} {:keys [content selected-item padding-bottom-override border-radius on-close shell? - gradient-cover? customization-color hide-handle? blur-radius] - :or {border-radius 12}}] + gradient-cover? customization-color hide-handle? blur-radius + hide-on-background-press?] + :or {border-radius 12 + hide-on-background-press? true}}] (let [theme (quo.theme/use-theme) {window-height :height} (rn/get-window) [sheet-height set-sheet-height] (rn/use-state 0) @@ -119,7 +121,7 @@ :on-layout handle-layout-height} ;; backdrop [rn/pressable - {:on-press #(rf/dispatch [:hide-bottom-sheet]) + {:on-press #(when hide-on-background-press? (rf/dispatch [:hide-bottom-sheet])) :style {:flex 1}} [reanimated/view {:style (reanimated/apply-animations-to-style diff --git a/src/status_im/common/keychain/events.cljs b/src/status_im/common/keychain/events.cljs index e441f71bbc..3686b96a90 100644 --- a/src/status_im/common/keychain/events.cljs +++ b/src/status_im/common/keychain/events.cljs @@ -144,6 +144,10 @@ (fn [_ [opts]] {:keychain/save-password-and-auth-method opts})) +(defn- whisper-key-name + [address] + (str address "-whisper")) + ;; NOTE: migrating the plaintext password in the keychain ;; with the hashed one. Added due to the sync onboarding ;; flow, where the password arrives already hashed. @@ -151,17 +155,38 @@ :keychain/password-hash-migration (fn [{:keys [key-uid callback] :or {callback identity}}] - (-> (get-password-migration! key-uid identity) - (.then (fn [migrated?] - (if migrated? - (callback) - (-> (get-user-password! key-uid identity) - (.then security/hash-masked-password) - (.then #(save-user-password! key-uid %)) - (.then #(save-password-migration! key-uid)) - (.then callback))))) - (.catch (fn [err] - (log/error "Failed to migrate the keychain password" - {:error err - :key-uid key-uid - :event :keychain/password-hash-migration})))))) + (keychain/get-credentials + (whisper-key-name key-uid) + (fn [whisper-key-data] + (if whisper-key-data + (callback) ;; we don't need to migrate keycard password + (-> (get-password-migration! key-uid identity) + (.then (fn [migrated?] + (if migrated? + (callback) + (-> (get-user-password! key-uid identity) + (.then security/hash-masked-password) + (.then #(save-user-password! key-uid %)) + (.then #(save-password-migration! key-uid)) + (.then callback))))) + (.catch (fn [err] + (log/error "Failed to migrate the keychain password" + {:error err + :key-uid key-uid + :event :keychain/password-hash-migration}))))))))) + +(re-frame/reg-fx + :keychain/get-keycard-keys + (fn [[key-uid callback]] + (keychain/get-credentials + key-uid + (fn [encryption-key-data] + (if encryption-key-data + (keychain/get-credentials + (whisper-key-name key-uid) + (fn [whisper-key-data] + (if whisper-key-data + (callback [(oops/oget encryption-key-data "password") + (oops/oget whisper-key-data "password")]) + (callback nil)))) + (callback nil)))))) diff --git a/src/status_im/common/resources.cljs b/src/status_im/common/resources.cljs index 6d6013f8e6..14eacd856d 100644 --- a/src/status_im/common/resources.cljs +++ b/src/status_im/common/resources.cljs @@ -25,7 +25,9 @@ :invite-friends (js/require "../resources/images/ui2/invite-friends.png") :transaction-progress (js/require "../resources/images/ui2/transaction-progress.png") :welcome-illustration (js/require "../resources/images/ui2/welcome_illustration.png") - :notifications (js/require "../resources/images/ui2/notifications.png")}) + :notifications (js/require "../resources/images/ui2/notifications.png") + :nfc-prompt (js/require "../resources/images/ui2/nfc-prompt.png") + :nfc-success (js/require "../resources/images/ui2/nfc-success.png")}) (def ui-themed {:angry-man diff --git a/src/status_im/common/standard_authentication/events.cljs b/src/status_im/common/standard_authentication/events.cljs index 7c2580a931..f29d139893 100644 --- a/src/status_im/common/standard_authentication/events.cljs +++ b/src/status_im/common/standard_authentication/events.cljs @@ -10,11 +10,14 @@ (defn authorize [{:keys [db]} [args]] - (let [key-uid (get-in db [:profile/profile :key-uid])] + (let [key-uid (get-in db [:profile/profile :key-uid]) + keycard? (get-in db [:profile/profile :keycard-pairing])] {:fx [[:effects.biometric/check-if-available {:key-uid key-uid :on-success #(rf/dispatch [:standard-auth/authorize-with-biometric args]) - :on-fail #(rf/dispatch [:standard-auth/authorize-with-password args])}]]})) + :on-fail (if keycard? + #(rf/dispatch [:standard-auth/authorize-with-keycard args]) + #(rf/dispatch [:standard-auth/authorize-with-password args]))}]]})) (schema/=> authorize events-schema/?authorize) (rf/reg-event-fx :standard-auth/authorize authorize) @@ -45,8 +48,11 @@ (defn on-biometric-success [{:keys [db]} [on-auth-success]] - (let [key-uid (get-in db [:profile/profile :key-uid])] - {:fx [[:keychain/get-user-password [key-uid on-auth-success]] + (let [key-uid (get-in db [:profile/profile :key-uid]) + keycard? (get-in db [:profile/profile :keycard-pairing])] + {:fx [(if keycard? + [:keychain/get-keycard-keys [key-uid on-auth-success]] + [:keychain/get-user-password [key-uid on-auth-success]]) [:dispatch [:standard-auth/set-success true]] [:dispatch [:standard-auth/reset-login-password]]]})) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 2b39d36ce9..e31b233f56 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -126,6 +126,7 @@ (def ^:const profile-pictures-visibility-none 3) (def ^:const min-password-length 6) +(def ^:const pincode-length 6) (def ^:const new-password-min-length 10) (def ^:const max-group-chat-participants 20) (def ^:const max-group-chat-name-length 24) diff --git a/src/status_im/contexts/keycard/effects.cljs b/src/status_im/contexts/keycard/effects.cljs new file mode 100644 index 0000000000..c831984ecc --- /dev/null +++ b/src/status_im/contexts/keycard/effects.cljs @@ -0,0 +1,111 @@ +(ns status-im.contexts.keycard.effects + (:require [keycard.keycard :as keycard] + [native-module.core :as native-module] + [react-native.async-storage :as async-storage] + [react-native.platform :as platform] + [status-im.contexts.profile.config :as profile.config] + [taoensso.timbre :as log] + [utils.re-frame :as rf] + [utils.transforms :as transforms])) + +(defonce ^:private active-listeners (atom [])) + +(defn register-card-events + [] + (doseq [listener @active-listeners] + (keycard/remove-event-listener listener)) + (reset! active-listeners + [(keycard/on-card-connected #(rf/dispatch [:keycard/on-card-connected])) + (keycard/on-card-disconnected #(rf/dispatch [:keycard/on-card-disconnected])) + (when platform/ios? + (keycard/on-nfc-user-cancelled #(rf/dispatch [:keycard.ios/on-nfc-user-cancelled]))) + (when platform/ios? + (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-disabled #(rf/dispatch [:keycard/on-check-nfc-enabled-success false]))])) +(rf/reg-fx :effects.keycard/register-card-events register-card-events) + +(defn check-nfc-enabled + [] + (log/debug "[keycard] check-nfc-enabled") + (keycard/check-nfc-enabled + {:on-success + (fn [response] + (log/debug "[keycard response] check-nfc-enabled") + (rf/dispatch [:keycard/on-check-nfc-enabled-success response]))})) +(rf/reg-fx :effects.keycard/check-nfc-enabled check-nfc-enabled) + +(rf/reg-fx + :effects.keycard.ios/start-nfc + (fn [args] + (log/debug "fx start-nfc") + (keycard/start-nfc args))) + +(rf/reg-fx + :effects.keycard.ios/stop-nfc + (fn [args] + (log/debug "fx stop-nfc") + (keycard/stop-nfc args))) + +(defn- error-object->map + [^js object] + {:code (.-code object) + :error (.-message object)}) + +(defn get-application-info + [{:keys [on-success on-failure] :as args}] + (log/debug "[keycard] get-application-info") + (keycard/get-application-info + (assoc + args + :on-success + (fn [response] + (log/debug "[keycard response succ] get-application-info") + (when on-success + (on-success response))) + :on-failure + (fn [response] + (log/error "[keycard response fail] get-application-info") + (when on-failure + (on-failure (error-object->map response))))))) +(rf/reg-fx :effects.keycard/get-application-info get-application-info) + +(defn get-keys + [{:keys [on-success on-failure] :as args}] + (log/debug "[keycard] get-keys") + (keycard/get-keys + (assoc + args + :on-success + (fn [response] + (log/debug "[keycard response succ] get-keys") + (when on-success + (on-success (transforms/js->clj response)))) + :on-failure + (fn [response] + (log/warn "[keycard response fail] get-keys" + (error-object->map response)) + (when on-failure + (on-failure (error-object->map response))))))) +(rf/reg-fx :effects.keycard/get-keys get-keys) + +(defn login + [{:keys [key-uid password whisper-private-key]}] + (native-module/login-account + (assoc (profile.config/login) + :keyUid key-uid + :password password + :keycardWhisperPrivateKey whisper-private-key))) +(rf/reg-fx :effects.keycard/login-with-keycard login) + +(defn retrieve-pairings + [] + (async-storage/get-item + "status-keycard-pairings" + #(rf/dispatch [:keycard/on-retrieve-pairings-success %]))) +(rf/reg-fx :effects.keycard/retrieve-pairings retrieve-pairings) + +(defn set-pairing-to-keycard + [pairings] + (keycard/set-pairings pairings)) +(rf/reg-fx :effects.keycard/set-pairing-to-keycard set-pairing-to-keycard) diff --git a/src/status_im/contexts/keycard/events.cljs b/src/status_im/contexts/keycard/events.cljs new file mode 100644 index 0000000000..bad13fbf65 --- /dev/null +++ b/src/status_im/contexts/keycard/events.cljs @@ -0,0 +1,57 @@ +(ns status-im.contexts.keycard.events + (:require [re-frame.core :as rf] + status-im.contexts.keycard.login.events + status-im.contexts.keycard.pin.events + status-im.contexts.keycard.sheet.events + [taoensso.timbre :as log])) + +(rf/reg-event-fx :keycard/on-check-nfc-enabled-success + (fn [{:keys [db]} [nfc-enabled?]] + {:db (assoc-in db [:keycard :nfc-enabled?] nfc-enabled?)})) + +(rf/reg-event-fx :keycard.ios/on-nfc-user-cancelled + (fn [{:keys [db]}] + (log/debug "[keycard] nfc user cancelled") + {:db (assoc-in db [:keycard :pin :status] nil) + :fx [(when-let [on-nfc-cancelled-event-vector (get-in db [:keycard :on-nfc-cancelled-event-vector])] + [:dispatch on-nfc-cancelled-event-vector])]})) + +(rf/reg-event-fx :keycard/on-card-connected + (fn [{:keys [db]} _] + (log/debug "[keycard] card globally connected") + {:db (assoc-in db [:keycard :card-connected?] true) + :fx [(when-let [event (get-in db [:keycard :on-card-connected-event-vector])] + [:dispatch event])]})) + +(rf/reg-event-fx :keycard/on-card-disconnected + (fn [{:keys [db]} _] + (log/debug "[keycard] card disconnected") + {:db (assoc-in db [:keycard :card-connected?] false) + :fx [(when-let [event (get-in db [:keycard :on-card-disconnected-event-vector])] + [:dispatch event])]})) + +(rf/reg-event-fx :keycard.ios/start-nfc + (fn [_] + {:effects.keycard.ios/start-nfc nil})) + +(rf/reg-event-fx :keycard.ios/on-nfc-timeout + (fn [{:keys [db]} _] + (log/debug "[keycard] nfc timeout") + {:db (assoc-in db [:keycard :card-connected?] false) + :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 + (fn [{:keys [db]} [pairings]] + {:db (assoc-in db [:keycard :pairings] pairings) + :fx [[:effects.keycard/set-pairing-to-keycard pairings]]})) + +(rf/reg-event-fx :keycard.ios/on-start-nfc-success + (fn [{:keys [db]} [{:keys [on-cancel-event-vector]}]] + (log/debug "[keycard] nfc started success") + {:db (assoc-in db [:keycard :on-nfc-cancelled-event-vector] on-cancel-event-vector)})) diff --git a/src/status_im/contexts/keycard/login/events.cljs b/src/status_im/contexts/keycard/login/events.cljs new file mode 100644 index 0000000000..b241a25d5e --- /dev/null +++ b/src/status_im/contexts/keycard/login/events.cljs @@ -0,0 +1,87 @@ +(ns status-im.contexts.keycard.login.events + (:require [status-im.contexts.keycard.utils :as keycard.utils] + [taoensso.timbre :as log] + [utils.re-frame :as rf])) + +(rf/reg-event-fx :keycard.login/on-get-keys-error + (fn [{:keys [db]} [error]] + (log/debug "[keycard] get keys error: " error) + (let [tag-was-lost? (keycard.utils/tag-lost? (:error error)) + pin-retries-count (keycard.utils/pin-retries (:error error))] + (if tag-was-lost? + {:db (assoc-in db [:keycard :pin :status] nil)} + (if (nil? pin-retries-count) + {:effects.utils/show-popup {:title "wrong-keycard"}} + {:db (-> db + (assoc-in [:keycard :application-info :pin-retry-counter] pin-retries-count) + (update-in [:keycard :pin] assoc :status :error)) + :fx [[:dispatch [:keycard/hide-connection-sheet]] + (when (zero? pin-retries-count) + [:effects.utils/show-popup {:title "frozen-keycard"}])]}))))) + +(rf/reg-event-fx :keycard.login/on-get-keys-success + (fn [{:keys [db]} [data]] + (let [{:keys [key-uid encryption-public-key + whisper-private-key]} data + key-uid (str "0x" key-uid) + profile (get-in db [:profile/profiles-overview key-uid])] + {:db + (-> db + (dissoc :keycard) + (update :profile/login assoc + :password encryption-public-key + :key-uid key-uid + :name (:name profile))) + :fx [[:dispatch [:keycard/hide-connection-sheet]] + [:effects.keycard/login-with-keycard + {:password encryption-public-key + :whisper-private-key whisper-private-key + :key-uid key-uid}]]}))) + +(rf/reg-event-fx :keycard.login/on-get-keys-from-keychain-success + (fn [{:keys [db]} [key-uid [encryption-public-key whisper-private-key]]] + (when (and encryption-public-key whisper-private-key) + (let [profile (get-in db [:profile/profiles-overview key-uid])] + {:db + (-> db + (dissoc :keycard) + (update :profile/login assoc + :password encryption-public-key + :key-uid key-uid + :name (:name profile))) + :fx [[:dispatch [:keycard/hide-connection-sheet]] + [:effects.keycard/login-with-keycard + {:password encryption-public-key + :whisper-private-key whisper-private-key + :key-uid key-uid}]]})))) + +(rf/reg-event-fx :keycard.login/on-get-application-info-success + (fn [{:keys [db]} [application-info]] + (let [profile (get-in db [:profile/profiles-overview (get-in db [:profile/login :key-uid])]) + pin (get-in db [:keycard :pin :text]) + error (keycard.utils/validate-application-info profile 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)) + :effects.keycard/get-keys {:pin pin + :on-success #(rf/dispatch [:keycard.login/on-get-keys-success %]) + :on-failure #(rf/dispatch [:keycard.login/on-get-keys-error %])}})))) + +(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-and-login + (fn [{:keys [db]}] + (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 %])}]] + (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])]}))) diff --git a/src/status_im/contexts/keycard/pin/events.cljs b/src/status_im/contexts/keycard/pin/events.cljs new file mode 100644 index 0000000000..eeba5ae4cf --- /dev/null +++ b/src/status_im/contexts/keycard/pin/events.cljs @@ -0,0 +1,21 @@ +(ns status-im.contexts.keycard.pin.events + (:require [utils.re-frame :as rf])) + +(rf/reg-event-fx :keycard.pin/delete-pressed + (fn [{:keys [db]}] + (let [pin (get-in db [:keycard :pin :text])] + (when (and pin (pos? (count pin))) + {:db (-> db + (assoc-in [:keycard :pin :text] (.slice pin 0 -1)) + (assoc-in [:keycard :pin :status] nil))})))) + +(rf/reg-event-fx :keycard.pin/number-pressed + (fn [{:keys [db]} [number max-numbers on-complete-event]] + (let [pin (get-in db [:keycard :pin :text]) + new-pin (str pin number)] + (when (<= (count new-pin) max-numbers) + {:db (-> db + (assoc-in [:keycard :pin :text] new-pin) + (assoc-in [:keycard :pin :status] nil)) + :fx [(when (= (dec max-numbers) (count pin)) + [:dispatch [on-complete-event]])]})))) diff --git a/src/status_im/contexts/keycard/pin/view.cljs b/src/status_im/contexts/keycard/pin/view.cljs new file mode 100644 index 0000000000..debc48cc66 --- /dev/null +++ b/src/status_im/contexts/keycard/pin/view.cljs @@ -0,0 +1,26 @@ +(ns status-im.contexts.keycard.pin.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as constants] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn auth + [callback-event-key] + (let [{:keys [text status]} (rf/sub [:keycard/pin]) + pin-retry-counter (rf/sub [:keycard/pin-retry-counter]) + error? (= status :error)] + [rn/view {:padding-bottom 12 :flex 1} + [rn/view {:flex 1 :justify-content :center :align-items :center :padding 34} + [quo/pin-input + {:blur? false + :number-of-pins constants/pincode-length + :number-of-filled-pins (count text) + :error? error? + :info (when error? + (i18n/label :t/pin-retries-left {:number pin-retry-counter}))}]] + [quo/numbered-keyboard + {:delete-key? true + :on-delete #(rf/dispatch [:keycard.pin/delete-pressed]) + :on-press #(rf/dispatch [:keycard.pin/number-pressed % constants/pincode-length + callback-event-key])}]])) diff --git a/src/status_im/contexts/keycard/sheet/events.cljs b/src/status_im/contexts/keycard/sheet/events.cljs new file mode 100644 index 0000000000..01f7419d0a --- /dev/null +++ b/src/status_im/contexts/keycard/sheet/events.cljs @@ -0,0 +1,30 @@ +(ns status-im.contexts.keycard.sheet.events + (:require [re-frame.core :as rf] + [react-native.platform :as platform] + [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 + (fn [_ [args]] + (if platform/android? + {:dispatch [:keycard/show-connection-sheet-component args]} + {:effects.keycard.ios/start-nfc + {:on-success + (fn [] + (log/debug "nfc started successfully. next: show-connection-sheet") + (rf/dispatch [:keycard.ios/on-start-nfc-success args])) + :on-failure + (fn [] + (log/debug "nfc failed star starting. not calling show-connection-sheet"))}}))) + +(rf/reg-event-fx :keycard/hide-connection-sheet + (fn [{:keys [db]}] + (if platform/android? + {:db (assoc-in db [:keycard :connection-sheet-opts] nil) + :fx [[:hide-nfc-sheet nil]]} + {:effects.keycard.ios/stop-nfc nil}))) diff --git a/src/status_im/contexts/keycard/sheet/view.cljs b/src/status_im/contexts/keycard/sheet/view.cljs new file mode 100644 index 0000000000..4422ed3852 --- /dev/null +++ b/src/status_im/contexts/keycard/sheet/view.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.keycard.sheet.view + (:require [quo.foundations.colors :as colors] + quo.theme + [react-native.core :as rn] + [status-im.common.resources :as resources] + [utils.re-frame :as rf])) + +(defn connect-keycard + [] + (let [connected? (rf/sub [:keycard/connected?]) + {:keys [on-close]} (rf/sub [:keycard/connection-sheet-opts]) + theme (quo.theme/use-theme)] + [rn/view {:flex 1} + [rn/view {:flex 1}] + [rn/view + {:style {:align-items :center + :padding-horizontal 36 + :padding-vertical 30 + :background-color (colors/theme-colors colors/white colors/neutral-95 theme)}} + [rn/text {:style {:font-size 26 :color "#9F9FA5" :margin-bottom 36}} + "Ready to Scan"] + [rn/image + {:source (resources/get-image :nfc-prompt)}] + [rn/text {:style {:font-size 16 :color :white :margin-vertical 36}} + (if connected? + "Connected. Don’t move your card." + "Hold your phone near a Status Keycard")] + [rn/pressable + {:on-press (fn [] + (when on-close (on-close)) + (rf/dispatch [:keycard/hide-connection-sheet])) + :style {:flex-direction :row}} + [rn/view + {:style {:background-color "#8E8E93" :flex 1 :align-items :center :padding 18 :border-radius 10}} + [rn/text {:style {:color :white :font-size 16}} + "Cancel"]]]]])) diff --git a/src/status_im/contexts/keycard/utils.cljs b/src/status_im/contexts/keycard/utils.cljs new file mode 100644 index 0000000000..968c2e8807 --- /dev/null +++ b/src/status_im/contexts/keycard/utils.cljs @@ -0,0 +1,45 @@ +(ns status-im.contexts.keycard.utils + (:require [taoensso.timbre :as log])) + +(def pin-mismatch-error #"Unexpected error SW, 0x63C(\d+)|wrongPIN\(retryCounter: (\d+)\)") + +(defn pin-retries + [error] + (when-let [matched-error (re-matches pin-mismatch-error error)] + (js/parseInt (second (filter some? matched-error))))) + +(defn tag-lost? + [error] + (or + (= error "Tag was lost.") + (= error "NFCError:100") + (re-matches #".*NFCError:100.*" error))) + +(defn validate-application-info + [profile {:keys [key-uid paired? pin-retry-counter puk-retry-counter] :as application-info}] + (let [profile-mismatch? (or (nil? profile) (not= (:key-uid profile) key-uid))] + (log/debug "[keycard] login-with-keycard" + "empty application info" (empty? application-info) + "no key-uid" (empty? key-uid) + "profile-mismatch?" profile-mismatch? + "no pairing" paired?) + (cond + (empty? application-info) + :not-keycard + + (empty? (:key-uid application-info)) + :keycard-blank + + profile-mismatch? + :keycard-wrong + + (not paired?) + :keycard-unpaired + + (and (zero? pin-retry-counter) + (or (nil? puk-retry-counter) + (pos? puk-retry-counter))) + nil + + :else + nil))) diff --git a/src/status_im/contexts/preview/quo/main.cljs b/src/status_im/contexts/preview/quo/main.cljs index b8db3dc7e5..1875c47d8e 100644 --- a/src/status_im/contexts/preview/quo/main.cljs +++ b/src/status_im/contexts/preview/quo/main.cljs @@ -144,6 +144,7 @@ small-option-card] [status-im.contexts.preview.quo.password.password-tips :as password-tips] [status-im.contexts.preview.quo.password.tips :as tips] + [status-im.contexts.preview.quo.pin-input.pin-input :as pin-input] [status-im.contexts.preview.quo.profile.collectible :as collectible] [status-im.contexts.preview.quo.profile.collectible-list-item :as collectible-list-item] [status-im.contexts.preview.quo.profile.expanded-collectible :as expanded-collectible] @@ -369,6 +370,8 @@ :component keyboard-key/view} {:name :numbered-keyboard :component numbered-keyboard/view}] + :pin-input [{:name :pin-input + :component pin-input/view}] :links [{:name :internal-link-card :options {:insets {:top true}} :component internal-link-card/view} diff --git a/src/status_im/contexts/preview/quo/pin_input/pin_input.cljs b/src/status_im/contexts/preview/quo/pin_input/pin_input.cljs new file mode 100644 index 0000000000..fca2d6773c --- /dev/null +++ b/src/status_im/contexts/preview/quo/pin_input/pin_input.cljs @@ -0,0 +1,27 @@ +(ns status-im.contexts.preview.quo.pin-input.pin-input + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.preview.quo.preview :as preview])) + +(def descriptor + [{:key :blur? :type :boolean} + {:type :number + :key :number-of-pins} + {:type :number + :key :number-of-filled-pins} + {:type :boolean + :key :error?} + {:type :text + :key :info}]) + +(defn view + [] + (let [[state set-state] (rn/use-state {:blur? false + :number-of-pins 6 + :number-of-filled-pins 0})] + [preview/preview-container + {:state state + :set-state set-state + :descriptor descriptor} + [rn/view {:style {:padding-vertical 40 :align-items :center :justify-content :center}} + [quo/pin-input state]]])) diff --git a/src/status_im/contexts/profile/login/events.cljs b/src/status_im/contexts/profile/login/events.cljs index 7a04139cb6..477596b3a0 100644 --- a/src/status_im/contexts/profile/login/events.cljs +++ b/src/status_im/contexts/profile/login/events.cljs @@ -186,9 +186,13 @@ (rf/reg-event-fx :profile.login/biometric-success (fn [{:keys [db]}] - (let [key-uid (get-in db [:profile/login :key-uid])] - {:keychain/get-user-password [key-uid - #(rf/dispatch [:profile.login/get-user-password-success %])]}))) + (let [key-uid (get-in db [:profile/login :key-uid]) + keycard? (get-in db [:profile/profiles-overview key-uid :keycard-pairing])] + (if keycard? + {:keychain/get-keycard-keys + [key-uid #(rf/dispatch [:keycard.login/on-get-keys-from-keychain-success key-uid %])]} + {:keychain/get-user-password + [key-uid #(rf/dispatch [:profile.login/get-user-password-success %])]})))) (rf/reg-event-fx :profile.login/biometric-auth-fail diff --git a/src/status_im/contexts/profile/profiles/view.cljs b/src/status_im/contexts/profile/profiles/view.cljs index 748907164b..ff4a9d2546 100644 --- a/src/status_im/contexts/profile/profiles/view.cljs +++ b/src/status_im/contexts/profile/profiles/view.cljs @@ -10,6 +10,7 @@ [status-im.common.standard-authentication.core :as standard-authentication] [status-im.config :as config] [status-im.constants :as constants] + [status-im.contexts.keycard.pin.view :as keycard.pin] [status-im.contexts.onboarding.common.background.view :as background] [status-im.contexts.profile.profiles.style :as style] [taoensso.timbre :as log] @@ -140,7 +141,7 @@ [:profile/profile-selected key-uid]) (rf/dispatch [:profile.login/login-with-biometric-if-available key-uid]) - (when-not keycard-pairing (set-hide-profiles)))}])) + (set-hide-profiles))}])) (defn- profiles-section [{:keys [hide-profiles]}] @@ -189,8 +190,8 @@ [:profile.login/biometric-success]) :on-fail #(rf/dispatch [:profile.login/biometric-auth-fail - %])}])) - ] + %])}]))] + [standard-authentication/password-input {:shell? true :blur? true @@ -199,7 +200,7 @@ (defn login-section [{:keys [show-profiles]}] (let [processing (rf/sub [:profile/login-processing]) - {:keys [key-uid name + {:keys [key-uid name keycard-pairing customization-color]} (rf/sub [:profile/login-profile]) sign-in-enabled? (rf/sub [:sign-in-enabled?]) profile-picture (rf/sub [:profile/login-profiles-picture key-uid]) @@ -228,7 +229,7 @@ :disabled? processing :accessibility-label :show-profiles} :i/multi-profile]] - [rn/scroll-view + [(if keycard-pairing rn/view rn/scroll-view) {:keyboard-should-persist-taps :always :style {:flex 1}} [quo/profile-card @@ -236,17 +237,20 @@ :customization-color (or customization-color :primary) :profile-picture profile-picture :card-style style/login-profile-card}] - [password-input]] - [quo/button - {:size 40 - :type :primary - :customization-color (or customization-color :primary) - :accessibility-label :login-button - :icon-left :i/unlocked - :disabled? (or (not sign-in-enabled?) processing) - :on-press login-multiaccount - :container-style {:margin-bottom (+ (safe-area/get-bottom) 12)}} - (i18n/label :t/log-in)]])) + (if keycard-pairing + [keycard.pin/auth :keycard/read-card-and-login] + [password-input])] + (when-not keycard-pairing + [quo/button + {:size 40 + :type :primary + :customization-color (or customization-color :primary) + :accessibility-label :login-button + :icon-left :i/unlocked + :disabled? (or (not sign-in-enabled?) processing) + :on-press login-multiaccount + :container-style {:margin-bottom (+ (safe-area/get-bottom) 12)}} + (i18n/label :t/log-in)])])) (defn view [] diff --git a/src/status_im/db.cljs b/src/status_im/db.cljs index 71259c3a24..2fe4ecd7fb 100644 --- a/src/status_im/db.cljs +++ b/src/status_im/db.cljs @@ -41,10 +41,5 @@ :visibility-status-updates {} :stickers/packs-pending #{} :settings/change-password {} - :keycard {:nfc-enabled? false - :pin {:original [] - :confirmation [] - :current [] - :puk [] - :enter-step :original}} + :keycard {} :theme :light}) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index a93ca91201..d77cd4055e 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -25,6 +25,8 @@ status-im.contexts.communities.overview.events status-im.contexts.communities.sharing.events status-im.contexts.contact.blocking.events + status-im.contexts.keycard.effects + status-im.contexts.keycard.events status-im.contexts.onboarding.common.overlay.events status-im.contexts.onboarding.events status-im.contexts.profile.events @@ -57,6 +59,9 @@ :theme/init-theme nil :network/listen-to-network-info nil :effects.biometric/get-supported-type nil + :effects.keycard/register-card-events nil + :effects.keycard/check-nfc-enabled nil + :effects.keycard/retrieve-pairings nil ;;app starting flow continues in get-profiles-overview :profile/get-profiles-overview #(rf/dispatch [:profile/get-profiles-overview-success %]) :effects.font/get-font-file-for-initials-avatar diff --git a/src/status_im/navigation/core.cljs b/src/status_im/navigation/core.cljs index 286b82a9b6..5c84909c6b 100644 --- a/src/status_im/navigation/core.cljs +++ b/src/status_im/navigation/core.cljs @@ -81,14 +81,22 @@ (fn [] views/bottom-sheet)) ;;;; Alert Banner + (navigation/register-component "alert-banner" (fn [] (gesture/gesture-handler-root-hoc views/alert-banner #js {:flex 0})) (fn [] views/alert-banner)) - ;;;; LEGACY (should be removed in status 2.0) + ;;;; NFC sheet (navigation/register-component - "popover" - (fn [] (gesture/gesture-handler-root-hoc views/popover-comp)) - (fn [] views/popover-comp))) + "nfc-sheet" + (fn [] (gesture/gesture-handler-root-hoc views/nfc-sheet-comp)) + (fn [] views/nfc-sheet-comp))) + +;;;; LEGACY (should be removed in status 2.0) + +(navigation/register-component + "popover" + (fn [] (gesture/gesture-handler-root-hoc views/popover-comp)) + (fn [] views/popover-comp)) diff --git a/src/status_im/navigation/effects.cljs b/src/status_im/navigation/effects.cljs index 181884cbad..b0b9079e7a 100644 --- a/src/status_im/navigation/effects.cljs +++ b/src/status_im/navigation/effects.cljs @@ -230,6 +230,7 @@ (fn [] (navigation/dissmiss-overlay "bottom-sheet"))) ;;;; Alert Banner + (rf/reg-fx :show-alert-banner (fn [[view-id theme]] (show-overlay "alert-banner" @@ -245,67 +246,17 @@ (reset! state/alert-banner-shown? false) (reload-status-nav-color-fx [view-id theme]))) -;;;; Merge options +;;;; NFC sheet -(rf/reg-fx :merge-options - (fn [{:keys [id options]}] - (navigation/merge-options id options))) +(rf/reg-fx :show-nfc-sheet + (fn [] (show-overlay "nfc-sheet"))) + +(rf/reg-fx :hide-nfc-sheet + (fn [] (navigation/dissmiss-overlay "nfc-sheet"))) ;;;; Legacy (should be removed in status 2.0) - -(defn- get-screen-component - [component] - (let [{:keys [options]} (get views/screens component)] - {:component {:id component - :name component - :options (merge (options/statusbar-and-navbar-options (:theme options) nil nil) - options)}})) - -(rf/reg-fx :set-stack-root-fx - (fn [[stack component]] - ;; We don't have bottom tabs as separate stacks anymore,. So the old way of pushing screens in - ;; specific tabs will not work. Disabled set-stack-root for :shell-stack as it is not working - ;; and currently only being used for browser and some rare keycard flows after login - (when-not (= @state/root-id :shell-stack) - (log/debug :set-stack-root-fx stack component) - (navigation/set-stack-root - (name stack) - (if (vector? component) - (mapv get-screen-component component) - (get-screen-component component)))))) - (rf/reg-fx :show-popover (fn [] (show-overlay "popover"))) (rf/reg-fx :hide-popover (fn [] (navigation/dissmiss-overlay "popover"))) - -(rf/reg-fx :show-visibility-status-popover - (fn [] (show-overlay "visibility-status-popover"))) - -(rf/reg-fx :hide-visibility-status-popover - (fn [] (navigation/dissmiss-overlay "visibility-status-popover"))) - -(rf/reg-fx :show-wallet-connect-sheet - (fn [] (show-overlay "wallet-connect-sheet"))) - -(rf/reg-fx :hide-wallet-connect-sheet - (fn [] (navigation/dissmiss-overlay "wallet-connect-sheet"))) - -(rf/reg-fx :show-wallet-connect-success-sheet - (fn [] (show-overlay "wallet-connect-success-sheet"))) - -(rf/reg-fx :hide-wallet-connect-success-sheet - (fn [] (navigation/dissmiss-overlay "wallet-connect-success-sheet"))) - -(rf/reg-fx :show-wallet-connect-app-management-sheet - (fn [] (show-overlay "wallet-connect-app-management-sheet"))) - -(rf/reg-fx :hide-wallet-connect-app-management-sheet - (fn [] (navigation/dissmiss-overlay "wallet-connect-app-management-sheet"))) - -(rf/reg-fx :show-signing-sheet - (fn [] (show-overlay "signing-sheet"))) - -(rf/reg-fx :hide-signing-sheet - (fn [] (navigation/dissmiss-overlay "signing-sheet"))) diff --git a/src/status_im/navigation/events.cljs b/src/status_im/navigation/events.cljs index 7ee2049576..50ca682e69 100644 --- a/src/status_im/navigation/events.cljs +++ b/src/status_im/navigation/events.cljs @@ -74,11 +74,6 @@ [{:keys [db]} root-id] {:set-root [root-id (:theme db)]}) -(rf/defn set-stack-root - {:events [:set-stack-root]} - [_ stack root] - {:set-stack-root-fx [stack root]}) - (rf/defn change-tab {:events [:navigate-change-tab]} [{:keys [db]} stack-id] diff --git a/src/status_im/navigation/view.cljs b/src/status_im/navigation/view.cljs index a35ecf3487..d920670d1c 100644 --- a/src/status_im/navigation/view.cljs +++ b/src/status_im/navigation/view.cljs @@ -11,6 +11,7 @@ [status-im.common.bottom-sheet-screen.view :as bottom-sheet-screen] [status-im.common.bottom-sheet.view :as bottom-sheet] [status-im.common.toasts.view :as toasts] + [status-im.contexts.keycard.sheet.view :as keycard.sheet] [status-im.navigation.screens :as screens] [status-im.setup.hot-reload :as reloader] [utils.re-frame :as rf])) @@ -121,6 +122,17 @@ [alert-banner/view]]) functional-compiler)) +(def nfc-sheet-comp + (reagent/reactify-component + (fn [] + (let [app-theme (rf/sub [:theme])] + ^{:key (str "nfc-sheet-" @reloader/cnt)} + [quo.theme/provider app-theme + [rn/keyboard-avoiding-view + {:style {:position :relative :flex 1}} + [keycard.sheet/connect-keycard]]])) + functional-compiler)) + ;; LEGACY (should be removed in status 2.0) (def popover-comp diff --git a/src/status_im/subs/keycard.cljs b/src/status_im/subs/keycard.cljs new file mode 100644 index 0000000000..53db0caecd --- /dev/null +++ b/src/status_im/subs/keycard.cljs @@ -0,0 +1,38 @@ +(ns status-im.subs.keycard + (:require [utils.re-frame :as rf])) + +(rf/reg-sub + :keycard/keycard-profile? + (fn [db] + (not (nil? (get-in db [:profile/profile :keycard-pairing]))))) + +(rf/reg-sub + :keycard/nfc-enabled? + :<- [:keycard] + (fn [keycard] + (:nfc-enabled? keycard))) + +(rf/reg-sub + :keycard/connected? + :<- [:keycard] + (fn [keycard] + (:card-connected? keycard))) + +(rf/reg-sub + :keycard/pin + :<- [:keycard] + (fn [keycard] + (:pin keycard))) + +(rf/reg-sub + :keycard/pin-retry-counter + :<- [:keycard] + (fn [keycard] + (get-in keycard [:application-info :pin-retry-counter]))) + +(rf/reg-sub + :keycard/connection-sheet-opts + :<- [:keycard] + (fn [keycard] + (:connection-sheet-opts keycard))) + diff --git a/src/status_im/subs/root.cljs b/src/status_im/subs/root.cljs index 029eb3207d..a364cd1a40 100644 --- a/src/status_im/subs/root.cljs +++ b/src/status_im/subs/root.cljs @@ -9,6 +9,7 @@ status-im.subs.community.account-selection status-im.subs.contact status-im.subs.general + status-im.subs.keycard status-im.subs.messages status-im.subs.onboarding status-im.subs.pairing @@ -186,3 +187,6 @@ ;; centralized-metrics (reg-root-key-sub :centralized-metrics/enabled? :centralized-metrics/enabled?) (reg-root-key-sub :centralized-metrics/user-confirmed? :centralized-metrics/user-confirmed?) + +;;keycard +(reg-root-key-sub :keycard :keycard) diff --git a/src/utils/re_frame.cljs b/src/utils/re_frame.cljs index ded7d3c593..be4d05e604 100644 --- a/src/utils/re_frame.cljs +++ b/src/utils/re_frame.cljs @@ -81,6 +81,8 @@ (def sub (comp deref re-frame/subscribe)) +(def reg-sub re-frame/reg-sub) + (def dispatch re-frame/dispatch) (def reg-fx re-frame/reg-fx)