diff --git a/src/quo/components/inputs/title_input/component_spec.cljs b/src/quo/components/inputs/title_input/component_spec.cljs index a39fe3b512..52544a771a 100644 --- a/src/quo/components/inputs/title_input/component_spec.cljs +++ b/src/quo/components/inputs/title_input/component_spec.cljs @@ -13,7 +13,7 @@ {:value "" :max-length 24}]) (h/fire-event :on-focus (h/query-by-label-text :profile-title-input)) - (-> (h/wait-for #(h/get-by-text "00")) + (-> (h/wait-for #(h/get-by-text "0")) (.then #(h/is-truthy (h/query-by-text "/24"))))) (h/test "renders with max length digits and character count" @@ -22,7 +22,7 @@ :max-length 24} "abc"]) (h/fire-event :on-focus (h/query-by-label-text :profile-title-input)) - (-> (h/wait-for #(h/get-by-text "03")) + (-> (h/wait-for #(h/get-by-text "3")) (.then #(h/is-truthy (h/query-by-text "/24"))))) (h/test "text updates on change" diff --git a/src/quo/components/inputs/title_input/view.cljs b/src/quo/components/inputs/title_input/view.cljs index 834a70a05b..fdc3c2104d 100644 --- a/src/quo/components/inputs/title_input/view.cljs +++ b/src/quo/components/inputs/title_input/view.cljs @@ -71,9 +71,7 @@ [text/text {:style (style/char-count blur? theme) :size :paragraph-2} - (pad-0 - (str - (count value)))] + (str (count value))] [text/text {:style (style/char-count blur? theme) :size :paragraph-2} diff --git a/src/status_im/contexts/wallet/account/edit_account/view.cljs b/src/status_im/contexts/wallet/account/edit_account/view.cljs index 3dffd8c670..531af3f7f9 100644 --- a/src/status_im/contexts/wallet/account/edit_account/view.cljs +++ b/src/status_im/contexts/wallet/account/edit_account/view.cljs @@ -1,10 +1,12 @@ (ns status-im.contexts.wallet.account.edit-account.view - (:require [quo.core :as quo] + (:require [clojure.string :as string] + [quo.core :as quo] [react-native.core :as rn] [reagent.core :as reagent] [status-im.contexts.wallet.account.edit-account.style :as style] [status-im.contexts.wallet.common.screen-base.create-or-edit-account.view :as create-or-edit-account] + [status-im.contexts.wallet.common.utils :as common.utils] [status-im.contexts.wallet.sheets.network-preferences.view :as network-preferences] [status-im.contexts.wallet.sheets.remove-account.view :as remove-account] @@ -35,6 +37,8 @@ (defn view [] (let [edited-account-name (reagent/atom nil) + name-error (reagent/atom nil) + emoji-color-error (reagent/atom nil) on-change-color (fn [edited-color {:keys [color] :as account}] (when (not= edited-color color) (save-account {:account account @@ -55,12 +59,16 @@ :as account} (rf/sub [:wallet/current-viewing-account]) network-details (rf/sub [:wallet/network-preference-details]) test-networks-enabled? (rf/sub [:profile/test-networks-enabled?]) + other-account-names (rf/sub [:wallet/accounts-names-without-current-account]) + other-emojis-and-colors (rf/sub [:wallet/accounts-emojis-and-colors-without-current-account]) network-preferences-key (if test-networks-enabled? :test-preferred-chain-ids :prod-preferred-chain-ids) account-name (or @edited-account-name name) - button-disabled? (or (nil? @edited-account-name) - (= name @edited-account-name))] + input-error (or @emoji-color-error @name-error) + button-disabled? (or (string/blank? @edited-account-name) + (= name @edited-account-name) + (some? input-error))] [create-or-edit-account/view {:page-nav-right-side [(when-not default-account? {:icon-name :i/delete @@ -69,16 +77,32 @@ (fn [] [remove-account/view])}])})] :account-name account-name + :placeholder (i18n/label :t/default-account-placeholder) :account-emoji emoji :account-color color - :on-change-name #(reset! edited-account-name %) - :on-change-color #(on-change-color % account) - :on-change-emoji #(on-change-emoji % account) + :on-change-name (fn [new-name] + (reset! edited-account-name new-name) + (reset! name-error (common.utils/get-account-name-error + @edited-account-name + other-account-names))) + :on-change-color (fn [new-color] + (if (other-emojis-and-colors [emoji new-color]) + (reset! emoji-color-error :emoji-and-color) + (do + (reset! emoji-color-error nil) + (on-change-color new-color account)))) + :on-change-emoji (fn [new-emoji] + (if (other-emojis-and-colors [new-emoji color]) + (reset! emoji-color-error :emoji-and-color) + (do + (reset! emoji-color-error nil) + (on-change-emoji new-emoji account)))) :section-label :t/account-info :bottom-action-label :t/confirm :bottom-action-props {:customization-color color :disabled? button-disabled? - :on-press #(on-confirm-name account)}} + :on-press #(on-confirm-name account)} + :error input-error} [quo/data-item {:status :default :size :default diff --git a/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/component_spec.cljs b/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/component_spec.cljs index 953abce02c..8a56ba3eac 100644 --- a/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/component_spec.cljs +++ b/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/component_spec.cljs @@ -7,9 +7,11 @@ (h/setup-restorable-re-frame) (h/test "Create Account button is disabled while no account name exists" - (h/setup-subs {:wallet/watch-only-accounts [] - :alert-banners/top-margin 0 - :get-screen-params {:address "0xmock-address"}}) + (h/setup-subs {:wallet/watch-only-accounts [] + :alert-banners/top-margin 0 + :get-screen-params {:address "0xmock-address"} + :wallet/accounts-names #{"My account 1" "My account 2"} + :wallet/accounts-emojis-and-colors #{["😊" :sky] ["😶" :army]}}) (h/render [confirm-address/view]) (h/is-truthy (h/get-by-text "0xmock-address")) (h/is-disabled (h/get-by-label-text :confirm-button-label)))) diff --git a/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/view.cljs b/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/view.cljs index 859cf404ec..24e3eb5e0d 100644 --- a/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/view.cljs +++ b/src/status_im/contexts/wallet/add_account/add_address_to_watch/confirm_address/view.cljs @@ -9,6 +9,7 @@ [status-im.contexts.wallet.add-account.add-address-to-watch.confirm-address.style :as style] [status-im.contexts.wallet.common.screen-base.create-or-edit-account.view :as create-or-edit-account] + [status-im.contexts.wallet.common.utils :as common.utils] [utils.i18n :as i18n] [utils.re-frame :as rf])) @@ -19,43 +20,64 @@ account-name (reagent/atom "") account-color (reagent/atom (rand-nth colors/account-colors)) account-emoji (reagent/atom (emoji-picker.utils/random-emoji)) - on-change-name #(reset! account-name %) - on-change-color #(reset! account-color %) - on-change-emoji #(reset! account-emoji %)] + name-error (reagent/atom nil) + emoji-color-error (reagent/atom nil)] (fn [] - [rn/view {:style style/container} - [create-or-edit-account/view - {:placeholder placeholder - :account-name @account-name - :account-emoji @account-emoji - :account-color @account-color - :on-change-name on-change-name - :on-change-color on-change-color - :on-change-emoji on-change-emoji - :watch-only? true - :bottom-action-label :t/add-watched-address - :bottom-action-props {:customization-color @account-color - :disabled? (string/blank? @account-name) - :accessibility-label :confirm-button-label - :on-press #(rf/dispatch [:wallet/add-account - {:type :watch - :account-name @account-name - :emoji @account-emoji - :color @account-color} - {:address address - :public-key ""}])}} - [quo/data-item - {:card? true - :emoji @account-emoji - :title (i18n/label :t/watched-address) - :subtitle address - :status :default - :size :default - :subtitle-type :default - :custom-subtitle (fn [] [quo/text - {:size :paragraph-2 - ;; TODO: monospace font - ;; https://github.com/status-im/status-mobile/issues/17009 - :weight :monospace} - address]) - :container-style style/data-item}]]]))) + (let [accounts-names (rf/sub [:wallet/accounts-names]) + accounts-emojis-and-colors (rf/sub [:wallet/accounts-emojis-and-colors]) + on-change-name (fn [new-name] + (reset! account-name new-name) + (reset! name-error (common.utils/get-account-name-error + @account-name + accounts-names))) + on-change-color (fn [new-color] + (reset! account-color new-color) + (reset! emoji-color-error + (when (accounts-emojis-and-colors + [@account-emoji @account-color]) + :emoji-and-color))) + on-change-emoji (fn [new-emoji] + (reset! account-emoji new-emoji) + (reset! emoji-color-error + (when (accounts-emojis-and-colors + [@account-emoji @account-color]) + :emoji-and-color))) + input-error (or @emoji-color-error @name-error)] + [rn/view {:style style/container} + [create-or-edit-account/view + {:placeholder placeholder + :account-name @account-name + :account-emoji @account-emoji + :account-color @account-color + :on-change-name on-change-name + :on-change-color on-change-color + :on-change-emoji on-change-emoji + :watch-only? true + :error input-error + :bottom-action-label :t/add-watched-address + :bottom-action-props {:customization-color @account-color + :disabled? (or (string/blank? @account-name) + (some? input-error)) + :accessibility-label :confirm-button-label + :on-press #(rf/dispatch [:wallet/add-account + {:type :watch + :account-name @account-name + :emoji @account-emoji + :color @account-color} + {:address address + :public-key ""}])}} + [quo/data-item + {:card? true + :emoji @account-emoji + :title (i18n/label :t/watched-address) + :subtitle address + :status :default + :size :default + :subtitle-type :default + :custom-subtitle (fn [] [quo/text + {:size :paragraph-2 + ;; TODO: monospace font + ;; https://github.com/status-im/status-mobile/issues/17009 + :weight :monospace} + address]) + :container-style style/data-item}]]])))) diff --git a/src/status_im/contexts/wallet/add_account/create_account/style.cljs b/src/status_im/contexts/wallet/add_account/create_account/style.cljs index bd2d3afb51..0af2fdfbf5 100644 --- a/src/status_im/contexts/wallet/add_account/create_account/style.cljs +++ b/src/status_im/contexts/wallet/add_account/create_account/style.cljs @@ -11,10 +11,11 @@ :bottom 0 :left 80}) -(def title-input-container +(defn title-input-container + [error?] {:padding-horizontal 20 :padding-top 12 - :padding-bottom 16}) + :padding-bottom (if error? 8 16)}) (def color-picker-container {:padding-vertical 12}) diff --git a/src/status_im/contexts/wallet/add_account/create_account/view.cljs b/src/status_im/contexts/wallet/add_account/create_account/view.cljs index a0661c54e4..e5d5c9dbae 100644 --- a/src/status_im/contexts/wallet/add_account/create_account/view.cljs +++ b/src/status_im/contexts/wallet/add_account/create_account/view.cljs @@ -12,6 +12,7 @@ [status-im.common.standard-authentication.core :as standard-auth] [status-im.constants :as constants] [status-im.contexts.wallet.add-account.create-account.style :as style] + [status-im.contexts.wallet.common.utils :as common.utils] [status-im.contexts.wallet.sheets.account-origin.view :as account-origin] [status-im.feature-flags :as ff] [utils.i18n :as i18n] @@ -74,16 +75,30 @@ (defn- input [_] (let [placeholder (i18n/label :t/default-account-placeholder)] - (fn [{:keys [account-color account-name on-change-text]}] - [quo/title-input - {:customization-color account-color - :placeholder placeholder - :on-change-text on-change-text - :max-length constants/wallet-account-name-max-length - :blur? true - :disabled? false - :default-value account-name - :container-style style/title-input-container}]))) + (fn [{:keys [account-color account-name on-change-text error]}] + [rn/view + [quo/title-input + {:customization-color account-color + :placeholder placeholder + :on-change-text on-change-text + :max-length constants/wallet-account-name-max-length + :blur? true + :disabled? false + :default-value account-name + :container-style (style/title-input-container error)}] + (when error + [quo/info-message + {:type :error + :size :default + :icon :i/info + :container-style {:margin-left 20 + :margin-bottom 16}} + (case error + :emoji (i18n/label :t/key-name-error-emoji) + :special-character (i18n/label :t/key-name-error-special-char) + :existing-name (i18n/label :t/name-must-differ-error) + :emoji-and-color (i18n/label :t/emoji-and-colors-unique-error) + nil)])]))) (defn- color-picker [_] @@ -154,9 +169,7 @@ children)))) (defn add-new-keypair-variant - [{:keys [on-change-text set-account-color set-emoji] - {:keys [account-name account-color emoji]} - :state}] + [{{:keys [account-name account-color emoji]} :state}] (let [on-auth-success (fn [password] (rf/dispatch [:wallet/import-and-create-keypair-with-account @@ -164,7 +177,7 @@ :account-preferences {:account-name @account-name :color @account-color :emoji @emoji}}]))] - (fn [{:keys [customization-color keypair-name]}] + (fn [{:keys [on-change-text set-account-color set-emoji customization-color keypair-name error]}] (let [{:keys [new-account-data]} (rf/sub [:wallet/create-account-new-keypair])] [floating-button {:account-color @account-color @@ -178,7 +191,8 @@ [input {:account-color @account-color :account-name @account-name - :on-change-text on-change-text}] + :on-change-text on-change-text + :error error}] [color-picker {:account-color @account-color :set-account-color set-account-color}] @@ -188,12 +202,10 @@ :keypair-title keypair-name}]])))) (defn derive-account-variant - [{:keys [on-change-text set-account-color set-emoji] - {:keys [account-name account-color emoji]} - :state}] + [{{:keys [account-name account-color emoji]} :state}] (let [derivation-path (reagent/atom "") set-derivation-path #(reset! derivation-path %)] - (fn [{:keys [customization-color]}] + (fn [{:keys [on-change-text set-account-color set-emoji customization-color error]}] (let [{:keys [derived-from key-uid]} (rf/sub [:wallet/selected-keypair]) on-auth-success (rn/use-callback @@ -219,7 +231,8 @@ {:account-color @account-color :slide-button-props {:on-auth-success on-auth-success :disabled? (or (empty? @account-name) - (= "" @derivation-path))}} + (= "" @derivation-path) + (some? error))}} [avatar {:account-color @account-color :emoji @emoji @@ -227,7 +240,8 @@ [input {:account-color @account-color :account-name @account-name - :on-change-text on-change-text}] + :on-change-text on-change-text + :error error}] [color-picker {:account-color @account-color :set-account-color set-account-color}] @@ -236,34 +250,62 @@ :customization-color customization-color}]])))) (defn view - [_] - (let [account-name (reagent/atom "") - account-color (reagent/atom (rand-nth colors/account-colors)) - emoji (reagent/atom (emoji-picker.utils/random-emoji)) - on-change-text #(reset! account-name %) - set-account-color #(reset! account-color %) - set-emoji #(reset! emoji %) - state {:account-name account-name - :account-color account-color - :emoji emoji}] + [] + (let [account-name (reagent/atom "") + account-color (reagent/atom (rand-nth colors/account-colors)) + emoji (reagent/atom (emoji-picker.utils/random-emoji)) + account-name-error (reagent/atom nil) + emoji-and-color-error? (reagent/atom false) + state {:account-name account-name + :account-color account-color + :emoji emoji + :account-name-error account-name-error + :emoji-and-color-error? emoji-and-color-error?}] (fn [] - (let [customization-color (rf/sub [:profile/customization-color]) + (let [customization-color (rf/sub [:profile/customization-color]) ;; Having a keypair means the user is importing it or creating it. - {:keys [keypair-name]} (rf/sub [:wallet/create-account-new-keypair])] + {:keys [keypair-name]} (rf/sub [:wallet/create-account-new-keypair]) + accounts-names (rf/sub [:wallet/accounts-names]) + accounts-emojis-and-colors (rf/sub [:wallet/accounts-emojis-and-colors]) + on-change-text (rn/use-callback + (fn [new-text] + (reset! account-name new-text) + (reset! account-name-error + (common.utils/get-account-name-error new-text + accounts-names))) + [accounts-names accounts-emojis-and-colors]) + check-emoji-and-color-error (fn [emoji color] + (let [repeated? (accounts-emojis-and-colors [emoji color])] + (reset! emoji-and-color-error? + (when repeated? :emoji-and-color)))) + set-account-color (rn/use-callback + (fn [new-color] + (reset! account-color new-color) + (check-emoji-and-color-error @emoji new-color)) + [accounts-emojis-and-colors @emoji]) + set-emoji (rn/use-callback + (fn [new-emoji] + (reset! emoji new-emoji) + (check-emoji-and-color-error new-emoji @account-color)) + [accounts-emojis-and-colors @account-color]) + error (or @account-name-error @emoji-and-color-error?)] + (rn/use-mount #(check-emoji-and-color-error @emoji @account-color)) (rn/use-unmount #(rf/dispatch [:wallet/clear-create-account])) (if keypair-name [add-new-keypair-variant - {:customization-color customization-color + {:state state + :customization-color customization-color :on-change-text on-change-text :set-account-color set-account-color :set-emoji set-emoji - :state state - :keypair-name keypair-name}] + :keypair-name keypair-name + :error error}] [derive-account-variant - {:customization-color customization-color + {:state state + :customization-color customization-color :on-change-text on-change-text :set-account-color set-account-color :set-emoji set-emoji - :state state}]))))) + :error error}]))))) diff --git a/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/style.cljs b/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/style.cljs index 0eba9e3d0a..de8ae531a7 100644 --- a/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/style.cljs +++ b/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/style.cljs @@ -15,14 +15,11 @@ :bottom 0 :left 76}) -(def title-input-container +(defn title-input-container + [error?] {:padding-horizontal 20 :padding-top 12 - :padding-bottom 16}) - -(def error-container - {:margin-horizontal 20 - :margin-bottom 16}) + :padding-bottom (if error? 8 16)}) (def divider-1 {:margin-bottom 12}) diff --git a/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/view.cljs b/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/view.cljs index a930501554..7571412f0b 100644 --- a/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/view.cljs +++ b/src/status_im/contexts/wallet/common/screen_base/create_or_edit_account/view.cljs @@ -11,11 +11,9 @@ (defn view [{:keys [page-nav-right-side placeholder account-name account-color account-emoji - on-change-name - on-change-color - on-change-emoji section-label - bottom-action-label bottom-action-props - custom-bottom-action watch-only?]} & children] + on-change-name on-change-color on-change-emoji section-label bottom-action-label + bottom-action-props custom-bottom-action watch-only? error]} + & children] (let [{window-width :width} (rn/get-window) footer (if custom-bottom-action custom-bottom-action @@ -52,14 +50,30 @@ :on-press #(rf/dispatch [:emoji-picker/open {:on-select on-change-emoji}]) :container-style style/reaction-button-container} :i/reaction]] - [quo/title-input - {:placeholder placeholder - :max-length constants/wallet-account-name-max-length - :blur? true - :default-value account-name - :auto-focus true - :on-change-text on-change-name - :container-style style/title-input-container}] + + [rn/view + [quo/title-input + {:placeholder placeholder + :max-length constants/wallet-account-name-max-length + :blur? true + :default-value account-name + :auto-focus true + :on-change-text on-change-name + :container-style (style/title-input-container error)}] + (when error + [quo/info-message + {:type :error + :size :default + :icon :i/info + :container-style {:margin-left 20 + :margin-bottom 16}} + (case error + :emoji (i18n/label :t/key-name-error-emoji) + :special-character (i18n/label :t/key-name-error-special-char) + :existing-name (i18n/label :t/name-must-differ-error) + :emoji-and-color (i18n/label :t/emoji-and-colors-unique-error) + nil)])] + [quo/divider-line {:container-style style/divider-1}] [quo/section-label {:section (i18n/label :t/colour) diff --git a/src/status_im/contexts/wallet/common/utils.cljs b/src/status_im/contexts/wallet/common/utils.cljs index 1362f94460..e26e606eae 100644 --- a/src/status_im/contexts/wallet/common/utils.cljs +++ b/src/status_im/contexts/wallet/common/utils.cljs @@ -4,7 +4,8 @@ [status-im.common.qr-codes.view :as qr-codes] [status-im.constants :as constants] [utils.money :as money] - [utils.number :as number])) + [utils.number :as number] + [utils.string])) (defn get-first-name [full-name] @@ -281,3 +282,10 @@ (defn make-limit-label-fiat [amount currency-symbol] (str currency-symbol amount)) + +(defn get-account-name-error + [s existing-account-names] + (cond + (utils.string/contains-emoji? s) :emoji + (existing-account-names s) :existing-name + (utils.string/contains-special-character? s) :special-character)) diff --git a/src/status_im/subs/wallet/wallet.cljs b/src/status_im/subs/wallet/wallet.cljs index 78df0a6705..8cafc42082 100644 --- a/src/status_im/subs/wallet/wallet.cljs +++ b/src/status_im/subs/wallet/wallet.cljs @@ -47,12 +47,11 @@ :wallet/home-tokens-loading? :<- [:wallet/tokens-loading] (fn [tokens-loading] - (if (empty? tokens-loading) - true - (->> tokens-loading - vals - (some true?) - boolean)))) + (or (empty? tokens-loading) + (->> tokens-loading + vals + (some true?) + boolean)))) (rf/reg-sub :wallet/current-viewing-account-tokens-loading? @@ -633,3 +632,34 @@ (->> accounts (some #(= :partially (:operable %))) boolean))) + +(rf/reg-sub + :wallet/accounts-names + :<- [:wallet/accounts] + (fn [accounts] + (set (map :name accounts)))) + +(rf/reg-sub + :wallet/accounts-names-without-current-account + :<- [:wallet/accounts-names] + :<- [:wallet/current-viewing-account] + (fn [[account-names current-viewing-account]] + (disj account-names (:name current-viewing-account)))) + +(defn- get-emoji-and-colors-from-accounts + [accounts] + (->> accounts + (map (fn [{:keys [emoji color]}] [emoji color])) + (set))) + +(rf/reg-sub + :wallet/accounts-emojis-and-colors + :<- [:wallet/accounts] + (fn [accounts] + (get-emoji-and-colors-from-accounts accounts))) + +(rf/reg-sub + :wallet/accounts-emojis-and-colors-without-current-account + :<- [:wallet/accounts-without-current-viewing-account] + (fn [accounts] + (get-emoji-and-colors-from-accounts accounts))) diff --git a/src/utils/string.cljs b/src/utils/string.cljs index 6ded68f40f..9de548234a 100644 --- a/src/utils/string.cljs +++ b/src/utils/string.cljs @@ -1,6 +1,7 @@ (ns utils.string (:require - [clojure.string :as string])) + [clojure.string :as string] + [utils.transforms :as transforms])) (defn truncate-str-memo "Given string and max threshold, trims the string to threshold length with `...` @@ -59,3 +60,16 @@ (take n) (map (comp string/upper-case str first)) string/join))) + +(def emoji-data (transforms/js->clj (js/require "../resources/data/emojis/en.json"))) +(def emoji-unicode-values (map :unicode emoji-data)) + +(defn contains-emoji? + [s] + (some (fn [emoji] + (string/includes? s emoji)) + emoji-unicode-values)) + +(defn contains-special-character? + [s] + (re-find #"[^a-zA-Z0-9\s]" s)) diff --git a/translations/en.json b/translations/en.json index ee60f50668..d02a77cefe 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2639,6 +2639,8 @@ "key-name-error-emoji": "Emojis are not allowed", "key-name-error-special-char": "Special characters are not allowed", "key-name-error-too-short": "Key pair name must be at least {{count}} characters", + "name-must-differ-error": "Name must differ from other accounts", + "emoji-and-colors-unique-error": "Emoji and colour combination must be unique", "display": "Display", "testnet-mode": "Testnet mode", "turn-on-testnet-mode": "Turn on testnet mode",