feat: implement address input (#17191)

Signed-off-by: Brian Sztamfater <brian@status.im>
This commit is contained in:
Brian Sztamfater 2023-09-13 10:26:29 -03:00 committed by GitHub
parent 9706111256
commit baa9dff237
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 414 additions and 2 deletions

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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