Implemented Wallet - Account Switcher (#18003)

This commit:

- Implements the Wallet Account Switcher for easy switching between wallet accounts
- Updates the About tab in the accounts screen to display the correct account address and derivation path along with the profile.
- Updates the account-item component to pass the state from the parent and refactors the depreciated color functions
- Moves the handle-bar in the bottom sheet to a standalone component 
- Adds customization-color in account-origin component

---------

Signed-off-by: Mohamed Javid <19339952+smohamedjavid@users.noreply.github.com>
This commit is contained in:
Mohamed Javid 2023-12-04 18:47:03 +05:30 committed by GitHub
parent a7178a4950
commit 56d135f1f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 293 additions and 249 deletions

View File

@ -0,0 +1,17 @@
(ns quo.components.ios.drawer-bar.style
(:require
[quo.foundations.colors :as colors]
[quo.theme :as theme]))
(def handle-container
{:padding-vertical 8
:height 20
:align-items :center})
(defn handle
[{:keys [theme]}]
{:width 32
:height 4
:background-color (colors/theme-colors colors/neutral-100 colors/white theme)
:opacity (theme/theme-value 0.05 0.1 theme)
:border-radius 100})

View File

@ -0,0 +1,12 @@
(ns quo.components.ios.drawer-bar.view
(:require
[quo.components.ios.drawer-bar.style :as style]
[quo.theme :as quo.theme]
[react-native.core :as rn]))
(defn- view-internal
[props]
[rn/view {:style style/handle-container}
[rn/view {:style (style/handle props)}]])
(def view (quo.theme/with-theme view-internal))

View File

@ -13,7 +13,7 @@
(h/render [account/view])
(h/fire-event :on-press-in (h/get-by-label-text :container))
(h/wait-for #(h/has-style (h/query-by-label-text :container)
{:backgroundColor (colors/custom-color :blue 50 5)})))
{:backgroundColor (colors/resolve-color :blue :light 5)})))
(h/test "on-press-in changes state to :pressed with blur? enabled"
(h/render [account/view {:blur? true}])
@ -21,31 +21,26 @@
(h/wait-for #(h/has-style (h/query-by-label-text :container)
{:backgroundColor colors/white-opa-5})))
(h/test "on-press-out changes state to :active"
(h/render [account/view])
(h/fire-event :on-press-in (h/get-by-label-text :container))
(h/fire-event :on-press-out (h/get-by-label-text :container))
(h/wait-for #(h/has-style (h/query-by-label-text :container)
{:backgroundColor (colors/custom-color :blue 50 10)})))
(h/test "render with state :active"
(h/render [account/view {:state :active}])
(h/has-style (h/query-by-label-text :container)
{:backgroundColor (colors/resolve-color :blue :light 10)}))
(h/test "on-press-out changes state to :active with blur? enabled"
(h/render [account/view {:blur? true}])
(h/fire-event :on-press-in (h/get-by-label-text :container))
(h/fire-event :on-press-out (h/get-by-label-text :container))
(h/wait-for #(h/has-style (h/query-by-label-text :container)
{:backgroundColor colors/white-opa-10})))
(h/test "render with state :active and blur? enabled"
(h/render [account/view
{:blur? true
:state :active}])
(h/has-style (h/query-by-label-text :container)
{:backgroundColor colors/white-opa-10}))
(h/test "on-press-out changes state to :selected"
(h/render [account/view {:selectable? true}])
(h/fire-event :on-press-in (h/get-by-label-text :container))
(h/fire-event :on-press-out (h/get-by-label-text :container))
(h/wait-for #(h/is-truthy (h/query-by-label-text :check-icon))))
(h/test "render with state :selected"
(h/render [account/view {:state :selected}])
(h/is-truthy (h/query-by-label-text :check-icon)))
(h/test "on-press-out calls on-press"
(h/test "calls on-press"
(let [on-press (h/mock-fn)]
(h/render [account/view {:on-press on-press}])
(h/fire-event :on-press-in (h/get-by-label-text :container))
(h/fire-event :on-press-out (h/get-by-label-text :container))
(h/fire-event :on-press (h/get-by-label-text :container))
(h/was-called on-press)))
(h/test "renders token props if type :tag"

View File

@ -3,12 +3,12 @@
[quo.foundations.colors :as colors]))
(defn- background-color
[{:keys [state blur? customization-color]}]
(cond (or (= state :pressed) (= state :selected))
(if blur? colors/white-opa-5 (colors/custom-color customization-color 50 5))
[{:keys [state pressed? blur? customization-color]}]
(cond (or pressed? (= state :selected))
(if blur? colors/white-opa-5 (colors/resolve-color customization-color :light 5))
(= state :active)
(if blur? colors/white-opa-10 (colors/custom-color customization-color 50 10))
(and (= state :pressed) blur?) colors/white-opa-10
(if blur? colors/white-opa-10 (colors/resolve-color customization-color :light 10))
(and pressed? blur?) colors/white-opa-10
:else :transparent))
(defn container

View File

@ -94,62 +94,42 @@
[icon/icon :i/check
{:color (if blur?
colors/white
(colors/theme-colors (colors/custom-color customization-color 50)
(colors/custom-color customization-color 60)
theme))}]])
(colors/resolve-color customization-color theme))}]])
(defn- f-internal-view
[]
(let [state (reagent/atom :default)
active-or-selected? (atom false)
timer (atom nil)
on-press-in (fn []
(when-not (= @state :selected)
(reset! timer (js/setTimeout #(reset! state :pressed) 100))))]
(fn [{:keys [type selectable? blur? customization-color on-press]
(defn- internal-view
[_]
(let [pressed? (reagent/atom false)
on-press-in #(reset! pressed? true)
on-press-out #(reset! pressed? false)]
(fn [{:keys [type state blur? customization-color on-press]
:or {customization-color :blue
type :default
state :default
blur? false}
:as props}]
(let [on-press-out (fn []
(let [new-state (if @active-or-selected?
:default
(if (and (= type :default) selectable?)
:selected
:active))]
(when @timer (js/clearTimeout @timer))
(reset! timer nil)
(reset! active-or-selected? (or (= new-state :active)
(= new-state :selected)))
(reset! state new-state)
(when on-press
(on-press))))]
(rn/use-effect
#(cond (and selectable? (= type :default) (= @state :active)) (reset! state :selected)
(and (not selectable?) (= type :default) (= @state :selected)) (reset! state :active))
[selectable?])
[rn/pressable
{:style (style/container
{:state @state :blur? blur? :customization-color customization-color})
{:state state
:blur? blur?
:customization-color customization-color
:pressed? @pressed?})
:on-press-in on-press-in
:on-press on-press
:on-press-out on-press-out
:accessibility-label :container}
[account-view props]
[rn/view {:style (when (= type :tag) style/token-tag-container)}
(when (or (= type :balance-neutral)
(= type :balance-negative)
(= type :balance-positive))
[balance-view props])
(when (= type :tag)
[token-tag props])
(when (= type :action)
[options-button props])
(when (and (= type :default)
(= @state :selected))
[check-icon props])]]))))
(cond
(#{:balance-neutral :balance-negative :balance-positive} type)
[balance-view props]
(defn- internal-view
[props]
[:f> f-internal-view props])
(= type :tag)
[token-tag props]
(= type :action)
[options-button props]
(and (= type :default) (= state :selected))
[check-icon props])]])))
(def view (quo.theme/with-theme internal-view))

View File

@ -20,11 +20,12 @@
(i18n/label :t/trip-accounts))])
(defn- row-icon
[profile-picture type secondary-color]
[customization-color profile-picture type secondary-color]
(case type
:default-keypair [user-avatar/user-avatar
{:size :xxs
:ring? false
:customization-color customization-color
:profile-picture profile-picture}]
:recovery-phrase [icons/icon
:i/seed
@ -41,10 +42,11 @@
nil))
(defn- row-view
[{:keys [type theme secondary-color profile-picture title stored subtitle on-press]}]
[{:keys [type theme secondary-color customization-color profile-picture title stored subtitle
on-press]}]
[rn/view {:style (style/row-container type theme)}
[rn/view {:style style/icon-container}
[row-icon profile-picture type secondary-color]]
[row-icon customization-color profile-picture type secondary-color]]
[rn/view
{:style style/row-content-container}
[row-title type title]
@ -68,13 +70,14 @@
{:color secondary-color}]])])
(defn- list-view
[{:keys [type stored profile-picture user-name theme secondary-color]}]
[{:keys [type stored customization-color profile-picture user-name theme secondary-color]}]
(let [stored-name (if (= :on-device stored)
(i18n/label :t/on-device)
(i18n/label :t/on-keycard))]
[row-view
{:type type
:stored stored
:customization-color customization-color
:profile-picture profile-picture
:title user-name
:subtitle stored-name
@ -106,10 +109,11 @@
(def view
"Create an account-origin UI component.
| key | values |
| ------------------|------------------------------------------------|
| ----------------------|------------------------------------------------|
| :type | :default-keypair :recovery-phrase :private-key
| :stored | :on-device :on-keycard
| :profile-picture | image source
| :customization-color | profile color
| :derivation-path | string
| :user-name | string
| :on-press | function"

View File

@ -66,6 +66,7 @@
quo.components.inputs.recovery-phrase.view
quo.components.inputs.search-input.view
quo.components.inputs.title-input.view
quo.components.ios.drawer-bar.view
quo.components.keycard.view
quo.components.links.link-preview.view
quo.components.links.url-preview-list.view
@ -268,6 +269,9 @@
(def search-input quo.components.inputs.search-input.view/search-input)
(def title-input quo.components.inputs.title-input.view/view)
;;;; iOS
(def drawer-bar quo.components.ios.drawer-bar.view/view)
;;;; Numbered Keyboard
(def keyboard-key quo.components.numbered-keyboard.keyboard-key.view/view)
(def numbered-keyboard quo.components.numbered-keyboard.numbered-keyboard.view/view)

View File

@ -1,19 +1,8 @@
(ns status-im2.common.bottom-sheet.style
(:require
[quo.foundations.colors :as colors]
[quo.theme :as theme]
[react-native.platform :as platform]))
(defn handle
[theme]
{:width 32
:height 4
:background-color (colors/theme-colors colors/neutral-100 colors/white theme)
:opacity (theme/theme-value 0.05 0.1 theme)
:border-radius 100
:align-self :center
:margin-vertical 8})
(defn sheet
[{:keys [top]} window-height selected-item]
{:position :absolute

View File

@ -63,7 +63,7 @@
item-height (reagent/atom 0)]
(fn [{:keys [hide? insets theme]}
{:keys [content selected-item padding-bottom-override border-radius on-close shell?
gradient-cover? customization-color]
gradient-cover? customization-color hide-handle?]
:or {border-radius 12}}]
(let [{window-height :height} (rn/get-window)
bg-opacity (reanimated/use-shared-value 0)
@ -122,7 +122,8 @@
[quo/gradient-cover
{:customization-color customization-color
:opacity 0.4}]])
[rn/view {:style (style/handle theme)}]
(when-not hide-handle?
[quo/drawer-bar])
[content]]]]]))))
(defn- internal-view

View File

@ -0,0 +1,9 @@
(ns status-im2.contexts.quo-preview.ios.drawer-bar
(:require
[quo.core :as quo]
[status-im2.contexts.quo-preview.preview :as preview]))
(defn view
[]
[preview/preview-container {}
[quo/drawer-bar]])

View File

@ -9,8 +9,10 @@
:type :select
:options [{:key :default} {:key :balance-positive} {:key :balance-neutral} {:key :balance-negative}
{:key :tag} {:key :action}]}
{:key :selectable? :type :boolean}
{:key :title-icon? :type :boolean}
{:key :state
:type :select
:options [{:key :active} {:key :selected}]}
{:key :emoji
:type :text}
(preview/customization-color-option {:key :account-color})
@ -20,8 +22,8 @@
(defn view
[]
(let [state (reagent/atom {:type :default
:selectable? false
:title-icon? false
:state :default
:customization-color :blue
:account-color :purple
:emoji "🍑"

View File

@ -78,6 +78,7 @@
recovery-phrase-input]
[status-im2.contexts.quo-preview.inputs.search-input :as search-input]
[status-im2.contexts.quo-preview.inputs.title-input :as title-input]
[status-im2.contexts.quo-preview.ios.drawer-bar :as drawer-bar]
[status-im2.contexts.quo-preview.keycard.keycard :as keycard]
[status-im2.contexts.quo-preview.links.link-preview :as link-preview]
[status-im2.contexts.quo-preview.links.url-preview :as url-preview]
@ -309,6 +310,8 @@
:component search-input/view}
{:name :title-input
:component title-input/view}]
:ios [{:name :drawer-bar
:component drawer-bar/view}]
:numbered-keyboard [{:name :keyboard-key
:component keyboard-key/view}
{:name :numbered-keyboard

View File

@ -2,8 +2,8 @@
(:require
[quo.core :as quo]
[react-native.core :as rn]
[status-im2.contexts.profile.utils :as profile.utils]
[status-im2.contexts.wallet.account.tabs.about.style :as style]
[status-im2.contexts.wallet.common.temp :as temp]
[utils.i18n :as i18n]
[utils.re-frame :as rf]))
@ -34,7 +34,8 @@
(defn view
[]
(let [{:keys [type address]} (rf/sub [:wallet/current-viewing-account])
(let [{:keys [customization-color] :as profile} (rf/sub [:profile/profile-with-image])
{:keys [type address path]} (rf/sub [:wallet/current-viewing-account])
networks (rf/sub [:wallet/network-details])
watch-only? (= type :watch)]
[rn/view {:style style/about-tab}
@ -53,4 +54,12 @@
:format :long}])
:container-style {:margin-bottom 12}
:on-press #(rf/dispatch [:show-bottom-sheet {:content about-options}])}]
(when (not watch-only?) [quo/account-origin temp/account-origin-state])]))
(when (not watch-only?)
[quo/account-origin
{:type :default-keypair
:stored :on-device
:profile-picture (profile.utils/photo profile)
:customization-color customization-color
:derivation-path path
:user-name (profile.utils/displayed-name profile)
:on-press #(js/alert "To be implemented")}])]))

View File

@ -9,6 +9,7 @@
networks (rf/sub [:wallet/network-details])]
[quo/page-nav
{:icon-name :i/close
:background :blur
:on-press on-press
:accessibility-label accessibility-label
:networks networks
@ -17,6 +18,5 @@
:account-switcher {:customization-color color
:on-press #(rf/dispatch [:show-bottom-sheet
{:content account-options/view
:gradient-cover? true
:customization-color color}])
:hide-handle? true}])
:emoji emoji}}]))

View File

@ -4,3 +4,33 @@
{:padding-horizontal 20
:padding-top 12
:padding-bottom 8})
(def options-container
{:position :absolute
:top 0
:left 0
:right 0
:z-index 1
:overflow :hidden})
(defn blur-container
[height]
{:height height
:position :absolute
:top 0
:left 0
:right 0})
(def gradient-container
{:position :absolute
:top 0
:left 0
:right 0})
(def divider-label
{:margin-top 8})
(defn list-container
[padding-top]
{:padding-top padding-top
:margin-horizontal 8})

View File

@ -1,18 +1,46 @@
(ns status-im2.contexts.wallet.common.sheets.account-options.view
(:require [quo.core :as quo]
(:require [oops.core :as oops]
[quo.core :as quo]
[quo.foundations.colors :as colors]
quo.theme
[react-native.blur :as blur]
[react-native.clipboard :as clipboard]
[react-native.core :as rn]
[react-native.gesture :as gesture]
[react-native.platform :as platform]
[reagent.core :as reagent]
[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-internal
[{:keys [theme]}]
(defn- render-account-item
[{:keys [color address] :as account}]
[quo/account-item
{:account-props (assoc account :customization-color color)
:on-press (fn []
(rf/dispatch [:wallet/switch-current-viewing-account address])
(rf/dispatch [:hide-bottom-sheet]))}])
(defn- options
[{:keys [theme show-account-selector? options-height]}]
(let [{:keys [name color emoji address]} (rf/sub [:wallet/current-viewing-account])]
[:<>
[rn/view
{:on-layout #(reset! options-height (oops/oget % "nativeEvent.layout.height"))
:style (when show-account-selector? style/options-container)}
(when show-account-selector?
[blur/view
{:style (style/blur-container @options-height)
:blur-radius (if platform/android? 20 10)
:blur-amount (if platform/ios? 20 10)
:blur-type (quo.theme/theme-value (if platform/ios? :light :xlight) :dark theme)
:overlay-color (quo.theme/theme-value colors/white-70-blur
colors/neutral-95-opa-70-blur
theme)}])
[rn/view {:style style/gradient-container}
[quo/gradient-cover
{:customization-color color
:opacity 0.4}]]
[quo/drawer-bar]
[quo/drawer-top
{:title name
:type :account
@ -39,17 +67,34 @@
{:icon :i/share
:accessibility-label :share-account
:label (i18n/label :t/share-account)}
{:icon :i/delete
{:add-divider? (not show-account-selector?)
:icon :i/delete
:accessibility-label :remove-account
:label (i18n/label :t/remove-account)
:danger? true}]]]
[quo/divider-line {:container-style {:margin-top 8}}]
(when show-account-selector?
[:<>
[quo/divider-line {:container-style style/divider-label}]
[quo/section-label
{:section (i18n/label :t/select-another-account)
:container-style style/drawer-section-label}]
[rn/flat-list
{:data temp/other-accounts
:render-fn (fn [account] [quo/account-item {:account-props account}])
:style {:margin-horizontal 8}}]]))
:container-style style/drawer-section-label}]])]))
(defn- view-internal
[]
(let [options-height (reagent/atom 0)]
(fn [{:keys [theme]}]
(let [accounts (rf/sub [:wallet/accounts-without-current-viewing-account])
show-account-selector? (pos? (count accounts))]
[:<>
(when show-account-selector?
[gesture/flat-list
{:data accounts
:render-fn render-account-item
:content-container-style (style/list-container @options-height)
:shows-vertical-scroll-indicator false}])
[options
{:show-account-selector? show-account-selector?
:theme theme
:options-height options-height}]]))))
(def view (quo.theme/with-theme view-internal))

View File

@ -1,10 +1,8 @@
(ns status-im2.contexts.wallet.common.temp
(:require
[clojure.string :as string]
[quo.foundations.resources :as quo.resources]
[react-native.core :as rn]
[status-im2.common.resources :as status.resources]
[status-im2.constants :as constants]
[utils.i18n :as i18n]))
(defn wallet-overview-state
@ -48,112 +46,6 @@
(def address "0x39cf6E0Ba4C4530735616e1Ee7ff5FbCB726fBd4")
(def data-item-state
{:description :default
:icon-right? true
:right-icon :i/options
:card? true
:label :none
:status :default
:size :default
:title "Address"
:customization-color :yellow})
(def account-origin-state
{:type :default-keypair
:stored :on-keycard
:profile-picture (status.resources/get-mock-image :user-picture-male5)
:derivation-path (string/replace constants/path-default-wallet #"/" " / ")
:user-name "Alisher Yakupov"
:on-press #(js/alert "pressed")})
(defn dapps-list
[{:keys [on-press-icon]}]
[{:dapp {:avatar (quo.resources/get-dapp :coingecko)
:name "Coingecko"
:value "coingecko.com"}
:state :default
:action :icon
:on-press-icon on-press-icon}
{:dapp {:avatar (quo.resources/get-dapp :uniswap)
:name "Uniswap"
:value "uniswap.org"}
:state :default
:action :icon
:on-press-icon on-press-icon}])
(def other-accounts
[{:customization-color :flamingo
:emoji "🍿"
:name "New House"
:address "0x21af6E0Ba4C4530735616e1Ee7ff5FbCB726f493"
:networks [{:network-name :ethereum :short-name "eth"}
{:network-name :optimism :short-name "opt"}]}
{:customization-color :blue
:emoji "🎮"
:name "My savings"
:address "0x43cf6E0Ba4C4530735616e1Ee7ff5FbCB726f98d"
:networks [{:network-name :ethereum :short-name "eth"}]}])
(def asset-snt
{:size 24
:type :token
:token-name "SNT"
:amount 1500
:token-logo (quo.resources/get-token :snt)})
(def piggy-bank
{:size 24
:type :account
:account-name "Piggy bank"
:emoji "🐷"})
(def aretha-gosling
{:size 24
:type :default
:full-name "Aretha Gosling"
:profile-picture (status.resources/mock-images :user-picture-female2)})
(def mainnet
{:size 24
:type :network
:network-logo (quo.resources/get-network :ethereum)
:network-name "Mainnet"})
(def activity-list
[{:date "Today"
:transaction :send
:timestamp "Today 22:20"
:status :pending
:counter 1
:first-tag asset-snt
:second-tag-prefix :t/from
:second-tag piggy-bank
:third-tag-prefix :t/to
:third-tag aretha-gosling
:fourth-tag-prefix :t/via
:fourth-tag mainnet
:blur? false}
{:date "Yesterday"
:transaction :receive
:timestamp "Yesterday 22:20"
:status :pending
:counter 1
:first-tag asset-snt
:second-tag-prefix :t/from
:second-tag piggy-bank
:third-tag-prefix :t/to
:third-tag aretha-gosling
:fourth-tag-prefix :t/via
:fourth-tag mainnet
:blur? false}])
(def collectible-list
[(status.resources/get-mock-image :collectible1)
(status.resources/get-mock-image :collectible2)
(status.resources/get-mock-image :collectible3)
(status.resources/get-mock-image :collectible4)])
(def buy-tokens-list
[{:title "Ramp"
:description :text

View File

@ -45,6 +45,10 @@
:ms 300}]]
[:dispatch [:wallet/show-account-created-toast address]]]}))
(rf/reg-event-fx :wallet/switch-current-viewing-account
(fn [{:keys [db]} [address]]
{:db (assoc-in db [:wallet :current-viewing-account-address] address)}))
(rf/reg-event-fx :wallet/close-account-page
(fn [{:keys [db]}]
{:db (update db :wallet dissoc :current-viewing-account-address)

View File

@ -86,5 +86,16 @@
(string/starts-with? (string/lower-case (:symbol %))
(string/lower-case query)))
sorted-tokens)]
(println filtered-tokens "3421342342432")
filtered-tokens)))
(rf/reg-sub
:wallet/current-viewing-account-address
:<- [:wallet]
:-> :current-viewing-account-address)
(rf/reg-sub
:wallet/accounts-without-current-viewing-account
:<- [:wallet/accounts]
:<- [:wallet/current-viewing-account-address]
(fn [[accounts current-viewing-account-address]]
(remove #(= (:address %) current-viewing-account-address) accounts)))

View File

@ -193,3 +193,40 @@
(testing "watch address activity state with has-activity value"
(swap! rf-db/app-db #(assoc-in % [:wallet :ui :watch-address-activity-state] :has-activity))
(is (= :has-activity (rf/sub [sub-name])))))
(h/deftest-sub :wallet/current-viewing-account-address
[sub-name]
(testing "returns the address of the current viewing account"
(swap! rf-db/app-db #(assoc-in % [:wallet :current-viewing-account-address] "0x1"))
(is (= "0x1" (rf/sub [sub-name])))))
(h/deftest-sub :wallet/accounts-without-current-viewing-account
[sub-name]
(testing "returns the accounts list without the current viewing account in it"
(swap! rf-db/app-db
#(-> %
(assoc-in [:wallet :accounts] accounts)
(assoc-in [:wallet :current-viewing-account-address] "0x2")))
(is
(= (list
{:path "m/44'/60'/0'/0/0"
:emoji "😃"
:key-uid "0x2f5ea39"
:address "0x1"
:wallet false
:name "Account One"
:type :generated
:chat false
:test-preferred-chain-ids #{5 420 421613}
:color :blue
:hidden false
:prod-preferred-chain-ids #{1 10 42161}
:position 0
:clock 1698945829328
:created-at 1698928839000
:operable "fully"
:mixedcase-address "0x7bcDfc75c431"
:public-key "0x04371e2d9d66b82f056bc128064"
:removed false
:tokens tokens-0x1})
(rf/sub [sub-name])))))