mirror of
https://github.com/status-im/status-mobile.git
synced 2025-01-13 18:25:45 +00:00
feat: implement address input (#17191)
Signed-off-by: Brian Sztamfater <brian@status.im>
This commit is contained in:
parent
9706111256
commit
baa9dff237
161
src/quo2/components/inputs/address_input/component_spec.cljs
Normal file
161
src/quo2/components/inputs/address_input/component_spec.cljs
Normal file
@ -0,0 +1,161 @@
|
||||
(ns quo2.components.inputs.address-input.component-spec
|
||||
(:require [quo2.components.inputs.address-input.view :as address-input]
|
||||
[test-helpers.component :as h]
|
||||
[react-native.clipboard :as clipboard]
|
||||
[quo2.foundations.colors :as colors]))
|
||||
|
||||
(def ens-regex #"^(?=.{5,255}$)([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$")
|
||||
|
||||
(h/describe "Address input"
|
||||
(h/test "default render"
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input {:ens-regex ens-regex}])
|
||||
(h/is-truthy (h/get-by-label-text :address-text-input))))
|
||||
|
||||
(h/test "on focus with blur? false"
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input {:ens-regex ens-regex}])
|
||||
(h/fire-event :on-focus (h/get-by-label-text :address-text-input))
|
||||
(h/has-prop (h/get-by-label-text :address-text-input) :placeholder-text-color colors/neutral-40)))
|
||||
|
||||
(h/test "on focus with blur? true"
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input
|
||||
{:blur? true
|
||||
:ens-regex ens-regex}])
|
||||
(h/fire-event :on-focus (h/get-by-label-text :address-text-input))
|
||||
(h/has-prop (h/get-by-label-text :address-text-input)
|
||||
:placeholder-text-color
|
||||
colors/neutral-80-opa-40)))
|
||||
|
||||
(h/test "scanned value is properly set"
|
||||
(let [on-change-text (h/mock-fn)
|
||||
scanned-value "scanned-value"]
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input
|
||||
{:scanned-value scanned-value
|
||||
:on-change-text on-change-text
|
||||
:ens-regex ens-regex}])
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :clear-button)))
|
||||
(h/was-called-with on-change-text scanned-value)
|
||||
(h/has-prop (h/get-by-label-text :address-text-input) :default-value scanned-value))))
|
||||
|
||||
(h/test "clear icon is shown when input has text"
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input
|
||||
{:scanned-value "scanned value"
|
||||
:ens-regex ens-regex}])
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :clear-button-container)))
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :clear-button)))))
|
||||
|
||||
(h/test "on blur with text and blur? false"
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input
|
||||
{:scanned-value "scanned value"
|
||||
:ens-regex ens-regex}])
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :clear-button)))
|
||||
(h/fire-event :on-focus (h/get-by-label-text :address-text-input))
|
||||
(h/fire-event :on-blur (h/get-by-label-text :address-text-input))
|
||||
(h/has-prop (h/get-by-label-text :address-text-input) :placeholder-text-color colors/neutral-30)))
|
||||
|
||||
(h/test "on blur with text blur? true"
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input
|
||||
{:scanned-value "scanned value"
|
||||
:blur? true
|
||||
:ens-regex ens-regex}])
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :clear-button)))
|
||||
(h/fire-event :on-focus (h/get-by-label-text :address-text-input))
|
||||
(h/fire-event :on-blur (h/get-by-label-text :address-text-input))
|
||||
(h/has-prop (h/get-by-label-text :address-text-input)
|
||||
:placeholder-text-color
|
||||
colors/neutral-80-opa-20)))
|
||||
|
||||
(h/test "on blur with no text and blur? false"
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input {:ens-regex ens-regex}])
|
||||
(h/fire-event :on-focus (h/get-by-label-text :address-text-input))
|
||||
(h/fire-event :on-blur (h/get-by-label-text :address-text-input))
|
||||
(h/has-prop (h/get-by-label-text :address-text-input) :placeholder-text-color colors/neutral-40)))
|
||||
|
||||
(h/test "on blur with no text blur? true"
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input
|
||||
{:blur? true
|
||||
:ens-regex ens-regex}])
|
||||
(h/fire-event :on-focus (h/get-by-label-text :address-text-input))
|
||||
(h/fire-event :on-blur (h/get-by-label-text :address-text-input))
|
||||
(h/has-prop (h/get-by-label-text :address-text-input)
|
||||
:placeholder-text-color
|
||||
colors/neutral-80-opa-40)))
|
||||
|
||||
(h/test "on-clear is called"
|
||||
(let [on-clear (h/mock-fn)]
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input
|
||||
{:scanned-value "scanned value"
|
||||
:on-clear on-clear
|
||||
:ens-regex ens-regex}])
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :clear-button)))
|
||||
(h/fire-event :press (h/get-by-label-text :clear-button))
|
||||
(h/was-called on-clear))))
|
||||
|
||||
(h/test "on-focus is called"
|
||||
(let [on-focus (h/mock-fn)]
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input {:on-focus on-focus}])
|
||||
(h/fire-event :on-focus (h/get-by-label-text :address-text-input))
|
||||
(h/was-called on-focus))))
|
||||
|
||||
(h/test "on-blur is called"
|
||||
(let [on-blur (h/mock-fn)]
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input
|
||||
{:on-blur on-blur
|
||||
:ens-regex ens-regex}])
|
||||
(h/fire-event :on-blur (h/get-by-label-text :address-text-input))
|
||||
(h/was-called on-blur))))
|
||||
|
||||
(h/test "on-scan is called"
|
||||
(let [on-scan (h/mock-fn)]
|
||||
(with-redefs [clipboard/get-string #(% "")]
|
||||
(h/render [address-input/address-input {:on-scan on-scan}])
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :scan-button)))
|
||||
(h/fire-event :press (h/get-by-label-text :scan-button))
|
||||
(h/was-called on-scan))))
|
||||
|
||||
(h/test "paste from clipboard"
|
||||
(let [clipboard "clipboard"]
|
||||
(with-redefs [clipboard/get-string #(% clipboard)]
|
||||
(h/render [address-input/address-input {:ens-regex ens-regex}])
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :paste-button)))
|
||||
(h/fire-event :press (h/get-by-label-text :paste-button))
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :clear-button)))
|
||||
(h/has-prop (h/get-by-label-text :address-text-input) :default-value clipboard))))
|
||||
|
||||
(h/test "ENS loading state and call on-detect-ens"
|
||||
(let [clipboard "test.eth"
|
||||
on-detect-ens (h/mock-fn)]
|
||||
(with-redefs [clipboard/get-string #(% clipboard)]
|
||||
(h/render [address-input/address-input
|
||||
{:on-detect-ens on-detect-ens
|
||||
:ens-regex ens-regex}])
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :paste-button)))
|
||||
(h/fire-event :press (h/get-by-label-text :paste-button))
|
||||
(h/wait-for #(h/is-falsy (h/get-by-label-text :clear-button)))
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :loading-button-container)))
|
||||
(h/was-called on-detect-ens))))
|
||||
|
||||
(h/test "ENS valid state and call on-detect-ens"
|
||||
(let [clipboard "test.eth"
|
||||
on-detect-ens (h/mock-fn)]
|
||||
(with-redefs [clipboard/get-string #(% clipboard)]
|
||||
(h/render [address-input/address-input
|
||||
{:on-detect-ens on-detect-ens
|
||||
:valid-ens? true
|
||||
:ens-regex ens-regex}])
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :paste-button)))
|
||||
(h/fire-event :press (h/get-by-label-text :paste-button))
|
||||
(h/wait-for #(h/is-falsy (h/get-by-label-text :clear-button)))
|
||||
(h/wait-for #(h/is-truthy (h/get-by-label-text :positive-button-container)))
|
||||
(h/was-called on-detect-ens)))))
|
40
src/quo2/components/inputs/address_input/style.cljs
Normal file
40
src/quo2/components/inputs/address_input/style.cljs
Normal file
@ -0,0 +1,40 @@
|
||||
(ns quo2.components.inputs.address-input.style
|
||||
(:require [quo2.foundations.colors :as colors]
|
||||
[quo2.components.markdown.text :as text]
|
||||
[react-native.platform :as platform]))
|
||||
|
||||
(def container
|
||||
{:padding-horizontal 20
|
||||
:padding-top 8
|
||||
:padding-bottom 16
|
||||
:height 48
|
||||
:flex-direction :row
|
||||
:align-items :flex-start})
|
||||
|
||||
(def buttons-container
|
||||
{:flex-direction :row
|
||||
:align-items :center
|
||||
:padding-top (when platform/android? 2)})
|
||||
|
||||
(def clear-icon-container
|
||||
{:justify-content :center
|
||||
:align-items :center
|
||||
:padding-top (if platform/ios? 6 2)
|
||||
:height 24
|
||||
:width 20})
|
||||
|
||||
(defn input-text
|
||||
[theme]
|
||||
(assoc (text/text-style {:size :paragraph-1
|
||||
:weight :monospace})
|
||||
:flex 1
|
||||
:color (colors/theme-colors colors/neutral-100 colors/white theme)
|
||||
:margin-top (if platform/ios? 0 -4)
|
||||
:margin-right 8
|
||||
:height (if platform/ios? 24 40)))
|
||||
|
||||
(defn accessory-button
|
||||
[blur? theme]
|
||||
{:border-color (if blur?
|
||||
(colors/theme-colors colors/neutral-80-opa-10 colors/white-opa-10 theme)
|
||||
(colors/theme-colors colors/neutral-30 colors/neutral-70 theme))})
|
163
src/quo2/components/inputs/address_input/view.cljs
Normal file
163
src/quo2/components/inputs/address_input/view.cljs
Normal file
@ -0,0 +1,163 @@
|
||||
(ns quo2.components.inputs.address-input.view
|
||||
(:require [react-native.core :as rn]
|
||||
[react-native.clipboard :as clipboard]
|
||||
[quo2.theme :as quo.theme]
|
||||
[quo2.foundations.colors :as colors]
|
||||
[quo2.components.icon :as icon]
|
||||
[quo2.components.buttons.button.view :as button]
|
||||
[quo2.components.inputs.address-input.style :as style]
|
||||
[utils.i18n :as i18n]
|
||||
[reagent.core :as reagent]
|
||||
[react-native.platform :as platform]))
|
||||
|
||||
(defn- icon-color
|
||||
[blur? theme]
|
||||
(if blur?
|
||||
(colors/theme-colors colors/neutral-80-opa-30 colors/white-opa-10 theme)
|
||||
(colors/theme-colors colors/neutral-40 colors/neutral-60 theme)))
|
||||
|
||||
(defn- clear-button
|
||||
[{:keys [on-press blur? theme]}]
|
||||
[rn/touchable-opacity
|
||||
{:accessibility-label :clear-button
|
||||
:style style/clear-icon-container
|
||||
:on-press on-press}
|
||||
[icon/icon :i/clear
|
||||
{:color (icon-color blur? theme)
|
||||
:size 20}]])
|
||||
|
||||
(defn- loading-icon
|
||||
[blur? theme]
|
||||
[rn/view {:style style/clear-icon-container}
|
||||
[icon/icon :i/loading
|
||||
{:color (icon-color blur? theme)
|
||||
:size 20}]])
|
||||
|
||||
(defn- positive-state-icon
|
||||
[theme]
|
||||
[rn/view {:style style/clear-icon-container}
|
||||
[icon/icon :i/positive-state
|
||||
{:color (colors/theme-colors (colors/custom-color :success 50)
|
||||
(colors/custom-color :success 60)
|
||||
theme)
|
||||
:size 20}]])
|
||||
|
||||
(defn- get-placeholder-text-color
|
||||
[status theme blur?]
|
||||
(cond
|
||||
(and (= status :default) blur?)
|
||||
(colors/theme-colors colors/neutral-80-opa-40 colors/white-opa-30 theme)
|
||||
(and (= status :default) (not blur?))
|
||||
(colors/theme-colors colors/neutral-40 colors/neutral-50 theme)
|
||||
(and (not= status :default) blur?)
|
||||
(colors/theme-colors colors/neutral-80-opa-20 colors/white-opa-20 theme)
|
||||
(and (not= status :default) (not blur?))
|
||||
(colors/theme-colors colors/neutral-30 colors/neutral-60 theme)))
|
||||
|
||||
(defn- f-address-input-internal
|
||||
[]
|
||||
(let [status (reagent/atom :default)
|
||||
value (reagent/atom "")
|
||||
clipboard (reagent/atom nil)
|
||||
focused? (atom false)]
|
||||
(fn [{:keys [scanned-value theme blur? on-change-text on-blur on-focus on-clear on-scan on-detect-ens
|
||||
ens-regex
|
||||
valid-ens?]}]
|
||||
(let [on-change (fn [text]
|
||||
(let [ens? (boolean (re-matches ens-regex text))]
|
||||
(if (> (count text) 0)
|
||||
(reset! status :typing)
|
||||
(reset! status :active))
|
||||
(reset! value text)
|
||||
(when on-change-text
|
||||
(on-change-text text))
|
||||
(when (and ens? on-detect-ens)
|
||||
(reset! status :loading)
|
||||
(on-detect-ens text))))
|
||||
on-paste (fn []
|
||||
(when-not (empty? @clipboard)
|
||||
(on-change @clipboard)
|
||||
(reset! value @clipboard)))
|
||||
on-clear (fn []
|
||||
(reset! value "")
|
||||
(reset! status (if @focused? :active :default))
|
||||
(when on-clear
|
||||
(on-clear)))
|
||||
on-scan #(when on-scan
|
||||
(on-scan))
|
||||
on-focus (fn []
|
||||
(when (= (count @value) 0)
|
||||
(reset! status :active))
|
||||
(reset! focused? true)
|
||||
(when on-focus (on-focus)))
|
||||
on-blur (fn []
|
||||
(when (= @status :active)
|
||||
(reset! status :default))
|
||||
(reset! focused? false)
|
||||
(when on-blur (on-blur)))
|
||||
placeholder-text-color (get-placeholder-text-color @status theme blur?)]
|
||||
(rn/use-effect (fn []
|
||||
(when-not (empty? scanned-value)
|
||||
(on-change scanned-value)))
|
||||
[scanned-value])
|
||||
(clipboard/get-string #(reset! clipboard %))
|
||||
[rn/view {:style style/container}
|
||||
[rn/text-input
|
||||
{:accessibility-label :address-text-input
|
||||
:style (style/input-text theme)
|
||||
:placeholder (i18n/label :t/name-ens-or-address)
|
||||
:placeholder-text-color placeholder-text-color
|
||||
:default-value @value
|
||||
:auto-complete (when platform/ios? :none)
|
||||
:auto-capitalize :none
|
||||
:auto-correct false
|
||||
:keyboard-appearance (quo.theme/theme-value :light :dark theme)
|
||||
:on-focus on-focus
|
||||
:on-blur on-blur
|
||||
:on-change-text on-change}]
|
||||
(when (or (= @status :default)
|
||||
(= @status :active))
|
||||
[rn/view
|
||||
{:style style/buttons-container
|
||||
:accessibility-label :paste-scan-buttons-container}
|
||||
[button/button
|
||||
{:accessibility-label :paste-button
|
||||
:type :outline
|
||||
:size 24
|
||||
:container-style {:margin-right 8}
|
||||
:inner-style (style/accessory-button blur? theme)
|
||||
:on-press on-paste}
|
||||
(i18n/label :t/paste)]
|
||||
[button/button
|
||||
{:accessibility-label :scan-button
|
||||
:icon-only? true
|
||||
:type :outline
|
||||
:size 24
|
||||
:inner-style (style/accessory-button blur? theme)
|
||||
:on-press on-scan}
|
||||
:main-icons/scan]])
|
||||
(when (= @status :typing)
|
||||
[rn/view
|
||||
{:style style/buttons-container
|
||||
:accessibility-label :clear-button-container}
|
||||
[clear-button
|
||||
{:on-press on-clear
|
||||
:blur? blur?
|
||||
:theme theme}]])
|
||||
(when (and (= @status :loading) (not valid-ens?))
|
||||
[rn/view
|
||||
{:style style/buttons-container
|
||||
:accessibility-label :loading-button-container}
|
||||
[loading-icon blur? theme]])
|
||||
(when (and (= @status :loading) valid-ens?)
|
||||
[rn/view
|
||||
{:style style/buttons-container
|
||||
:accessibility-label :positive-button-container}
|
||||
[positive-state-icon theme]])]))))
|
||||
|
||||
(defn address-input-internal
|
||||
[props]
|
||||
[:f> f-address-input-internal props])
|
||||
|
||||
(def address-input
|
||||
(quo.theme/with-theme address-input-internal))
|
@ -52,6 +52,7 @@
|
||||
quo2.components.info.info-message
|
||||
quo2.components.info.information-box.view
|
||||
quo2.components.inputs.input.view
|
||||
quo2.components.inputs.address-input.view
|
||||
quo2.components.inputs.locked-input.view
|
||||
quo2.components.inputs.profile-input.view
|
||||
quo2.components.inputs.recovery-phrase.view
|
||||
@ -231,6 +232,7 @@
|
||||
|
||||
;;;; Inputs
|
||||
(def input quo2.components.inputs.input.view/input)
|
||||
(def address-input quo2.components.inputs.address-input.view/address-input)
|
||||
(def locked-input quo2.components.inputs.locked-input.view/locked-input)
|
||||
(def profile-input quo2.components.inputs.profile-input.view/profile-input)
|
||||
(def recovery-phrase-input quo2.components.inputs.recovery-phrase.view/recovery-phrase-input)
|
||||
|
@ -27,9 +27,10 @@
|
||||
[quo2.components.dropdowns.network-dropdown.component-spec]
|
||||
[quo2.components.gradient.gradient-cover.component-spec]
|
||||
[quo2.components.graph.wallet-graph.component-spec]
|
||||
[quo2.components.inputs.address-input.component-spec]
|
||||
[quo2.components.inputs.locked-input.component-spec]
|
||||
[quo2.components.inputs.input.component-spec]
|
||||
[quo2.components.inputs.profile-input.component-spec]
|
||||
[quo2.components.inputs.locked-input.component-spec]
|
||||
[quo2.components.inputs.recovery-phrase.component-spec]
|
||||
[quo2.components.inputs.title-input.component-spec]
|
||||
[quo2.components.keycard.component-spec]
|
||||
|
@ -182,6 +182,7 @@
|
||||
(def regx-universal-link #"((^https?://join.status.im/)|(^status-im://))[\x00-\x7F]+$")
|
||||
(def regx-community-universal-link #"((^https?://join.status.im/)|(^status-im://))c/([\x00-\x7F]+)$")
|
||||
(def regx-deep-link #"((^ethereum:.*)|(^status-im://[\x00-\x7F]+$))")
|
||||
(def regx-ens #"^(?=.{5,255}$)([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$")
|
||||
|
||||
(def ^:const dapp-permission-contact-code "contact-code")
|
||||
(def ^:const dapp-permission-web3 "web3")
|
||||
|
@ -0,0 +1,35 @@
|
||||
(ns status-im2.contexts.quo-preview.inputs.address-input
|
||||
(:require [quo2.core :as quo]
|
||||
[reagent.core :as reagent]
|
||||
[status-im2.contexts.quo-preview.preview :as preview]
|
||||
[status-im2.constants :as constants]))
|
||||
|
||||
(def descriptor
|
||||
[{:label "Scanned value:"
|
||||
:key :scanned-value
|
||||
:type :text}
|
||||
{:key :blur?
|
||||
:type :boolean}])
|
||||
|
||||
(defn view
|
||||
[]
|
||||
(let [state (reagent/atom {:scanned-value ""
|
||||
:blur? false
|
||||
:valid-ens? false})
|
||||
timer (atom nil)]
|
||||
(fn []
|
||||
[preview/preview-container
|
||||
{:state state
|
||||
:descriptor descriptor
|
||||
:blur? (:blur? @state)
|
||||
:show-blur-background? true}
|
||||
[quo/address-input
|
||||
(merge @state
|
||||
{:on-scan #(js/alert "Not implemented yet")
|
||||
:ens-regex constants/regx-ens
|
||||
:on-detect-ens (fn [_]
|
||||
(swap! state assoc :valid-ens? false)
|
||||
(when @timer
|
||||
(js/clearTimeout @timer))
|
||||
(reset! timer (js/setTimeout #(swap! state assoc :valid-ens? true)
|
||||
2000)))})]])))
|
@ -53,6 +53,7 @@
|
||||
[status-im2.contexts.quo-preview.info.info-message :as info-message]
|
||||
[status-im2.contexts.quo-preview.info.information-box :as information-box]
|
||||
[status-im2.contexts.quo-preview.inputs.input :as input]
|
||||
[status-im2.contexts.quo-preview.inputs.address-input :as address-input]
|
||||
[status-im2.contexts.quo-preview.inputs.locked-input :as locked-input]
|
||||
[status-im2.contexts.quo-preview.inputs.recovery-phrase-input :as recovery-phrase-input]
|
||||
[status-im2.contexts.quo-preview.inputs.profile-input :as profile-input]
|
||||
@ -235,6 +236,8 @@
|
||||
:component information-box/view}]
|
||||
:inputs [{:name :input
|
||||
:component input/view}
|
||||
{:name :address-input
|
||||
:component address-input/view}
|
||||
{:name :locked-input
|
||||
:component locked-input/view}
|
||||
{:name :profile-input
|
||||
|
@ -224,3 +224,8 @@
|
||||
(defn has-style
|
||||
[mock styles]
|
||||
(.toHaveStyle (js/expect mock) (clj->js styles)))
|
||||
|
||||
(defn has-prop
|
||||
([element prop] (has-prop element prop js/undefined))
|
||||
([element prop value]
|
||||
(.toHaveProp (js/expect element) (camel-snake-kebab/->camelCaseString prop) value)))
|
||||
|
@ -2305,5 +2305,6 @@
|
||||
"destroy": "Destroy",
|
||||
"mint": "Mint",
|
||||
"via": "via",
|
||||
"x-counter": "x{{counter}}"
|
||||
"x-counter": "x{{counter}}",
|
||||
"name-ens-or-address": "Name, ENS, or address"
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user