diff --git a/src/quo/components/info/info_message.cljs b/src/quo/components/info/info_message.cljs index 9a5ad131f8..84fe2e9f44 100644 --- a/src/quo/components/info/info_message.cljs +++ b/src/quo/components/info/info_message.cljs @@ -3,22 +3,17 @@ [quo.components.icon :as quo.icons] [quo.components.markdown.text :as text] [quo.foundations.colors :as colors] - [quo.theme :as theme] + [quo.theme :as quo.theme] [react-native.core :as rn])) -(def themes - {:light {:default colors/neutral-50 - :success colors/success-50 - :error colors/danger-50} - :dark {:default colors/neutral-40 - :success colors/success-60 - :error colors/danger-60}}) - (defn get-color - [k] - (get-in themes [(theme/get-theme) k])) + [k theme] + (case k + :success (colors/resolve-color :success theme) + :error (colors/resolve-color :danger theme) + (colors/theme-colors colors/neutral-50 colors/neutral-40 theme))) -(defn info-message +(defn view-internal "[info-message opts \"message\"] opts {:type :default/:success/:error @@ -27,11 +22,11 @@ :text-color colors/white ;; text color override :icon-color colors/white ;; icon color override :no-icon-color? false ;; disable tint color for icon" - [{:keys [type size icon text-color icon-color no-icon-color? style]} message] + [{:keys [type size theme icon text-color icon-color no-icon-color? style accessibility-label]} message] (let [weight (if (= size :default) :regular :medium) icon-size (if (= size :default) 16 12) size (if (= size :default) :paragraph-2 :label) - text-color (or text-color (get-color type)) + text-color (or text-color (get-color type theme)) icon-color (or icon-color text-color)] [rn/view {:style (merge {:flex-direction :row @@ -42,7 +37,10 @@ :no-color no-icon-color? :size icon-size}] [text/text - {:size size - :weight weight - :style {:color text-color - :margin-horizontal 4}} message]])) + {:accessibility-label accessibility-label + :size size + :weight weight + :style {:color text-color + :margin-horizontal 4}} message]])) + +(def info-message (quo.theme/with-theme view-internal)) diff --git a/src/status_im2/common/password_authentication/view.cljs b/src/status_im2/common/password_authentication/view.cljs index 7a4c30c979..ee3d988f90 100644 --- a/src/status_im2/common/password_authentication/view.cljs +++ b/src/status_im2/common/password_authentication/view.cljs @@ -31,10 +31,10 @@ :on-change-text #(reset! entered-password %)}] (when (not-empty error) [quo/info-message - {:type :error - :size :default - :icon :i/info - :style {:margin-top 8}} + {:type :error + :size :default + :icon :i/info + :containstyle {:margin-top 8}} (i18n/label :t/oops-wrong-password)]) [quo/button {:container-style {:margin-bottom 12 :margin-top 40} diff --git a/src/status_im2/contexts/wallet/add_address_to_watch/component_spec.cljs b/src/status_im2/contexts/wallet/add_address_to_watch/component_spec.cljs new file mode 100644 index 0000000000..0411bfc4e0 --- /dev/null +++ b/src/status_im2/contexts/wallet/add_address_to_watch/component_spec.cljs @@ -0,0 +1,38 @@ +(ns status-im2.contexts.wallet.add-address-to-watch.component-spec + (:require + [re-frame.core :as re-frame] + [status-im2.contexts.wallet.add-address-to-watch.view :as add-address-to-watch] + [test-helpers.component :as h])) + +(defn setup-subs + [subscriptions] + (doseq [keyval subscriptions] + (re-frame/reg-sub + (key keyval) + (fn [_] (val keyval))))) + +(h/describe "select address for watch only account" + (h/test "validation messages show for already used addressed" + (setup-subs {:wallet/scanned-address nil + :wallet/addresses (set + ["0x12E838Ae1f769147b12956485dc56e57138f3AC8" + "0x22E838Ae1f769147b12956485dc56e57138f3AC8"]) + :profile/customization-color :blue}) + (h/render [add-address-to-watch/view]) + (h/is-falsy (h/query-by-label-text :error-message)) + (h/fire-event :change-text + (h/get-by-label-text :add-address-to-watch) + "0x12E838Ae1f769147b12956485dc56e57138f3AC8") + (h/is-truthy (h/get-by-translation-text :address-already-in-use)))) + +(h/test "validation messages show for invalid address" + (setup-subs {:wallet/scanned-address nil + :wallet/addresses (set + ["0x12E838Ae1f769147b12956485dc56e57138f3AC8" + "0x22E838Ae1f769147b12956485dc56e57138f3AC8"]) + :profile/customization-color :blue}) + (h/render [add-address-to-watch/view]) + (h/is-falsy (h/query-by-label-text :error-message)) + (h/fire-event :change-text (h/get-by-label-text :add-address-to-watch) "0x12E838Ae1f769147b") + (h/is-truthy (h/get-by-translation-text :invalid-address))) + diff --git a/src/status_im2/contexts/wallet/add_address_to_watch/confirm_address/style.cljs b/src/status_im2/contexts/wallet/add_address_to_watch/confirm_address/style.cljs index 6c23b7c053..c322816c9a 100644 --- a/src/status_im2/contexts/wallet/add_address_to_watch/confirm_address/style.cljs +++ b/src/status_im2/contexts/wallet/add_address_to_watch/confirm_address/style.cljs @@ -3,18 +3,7 @@ (def container {:flex 1}) -(def input - {:margin-right 12 - :flex 1}) - (def data-item {:margin-horizontal 20 :padding-vertical 8 :padding-horizontal 12}) - -(defn button-container - [bottom] - {:position :absolute - :bottom (+ bottom 12) - :left 20 - :right 20}) diff --git a/src/status_im2/contexts/wallet/add_address_to_watch/confirm_address/view.cljs b/src/status_im2/contexts/wallet/add_address_to_watch/confirm_address/view.cljs index 70da0f8d6f..26260c9074 100644 --- a/src/status_im2/contexts/wallet/add_address_to_watch/confirm_address/view.cljs +++ b/src/status_im2/contexts/wallet/add_address_to_watch/confirm_address/view.cljs @@ -3,7 +3,6 @@ [clojure.string :as string] [quo.core :as quo] [quo.foundations.colors :as colors] - [quo.theme :as quo.theme] [re-frame.core :as re-frame] [react-native.core :as rn] [reagent.core :as reagent] @@ -14,13 +13,12 @@ [utils.i18n :as i18n] [utils.re-frame :as rf])) -(defn- view-internal +(defn view [] (let [{:keys [address]} (rf/sub [:get-screen-params]) number-of-accounts (count (rf/sub [:profile/wallet-accounts])) account-name (reagent/atom (i18n/label :t/default-account-name {:number (inc number-of-accounts)})) - address-title (i18n/label :t/watch-address) account-color (reagent/atom (rand-nth colors/account-colors)) account-emoji (reagent/atom (emoji-picker.utils/random-emoji)) on-change-name #(reset! account-name %) @@ -40,8 +38,9 @@ :on-change-name on-change-name :on-change-color on-change-color :on-change-emoji on-change-emoji + :watch-only? true :bottom-action? true - :bottom-action-label :t/create-account + :bottom-action-label :t/add-watched-address :bottom-action-props {:customization-color @account-color :disabled? (string/blank? @account-name) :on-press #(re-frame/dispatch [:navigate-to @@ -51,11 +50,9 @@ :right-icon :i/advanced :icon-right? true :emoji @account-emoji - :title address-title + :title (i18n/label :t/watched-address) :subtitle address :status :default :size :default :container-style style/data-item :on-press #(js/alert "To be implemented")}]]]))) - -(def view (quo.theme/with-theme view-internal)) diff --git a/src/status_im2/contexts/wallet/add_address_to_watch/style.cljs b/src/status_im2/contexts/wallet/add_address_to_watch/style.cljs index 3cd0b8df90..325cd607f0 100644 --- a/src/status_im2/contexts/wallet/add_address_to_watch/style.cljs +++ b/src/status_im2/contexts/wallet/add_address_to_watch/style.cljs @@ -5,13 +5,24 @@ :margin-top 12 :margin-bottom 20}) -(def input-container - {:flex-direction :row - :padding-horizontal 20 - :align-items :flex-end}) - (def button-container {:position :absolute :bottom 22 :left 20 :right 20}) + +(def scan + {:align-self + :flex-end}) + +(def input + {:flex 1 + :margin-right 12}) + +(def input-container + {:flex-direction :row + :margin-horizontal 20}) + +(def info-message + {:margin-top 8 + :margin-left 20}) diff --git a/src/status_im2/contexts/wallet/add_address_to_watch/view.cljs b/src/status_im2/contexts/wallet/add_address_to_watch/view.cljs index 0fb445b8ad..aa8483492a 100644 --- a/src/status_im2/contexts/wallet/add_address_to_watch/view.cljs +++ b/src/status_im2/contexts/wallet/add_address_to_watch/view.cljs @@ -2,51 +2,114 @@ (:require [clojure.string :as string] [quo.core :as quo] - [quo.theme :as quo.theme] - [re-frame.core :as re-frame] [react-native.clipboard :as clipboard] [react-native.core :as rn] [reagent.core :as reagent] [status-im2.contexts.wallet.add-address-to-watch.style :as style] + [status-im2.contexts.wallet.common.validation :as validation] [utils.i18n :as i18n] [utils.re-frame :as rf])) -(defn view-internal +(defn validate-message + [addresses] + (fn [s] + (cond + (or (= s nil) (= s "")) nil + (contains? addresses s) (i18n/label :t/address-already-in-use) + (not (or (validation/eth-address? s) + (validation/ens-name? s))) (i18n/label :t/invalid-address) + :else nil))) + +(defn- address-input + [{:keys [input-value validation-msg validate + clear-input]}] + (let [scanned-address (rf/sub [:wallet/scanned-address]) + empty-input? (and (string/blank? @input-value) + (string/blank? scanned-address)) + + on-change-text (fn [new-text] + (reset! validation-msg (validate new-text)) + (reset! input-value new-text) + (when (and scanned-address (not= scanned-address new-text)) + (rf/dispatch [:wallet/clean-scanned-address]))) + paste-on-input #(clipboard/get-string + (fn [clipboard-text] + (on-change-text clipboard-text)))] + (rn/use-effect (fn [] + (when-not (string/blank? scanned-address) + (on-change-text scanned-address))) + [scanned-address]) + [rn/view + {:style style/input-container} + [quo/input + {:accessibility-label :add-address-to-watch + :placeholder (i18n/label :t/address-placeholder) + :container-style style/input + :label (i18n/label :t/eth-or-ens) + :auto-capitalize :none + :multiline? true + :on-clear clear-input + :return-key-type :done + :clearable? (not empty-input?) + :on-change-text on-change-text + :button (when empty-input? + {:on-press paste-on-input + :text (i18n/label :t/paste)}) + :value @input-value}] + [quo/button + {:type :outline + :on-press (fn [] + (rn/dismiss-keyboard!) + (rf/dispatch [:open-modal :scan-address])) + :container-style style/scan + :size 40 + :icon-only? true} + :i/scan]])) + +(defn view [] - (let [input-value (reagent/atom "") + (let [addresses (rf/sub [:wallet/addresses]) + input-value (reagent/atom nil) + validate (validate-message (set addresses)) + validation-msg (reagent/atom (validate + @input-value)) + clear-input (fn [] + (reset! input-value nil) + (reset! validation-msg nil) + (rf/dispatch [:wallet/clean-scanned-address])) customization-color (rf/sub [:profile/customization-color])] + (rf/dispatch [:wallet/clean-scanned-address]) (fn [] [rn/view {:style {:flex 1}} [quo/page-nav {:type :no-title :icon-name :i/close - :on-press #(rf/dispatch [:navigate-back])}] + :on-press (fn [] + (rf/dispatch [:wallet/clean-scanned-address]) + (rf/dispatch [:navigate-back]))}] [quo/text-combinations {:container-style style/header-container :title (i18n/label :t/add-address) :description (i18n/label :t/enter-eth)}] - [rn/view {:style style/input-container} - [quo/input - {:label (i18n/label :t/eth-or-ens) - :button {:on-press (fn [] (clipboard/get-string #(reset! input-value %))) - :text (i18n/label :t/paste)} - :placeholder (str "0x123abc... " (string/lower-case (i18n/label :t/or)) " bob.eth") - :container-style {:margin-right 12 - :flex 1} - :weight :monospace - :on-change-text #(reset! input-value %) - :default-value @input-value}] - [quo/button - {:icon-only? true - :type :outline} :i/scan]] + [:f> address-input + {:input-value input-value + :validate validate + :validation-msg validation-msg + :clear-input clear-input}] + (when @validation-msg + [quo/info-message + {:accessibility-label :error-message + :size :default + :icon :i/info + :type :error + :style style/info-message} + @validation-msg]) [quo/button {:customization-color customization-color - :disabled? (clojure.string/blank? @input-value) - :on-press #(re-frame/dispatch [:navigate-to - :confirm-address-to-watch - {:address @input-value}]) + :disabled? (string/blank? @input-value) + :on-press #(rf/dispatch [:navigate-to + :confirm-address-to-watch + {:address @input-value}]) :container-style style/button-container} (i18n/label :t/continue)]]))) - -(def view (quo.theme/with-theme view-internal)) diff --git a/src/status_im2/contexts/wallet/common/screen_base/create_or_edit_account/view.cljs b/src/status_im2/contexts/wallet/common/screen_base/create_or_edit_account/view.cljs index 273ff1ece9..7609dfd681 100644 --- a/src/status_im2/contexts/wallet/common/screen_base/create_or_edit_account/view.cljs +++ b/src/status_im2/contexts/wallet/common/screen_base/create_or_edit_account/view.cljs @@ -1,6 +1,5 @@ (ns status-im2.contexts.wallet.common.screen-base.create-or-edit-account.view (:require [quo.core :as quo] - [quo.theme :as quo.theme] [react-native.core :as rn] [react-native.safe-area :as safe-area] [status-im2.constants :as constants] @@ -8,12 +7,12 @@ [utils.i18n :as i18n] [utils.re-frame :as rf])) -(defn- view-internal +(defn view [{:keys [margin-top? page-nav-right-side account-name account-color account-emoji on-change-name on-change-color on-change-emoji on-focus on-blur section-label bottom-action? bottom-action-label bottom-action-props - custom-bottom-action]} & children] + custom-bottom-action watch-only?]} & children] (let [{:keys [top bottom]} (safe-area/get-insets) margin-top (if (false? margin-top?) 0 top) {window-width :width} (rn/get-window)] @@ -36,7 +35,7 @@ {:customization-color account-color :size 80 :emoji account-emoji - :type :default}] + :type (if watch-only? :watch-only :default)}] [quo/button {:size 32 :type :grey @@ -81,5 +80,3 @@ :type :primary} bottom-action-props) (i18n/label bottom-action-label)])])])) - -(def view (quo.theme/with-theme view-internal)) diff --git a/src/status_im2/contexts/wallet/common/sheets/account_options/view.cljs b/src/status_im2/contexts/wallet/common/sheets/account_options/view.cljs index 8ae602f90f..66470baf5c 100644 --- a/src/status_im2/contexts/wallet/common/sheets/account_options/view.cljs +++ b/src/status_im2/contexts/wallet/common/sheets/account_options/view.cljs @@ -1,13 +1,16 @@ (ns status-im2.contexts.wallet.common.sheets.account-options.view (:require [quo.core :as quo] + [quo.foundations.colors :as colors] + quo.theme + [react-native.clipboard :as clipboard] [react-native.core :as rn] [status-im2.contexts.wallet.common.sheets.account-options.style :as style] [status-im2.contexts.wallet.common.temp :as temp] [utils.i18n :as i18n] [utils.re-frame :as rf])) -(defn view - [] +(defn- view-internal + [{:keys [theme]}] (let [{:keys [name color emoji address]} (rf/sub [:wallet/current-viewing-account])] [:<> [quo/drawer-top @@ -26,7 +29,13 @@ :on-press #(rf/dispatch [:navigate-to :wallet-edit-account])} {:icon :i/copy :accessibility-label :copy-address - :label (i18n/label :t/copy-address)} + :label (i18n/label :t/copy-address) + :on-press (fn [] + (rf/dispatch [:toasts/upsert + {:icon :i/correct + :icon-color (colors/resolve-color :success theme) + :text (i18n/label :t/address-copied)}]) + (clipboard/set-string address))} {:icon :i/share :accessibility-label :share-account :label (i18n/label :t/share-account)} @@ -42,3 +51,5 @@ {:data temp/other-accounts :render-fn (fn [account] [quo/account-item {:account-props account}]) :style {:margin-horizontal 8}}]])) + +(def view (quo.theme/with-theme view-internal)) diff --git a/src/status_im2/contexts/wallet/common/validation.cljs b/src/status_im2/contexts/wallet/common/validation.cljs new file mode 100644 index 0000000000..eb793c52f5 --- /dev/null +++ b/src/status_im2/contexts/wallet/common/validation.cljs @@ -0,0 +1,5 @@ +(ns status-im2.contexts.wallet.common.validation + (:require [status-im2.constants :as constants])) + +(defn ens-name? [s] (re-find constants/regx-ens s)) +(defn eth-address? [s] (re-find constants/regx-address s)) diff --git a/src/status_im2/contexts/wallet/send/select_address/view.cljs b/src/status_im2/contexts/wallet/send/select_address/view.cljs index ff042d09d7..ce80379c68 100644 --- a/src/status_im2/contexts/wallet/send/select_address/view.cljs +++ b/src/status_im2/contexts/wallet/send/select_address/view.cljs @@ -48,7 +48,7 @@ (fn [] (let [scanned-address (rf/sub [:wallet/scanned-address]) send-address (rf/sub [:wallet/send-address]) - valid-ens-or-address? (boolean (rf/sub [:wallet/valid-ens-or-address?]))] + valid-ens-or-address? (rf/sub [:wallet/valid-ens-or-address?])] [quo/address-input {:on-focus #(reset! input-focused? true) :on-blur #(reset! input-focused? false) diff --git a/src/status_im2/core_spec.cljs b/src/status_im2/core_spec.cljs index a400d3b73f..42d5c9a9f5 100644 --- a/src/status_im2/core_spec.cljs +++ b/src/status_im2/core_spec.cljs @@ -3,4 +3,5 @@ [status-im2.common.floating-button-page.component-spec] [status-im2.contexts.chat.messages.content.audio.component-spec] [status-im2.contexts.communities.actions.community-options.component-spec] + [status-im2.contexts.wallet.add-address-to-watch.component-spec] [status-im2.contexts.wallet.create-account.edit-derivation-path.component-spec])) diff --git a/src/status_im2/subs/wallet/wallet.cljs b/src/status_im2/subs/wallet/wallet.cljs index 14ec684edf..9cd8bd0731 100644 --- a/src/status_im2/subs/wallet/wallet.cljs +++ b/src/status_im2/subs/wallet/wallet.cljs @@ -21,6 +21,14 @@ vals (sort-by :position))) +(rf/reg-sub + :wallet/addresses + :<- [:wallet] + :-> #(->> % + :accounts + keys + set)) + (rf/reg-sub :wallet/balances :<- [:wallet/accounts] diff --git a/src/status_im2/subs/wallet/wallet_test.cljs b/src/status_im2/subs/wallet/wallet_test.cljs index 6bc98ff70c..8b2b3da0a2 100644 --- a/src/status_im2/subs/wallet/wallet_test.cljs +++ b/src/status_im2/subs/wallet/wallet_test.cljs @@ -167,3 +167,17 @@ :balance 3250 :tokens tokens-0x1} (rf/sub [sub-name]))))) + + +(h/deftest-sub :wallet/addresses + [sub-name] + (testing "returns all addresses" + (swap! rf-db/app-db + #(-> % + (assoc-in [:wallet :accounts] accounts) + (assoc-in [:wallet :current-viewing-account-address] "0x1"))) + + (is + (= (set ["0x1" "0x2"]) + (rf/sub [sub-name]))))) + diff --git a/translations/en.json b/translations/en.json index ab0b8a252d..6c1e723256 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1565,7 +1565,8 @@ "delete-account": "Delete account", "delete-keys-keycard": "Delete keys from Keycard", "watch-only": "Watch-only", - "watch-address": "Watch address", + "watched-address": "Watched address", + "add-watched-address": "Add watched address", "cant-report-bug": "Can't report a bug", "mail-should-be-configured": "Mail client should be configured", "check-on-block-explorer": "Check on block explorer", @@ -2384,5 +2385,9 @@ "address-activity": "This address has activity", "keypairs": "Keypairs", "keypairs-description": "Select keypair to derive your new account from", - "confirm-account-origin": "Confirm account origin" + "confirm-account-origin": "Confirm account origin", + "address-placeholder": "0x123abc... or bob.eth", + "invalid-address": "It’s not Ethereum address or ENS name", + "address-already-in-use": "Address already being used", + "address-copied": "Address copied" }