feat(wallet)_: Show buy and receive cta on zero balance (#21690)

This commit:

- adds wallet-card component
- adds Buy and Receive CTAs above the assets list in the wallet home if the balance is zero

Signed-off-by: Mohamed Javid <19339952+smohamedjavid@users.noreply.github.com>
This commit is contained in:
Mohamed Javid 2024-11-30 00:03:29 +05:30 committed by GitHub
parent 6859fb8f8e
commit 52a8f8fc22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 216 additions and 12 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,34 @@
(ns quo.components.cards.wallet-card.component-spec
(:require
[quo.components.cards.wallet-card.view :as wallet-card]
[quo.foundations.resources :as resources]
[test-helpers.component :as h]))
(def ^:private base-props
{:image (resources/get-image :keycard-logo)
:title "Buy"
:subtitle "Start investing now"})
(h/describe "cards: wallet card"
(h/test "Test default render"
(let [event (h/mock-fn)]
(h/render-with-theme-provider [wallet-card/view
(assoc base-props
:on-press
event)])
(h/is-truthy (h/get-by-label-text :wallet-card))
(h/is-truthy (h/get-by-text "Buy"))
(h/is-truthy (h/get-by-text "Start investing now"))
(h/is-truthy (h/get-by-label-text :image))
(h/fire-event :press (h/get-by-label-text :wallet-card))
(h/was-called event)))
(h/test "Test render with dismissible prop"
(let [event (h/mock-fn)]
(h/render-with-theme-provider [wallet-card/view
(assoc base-props
:dismissible? true
:on-press-close event)])
(h/is-truthy (h/get-by-label-text :close-icon))
(h/fire-event :press (h/get-by-label-text :icon-container))
(h/was-called event))))

View File

@ -0,0 +1,14 @@
(ns quo.components.cards.wallet-card.schema)
(def ?schema
[:=>
[:catn
[:props
[:map {:closed true}
[:image :schema.common/image-source]
[:title :string]
[:subtitle :string]
[:dismissible? {:optional true} :boolean]
[:on-press {:optional true} fn?]
[:on-press-close {:optional true} fn?]]]]
:any])

View File

@ -0,0 +1,30 @@
(ns quo.components.cards.wallet-card.style
(:require [quo.foundations.colors :as colors]
[quo.foundations.shadows :as shadows]))
(defn root-container
[theme]
(assoc (shadows/get 2 theme)
:border-radius 16
:padding-vertical 10
:padding-horizontal 12
:width 161
:background-color (colors/theme-colors colors/white colors/neutral-90 theme)))
(def top-container
{:flex-direction :row
:height 32
:justify-content :space-between
:margin-bottom 8})
(def image
{:height 32
:width 32})
(defn title
[theme]
{:color (colors/theme-colors colors/neutral-100 colors/white theme)})
(defn subtitle
[theme]
{:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)})

View File

@ -0,0 +1,44 @@
(ns quo.components.cards.wallet-card.view
(:require [quo.components.cards.wallet-card.schema :as component-schema]
[quo.components.cards.wallet-card.style :as style]
[quo.components.icon :as icon]
[quo.components.markdown.text :as text]
[quo.theme :as quo.theme]
[react-native.core :as rn]
[react-native.fast-image :as fast-image]
[schema.core :as schema]))
(defn- view-internal
[{:keys [image title subtitle dismissible? on-press on-press-close]}]
(let [theme (quo.theme/use-theme)]
[rn/pressable
{:on-press on-press
:accessibility-label :wallet-card}
[rn/view {:style (style/root-container theme)}
[rn/view {:style style/top-container}
[fast-image/fast-image
{:style style/image
:source image
:accessibility-label :image}]
(when dismissible?
[rn/pressable
{:on-press on-press-close
:accessibility-label :icon-container
:hit-slop {:top 5 :bottom 5 :left 5 :right 5}}
[icon/icon :i/close
{:size 12
:accessibility-label :close-icon}]])]
[text/text
{:style (style/title theme)
:size :paragraph-1
:weight :semi-bold
:number-of-lines 1}
title]
[text/text
{:style (style/subtitle theme)
:size :paragraph-2
:weight :regular
:number-of-lines 1}
subtitle]]]))
(def view (schema/instrument #'view-internal component-schema/?schema))

View File

@ -28,6 +28,7 @@
quo.components.calendar.calendar-day.view quo.components.calendar.calendar-day.view
quo.components.calendar.calendar-year.view quo.components.calendar.calendar-year.view
quo.components.calendar.calendar.view quo.components.calendar.calendar.view
quo.components.cards.wallet-card.view
quo.components.code.snippet-preview.view quo.components.code.snippet-preview.view
quo.components.code.snippet.view quo.components.code.snippet.view
quo.components.colors.color-picker.view quo.components.colors.color-picker.view
@ -241,6 +242,7 @@
;;;; Cards ;;;; Cards
(def small-option-card quo.components.onboarding.small-option-card.view/small-option-card) (def small-option-card quo.components.onboarding.small-option-card.view/small-option-card)
(def keycard quo.components.keycard.view/keycard) (def keycard quo.components.keycard.view/keycard)
(def wallet-card quo.components.cards.wallet-card.view/view)
;;;; Colors ;;;; Colors
(def color-picker quo.components.colors.color-picker.view/view) (def color-picker quo.components.colors.color-picker.view/view)

View File

@ -37,7 +37,9 @@
:nfc-success (js/require "../resources/images/ui2/nfc-success.png") :nfc-success (js/require "../resources/images/ui2/nfc-success.png")
:preparing-status (js/require "../resources/images/ui2/preparing-status.png") :preparing-status (js/require "../resources/images/ui2/preparing-status.png")
:syncing-devices (js/require "../resources/images/ui2/syncing_devices.png") :syncing-devices (js/require "../resources/images/ui2/syncing_devices.png")
:syncing-wrong (js/require "../resources/images/ui2/syncing_wrong.png")}) :syncing-wrong (js/require "../resources/images/ui2/syncing_wrong.png")
:buy (js/require "../resources/images/ui2/buy.png")
:receive (js/require "../resources/images/ui2/receive.png")})
(def ui-themed (def ui-themed
{:angry-man {:angry-man

View File

@ -0,0 +1,29 @@
(ns status-im.contexts.preview.quo.cards.wallet-card
(:require
[quo.core :as quo]
[reagent.core :as reagent]
[status-im.common.resources :as resources]
[status-im.contexts.preview.quo.preview :as preview]))
(def descriptor
[{:key :title
:type :text}
{:key :subtitle
:type :text}
{:key :dismissible?
:type :boolean}])
(defn view
[]
(let [state (reagent/atom {:image (resources/get-image :buy)
:title "Buy"
:subtitle "Start investing now"
:on-press #(js/alert "Item pressed")
:on-press-close #(js/alert "Close pressed")
:dismissible? false})]
(fn []
[preview/preview-container
{:state state
:descriptor descriptor
:component-container-style {:align-items :center}}
[quo/wallet-card @state]])))

View File

@ -34,6 +34,7 @@
[status-im.contexts.preview.quo.calendar.calendar :as calendar] [status-im.contexts.preview.quo.calendar.calendar :as calendar]
[status-im.contexts.preview.quo.calendar.calendar-day :as calendar-day] [status-im.contexts.preview.quo.calendar.calendar-day :as calendar-day]
[status-im.contexts.preview.quo.calendar.calendar-year :as calendar-year] [status-im.contexts.preview.quo.calendar.calendar-year :as calendar-year]
[status-im.contexts.preview.quo.cards.wallet-card :as wallet-card]
[status-im.contexts.preview.quo.code.snippet :as code-snippet] [status-im.contexts.preview.quo.code.snippet :as code-snippet]
[status-im.contexts.preview.quo.code.snippet-preview :as code-snippet-preview] [status-im.contexts.preview.quo.code.snippet-preview :as code-snippet-preview]
[status-im.contexts.preview.quo.colors.color :as color] [status-im.contexts.preview.quo.colors.color :as color]
@ -274,6 +275,8 @@
:component calendar-day/view} :component calendar-day/view}
{:name :calendar-year {:name :calendar-year
:component calendar-year/view}] :component calendar-year/view}]
:cards [{:name :wallet-card
:component wallet-card/view}]
:code [{:name :snippet :code [{:name :snippet
:component code-snippet/view} :component code-snippet/view}
{:name :snippet-preview {:name :snippet-preview

View File

@ -4,3 +4,9 @@
(def list-container (def list-container
{:padding-horizontal 8 {:padding-horizontal 8
:padding-bottom constants/floating-shell-button-height}) :padding-bottom constants/floating-shell-button-height})
(def buy-and-receive-cta-container
{:flex-direction :row
:justify-content :space-between
:padding-horizontal 20
:padding-vertical 8})

View File

@ -2,14 +2,39 @@
(:require (:require
[quo.core :as quo] [quo.core :as quo]
[react-native.core :as rn] [react-native.core :as rn]
[status-im.common.resources :as resources]
[status-im.contexts.wallet.common.token-value.view :as token-value] [status-im.contexts.wallet.common.token-value.view :as token-value]
[status-im.contexts.wallet.home.tabs.assets.style :as style] [status-im.contexts.wallet.home.tabs.assets.style :as style]
[status-im.contexts.wallet.sheets.buy-token.view :as buy-token]
[utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(defn view (defn view
[] []
(let [tokens-loading? (rf/sub [:wallet/home-tokens-loading?]) (let [tokens-loading? (rf/sub [:wallet/home-tokens-loading?])
{:keys [tokens]} (rf/sub [:wallet/aggregated-token-values-and-balance])] {:keys [tokens]} (rf/sub [:wallet/aggregated-token-values-and-balance])
zero-balance? (rf/sub [:wallet/zero-balance-in-all-non-watched-accounts?])
buy-assets (rn/use-callback
(fn []
(rf/dispatch [:show-bottom-sheet
{:content buy-token/view}])))
receive-assets (rn/use-callback
(fn []
(rf/dispatch [:open-modal :screen/share-shell {:initial-tab :wallet}])))]
[:<>
(when (and (some? tokens-loading?) (not tokens-loading?) zero-balance?)
[rn/view
{:style style/buy-and-receive-cta-container}
[quo/wallet-card
{:image (resources/get-image :buy)
:title (i18n/label :t/ways-to-buy)
:subtitle (i18n/label :t/via-card-or-bank)
:on-press buy-assets}]
[quo/wallet-card
{:image (resources/get-image :receive)
:title (i18n/label :t/receive)
:subtitle (i18n/label :t/deposit-to-your-wallet)
:on-press receive-assets}]])
(if tokens-loading? (if tokens-loading?
[quo/skeleton-list [quo/skeleton-list
{:content :assets {:content :assets
@ -19,4 +44,4 @@
{:render-fn token-value/view {:render-fn token-value/view
:data tokens :data tokens
:render-data {:entry-point :wallet-stack} :render-data {:entry-point :wallet-stack}
:content-container-style style/list-container}]))) :content-container-style style/list-container}])]))

View File

@ -8,6 +8,7 @@
[status-im.contexts.wallet.send.utils :as send-utils] [status-im.contexts.wallet.send.utils :as send-utils]
[status-im.contexts.wallet.sheets.missing-keypair.view :as missing-keypair] [status-im.contexts.wallet.sheets.missing-keypair.view :as missing-keypair]
[status-im.subs.wallet.add-account.address-to-watch] [status-im.subs.wallet.add-account.address-to-watch]
[utils.money :as money]
[utils.number] [utils.number]
[utils.security.core :as security])) [utils.security.core :as security]))
@ -694,6 +695,17 @@
:formatted-balance formatted-balance :formatted-balance formatted-balance
:tokens sorted-token-values}))) :tokens sorted-token-values})))
(rf/reg-sub
:wallet/zero-balance-in-all-non-watched-accounts?
:<- [:wallet/aggregated-tokens]
:<- [:profile/currency]
:<- [:wallet/prices-per-token]
(fn [[aggregated-tokens currency prices-per-token]]
(let [balance (utils/calculate-balance-from-tokens {:currency currency
:tokens aggregated-tokens
:prices-per-token prices-per-token})]
(and (not-empty aggregated-tokens) (money/equal-to balance 0)))))
(rf/reg-sub (rf/reg-sub
:wallet/network-preference-details :wallet/network-preference-details
:<- [:wallet/current-viewing-account] :<- [:wallet/current-viewing-account]

View File

@ -761,6 +761,7 @@
"deleted-this-message": "deleted this message", "deleted-this-message": "deleted this message",
"delivered": "Delivered", "delivered": "Delivered",
"deny": "Deny", "deny": "Deny",
"deposit-to-your-wallet": "Deposit to your wallet",
"derivation-path": "Derivation path", "derivation-path": "Derivation path",
"derivation-path-copied": "Derivation path copied", "derivation-path-copied": "Derivation path copied",
"derivation-path-desc": "Derivation paths are the routes your Status Wallet uses to generate addresses from your private key.", "derivation-path-desc": "Derivation paths are the routes your Status Wallet uses to generate addresses from your private key.",
@ -2750,6 +2751,7 @@
"verified-community": "✓ Verified community", "verified-community": "✓ Verified community",
"version": "App version", "version": "App version",
"via": "via", "via": "via",
"via-card-or-bank": "Via card or bank",
"view": "View", "view": "View",
"view-address-on-arbiscan": "View address on Arbiscan", "view-address-on-arbiscan": "View address on Arbiscan",
"view-address-on-etherscan": "View address on Etherscan", "view-address-on-etherscan": "View address on Etherscan",
@ -2848,6 +2850,7 @@
"watch-only": "Watch-only", "watch-only": "Watch-only",
"watched-account-removed": "Watched address has been removed", "watched-account-removed": "Watched address has been removed",
"watched-address": "Watched address", "watched-address": "Watched address",
"ways-to-buy": "Ways to buy",
"ways-to-buy-assets": "Ways to buy assets", "ways-to-buy-assets": "Ways to buy assets",
"wc-brand-guide": "Guidance on using branding such as trademarks and logos", "wc-brand-guide": "Guidance on using branding such as trademarks and logos",
"wc-disclaimer": "Disclaimers (including third party providers), warranties, and legal releases", "wc-disclaimer": "Disclaimers (including third party providers), warranties, and legal releases",