feat(wallet): Add account name, emoji and color validation (#20422)

* Fix extra `0` in create/edit account title input

* Validate name, color and emoji in account creation/edition screen

* Refactor sub

* Fix button disabled condition and placeholder
This commit is contained in:
Ulises Manuel 2024-06-19 11:25:46 -06:00 committed by GitHub
parent e9f98fcf85
commit f192648518
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 275 additions and 121 deletions

View File

@ -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"

View File

@ -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}

View File

@ -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

View File

@ -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))))

View File

@ -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}]]]))))

View File

@ -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})

View File

@ -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}])))))

View File

@ -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})

View File

@ -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)

View File

@ -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))

View File

@ -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)))

View File

@ -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))

View File

@ -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",