From e6f9932dc513c802c747e03fa7c396caa84f8b35 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Wed, 24 Apr 2024 00:28:49 +0300 Subject: [PATCH] chore(wallet) import private key main screen (#19625) Co-authored-by: Jamie Caprani --- .../settings/section_label/view.cljs | 6 +- src/status_im/constants.cljs | 1 + .../import_private_key/style.cljs | 36 +++++ .../import_private_key/view.cljs | 133 ++++++++++++++++++ .../create_account/select_keypair/view.cljs | 5 +- .../contexts/wallet/common/validation.cljs | 1 + src/status_im/feature_flags.cljs | 1 + src/status_im/navigation/screens.cljs | 6 + translations/en.json | 10 +- 9 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 src/status_im/contexts/wallet/add_account/create_account/import_private_key/style.cljs create mode 100644 src/status_im/contexts/wallet/add_account/create_account/import_private_key/view.cljs diff --git a/src/quo/components/settings/section_label/view.cljs b/src/quo/components/settings/section_label/view.cljs index 345cd76662..d1568c8992 100644 --- a/src/quo/components/settings/section_label/view.cljs +++ b/src/quo/components/settings/section_label/view.cljs @@ -30,8 +30,10 @@ (let [theme (quo.theme/use-theme) color (get-text-color theme (or blur? false)) description? (not (nil? description)) - root-view (if (seq container-style) rn/view :<>)] - [root-view {:style container-style} + root-view (if (seq container-style) rn/view :<>) + root-style (when (seq container-style) + {:style container-style})] + [root-view root-style [text/text {:number-of-lines 1 :size (if description? :paragraph-1 :paragraph-2) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 78de93d412..d0f9b888a3 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -214,6 +214,7 @@ (def regx-string-public-key "0x04[0-9a-f]{128}") (def regx-compressed-key (re-pattern (str "^" regx-string-compressed-key "$"))) (def regx-public-key (re-pattern (str "^" regx-string-public-key "$"))) +(def regx-private-key #"^0x[0-9a-fA-F]{64}$") (def regx-emoji #"^((?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC69\uDC6E\uDC70-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD26\uDD30-\uDD39\uDD3D\uDD3E\uDDD1-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])?|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDEEB\uDEEC\uDEF4-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267B\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])\uFE0F|[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF])+$") (def regx-bold #"\*[^*]+\*") diff --git a/src/status_im/contexts/wallet/add_account/create_account/import_private_key/style.cljs b/src/status_im/contexts/wallet/add_account/create_account/import_private_key/style.cljs new file mode 100644 index 0000000000..432162dc4e --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/import_private_key/style.cljs @@ -0,0 +1,36 @@ +(ns status-im.contexts.wallet.add-account.create-account.import-private-key.style + (:require [quo.foundations.colors :as colors])) + +(def input + {:margin-top 12 + :padding-horizontal 20}) + +(def indicator + {:padding-horizontal 20 + :padding-vertical 8}) + +(def info-box + {:margin-bottom 20}) + +(def page-top + {:margin-top 2}) + +(def key-section + {:margin-top 22 + :padding-horizontal 20}) + +(def section-label + {:margin-bottom 8}) + +(defn public-address + [state theme] + (let [border-color (case state + :active-address (colors/resolve-color :success theme 40) + :inactive-address (colors/resolve-color :warning theme 40) + colors/neutral-20)] + {:border-width 1 + :border-color border-color + :border-style :dashed + :border-radius 16 + :padding-vertical 8 + :padding-horizontal 12})) diff --git a/src/status_im/contexts/wallet/add_account/create_account/import_private_key/view.cljs b/src/status_im/contexts/wallet/add_account/create_account/import_private_key/view.cljs new file mode 100644 index 0000000000..dca9fec6df --- /dev/null +++ b/src/status_im/contexts/wallet/add_account/create_account/import_private_key/view.cljs @@ -0,0 +1,133 @@ +(ns status-im.contexts.wallet.add-account.create-account.import-private-key.view + (:require + [quo.core :as quo] + [quo.theme :as theme] + [react-native.clipboard :as clipboard] + [react-native.core :as rn] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.add-account.create-account.import-private-key.style :as style] + [status-im.contexts.wallet.common.validation :as v] + [utils.debounce :as debounce] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn- address-input + [{:keys [input-value set-input-value set-flow-state error?]}] + (let [check-address (rn/use-callback + (debounce/debounce (fn [v] + (if (empty? v) + (set-flow-state nil) + ;; TODO check for validation + (if-not (v/private-key? v) + (set-flow-state :invalid-private-key) + ;; TODO add real requests + (do + (set-flow-state :scanning) + (js/setTimeout + set-flow-state + 400 + (rand-nth [:active-address :inactive-address])))))) + 500)) + on-change (rn/use-callback + (fn [v] + (set-input-value v) + (check-address v))) + on-paste (rn/use-callback + (fn [] + (clipboard/get-string + (fn [clipboard] + (when-not (empty? clipboard) + (on-change clipboard))))))] + [quo/input + {:accessibility-label :add-address-to-watch + :placeholder (i18n/label :t/enter-private-key-placeholder) + :container-style style/input + :label (i18n/label :t/private-key) + :type :password + :error? error? + :return-key-type :done + :on-change-text on-change + :button (when (empty? input-value) + {:on-press on-paste + :text (i18n/label :t/paste)}) + :value input-value}])) + +(defn- activity-indicator + [state] + (let [{:keys [message] :as props} + (case state + :scanning + {:type :info + :icon :i/scanning + :message :t/scanning-for-activity} + + :inactive-address + {:type :warning + :icon :i/info + :message :t/this-account-has-no-activity} + + :active-address + {:type :success + :icon :i/done + :message :t/this-address-has-activity} + + :invalid-private-key + {:type :error + :icon :i/info + :message :t/invalid-private-key} + + nil)] + (when props + [quo/info-message + (assoc props + :size :default + :style style/indicator) + (i18n/label message)]))) + +(defn view + [] + (let [theme (theme/use-theme) + customization-color (rf/sub [:profile/customization-color]) + [input-value set-input-value] (rn/use-state "") + [flow-state set-flow-state] (rn/use-state) + error? (= :invalid-private-key flow-state)] + [rn/view {:flex 1} + [floating-button-page/view + {:customization-color customization-color + :header [quo/page-nav + {:background :white + :type :no-title + :icon-name :i/close + :on-press #(rf/dispatch [:navigate-back])}] + :footer [:<> + (when-not (#{:active-address :inactive-address} flow-state) + [quo/information-box + {:type :default + :icon :i/info + :style style/info-box} + (i18n/label :t/import-private-key-info)]) + [quo/button + {:customization-color customization-color + :disabled? (or (empty? input-value) error?) + :on-press #()} + (i18n/label :t/continue)]]} + [quo/page-top + {:container-style style/page-top + :title (i18n/label :t/import-private-key) + :description :text + :description-text (i18n/label :t/enter-private-key)}] + [address-input + {:input-value input-value + :set-input-value set-input-value + :set-flow-state set-flow-state + :error? error?}] + (when (#{:scanning :active-address :inactive-address} flow-state) + [rn/view {:style style/key-section} + [quo/section-label + {:section (i18n/label :t/private-key-public-address) + :container-style style/section-label}] + ;; TODO Get real address + [rn/view {:style (style/public-address flow-state theme)} + [quo/text "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"]]]) + (when (seq input-value) + [activity-indicator flow-state])]])) diff --git a/src/status_im/contexts/wallet/add_account/create_account/select_keypair/view.cljs b/src/status_im/contexts/wallet/add_account/create_account/select_keypair/view.cljs index 113e5771e6..76abc4379a 100644 --- a/src/status_im/contexts/wallet/add_account/create_account/select_keypair/view.cljs +++ b/src/status_im/contexts/wallet/add_account/create_account/select_keypair/view.cljs @@ -5,6 +5,7 @@ [react-native.core :as rn] [status-im.constants :as constants] [status-im.contexts.wallet.add-account.create-account.select-keypair.style :as style] + [status-im.feature-flags :as ff] [utils.address :as utils] [utils.i18n :as i18n] [utils.re-frame :as rf])) @@ -22,7 +23,9 @@ :add-divider? true} {:icon :i/key :accessibility-label :import-private-key - :label (i18n/label :t/import-private-key)}]]]) + :label (i18n/label :t/import-private-key) + :on-press (when (ff/enabled? ::wallet.import-private-key) + #(rf/dispatch [:navigate-to :screen/wallet.import-private-key]))}]]]) (defn- parse-accounts [given-accounts] diff --git a/src/status_im/contexts/wallet/common/validation.cljs b/src/status_im/contexts/wallet/common/validation.cljs index 9d4d11c6d6..bab2ab4af5 100644 --- a/src/status_im/contexts/wallet/common/validation.cljs +++ b/src/status_im/contexts/wallet/common/validation.cljs @@ -3,3 +3,4 @@ (defn ens-name? [s] (re-find constants/regx-ens s)) (defn eth-address? [s] (re-find constants/regx-multichain-address s)) +(defn private-key? [s] (re-find constants/regx-private-key s)) diff --git a/src/status_im/feature_flags.cljs b/src/status_im/feature_flags.cljs index 1f331d6e6b..3356bf78d2 100644 --- a/src/status_im/feature_flags.cljs +++ b/src/status_im/feature_flags.cljs @@ -13,6 +13,7 @@ {::wallet.bridge-token (enabled-in-env? :FLAG_BRIDGE_TOKEN_ENABLED) ::wallet.edit-derivation-path (enabled-in-env? :FLAG_EDIT_DERIVATION_PATH) ::wallet.remove-account (enabled-in-env? :FLAG_REMOVE_ACCOUNT_ENABLED) + ::wallet.import-private-key (enabled-in-env? :FLAG_IMPORT_PRIVATE_KEY_ENABLED) ::wallet.long-press-watch-only-asset (enabled-in-env? :FLAG_LONG_PRESS_WATCH_ONLY_ASSET_ENABLED) ::wallet.assets-modal-manage-tokens (enabled-in-env? :FLAG_ASSETS_MODAL_MANAGE_TOKENS) ::wallet.assets-modal-hide (enabled-in-env? :FLAG_ASSETS_MODAL_HIDE) diff --git a/src/status_im/navigation/screens.cljs b/src/status_im/navigation/screens.cljs index f2bf40c5eb..00eb2b14a5 100644 --- a/src/status_im/navigation/screens.cljs +++ b/src/status_im/navigation/screens.cljs @@ -70,6 +70,8 @@ [status-im.contexts.wallet.add-account.add-address-to-watch.view :as wallet-add-address-to-watch] [status-im.contexts.wallet.add-account.create-account.edit-derivation-path.view :as wallet-edit-derivation-path] + [status-im.contexts.wallet.add-account.create-account.import-private-key.view :as + wallet-import-private-key] [status-im.contexts.wallet.add-account.create-account.new-keypair.backup-recovery-phrase.view :as wallet-backup-recovery-phrase] [status-im.contexts.wallet.add-account.create-account.new-keypair.check-your-backup.view :as @@ -393,6 +395,10 @@ {:name :screen/wallet.edit-derivation-path :component wallet-edit-derivation-path/view} + {:name :screen/wallet.import-private-key + :options {:insets {:top? true}} + :component wallet-import-private-key/view} + {:name :screen/wallet.collectible :component wallet-collectible/view} diff --git a/translations/en.json b/translations/en.json index c3a21f00ca..93b87dcb38 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2579,5 +2579,13 @@ "one-user-was-invited": "1 user was invited", "n-users-were-invited": "{{count}} users were invited", "invite-friend-to-status": "Invite friends to Status", - "send-community-link": "Send community link" + "send-community-link": "Send community link", + "enter-private-key": "Enter the private key of an address", + "enter-private-key-placeholder": "Enter your 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.", + "invalid-private-key": "It’s not a valid private key", + "private-key-public-address": "Public address of private key", + "this-account-has-no-activity": "This account has no activity", + "this-address-has-activity": "This address has activity", + "scanning-for-activity": "Scanning for activity..." }