From 21c2a525cf7c1f8b9247675ed00cb82b406f2126 Mon Sep 17 00:00:00 2001 From: Mohsen Date: Fri, 22 Nov 2024 16:12:00 +0300 Subject: [PATCH] [#21557] feat: show from account page in swap flow (#21611) * [#21557] feat: show from account page in swap flow * [#21557] feat: add disabled state to account-item * [#21557] fix: apply suggestion * [#21557] fix: add format address util and rename screen * [#21557] fix: check and count account with token balance * [#21557] fix: check for root screen --- .../components/list_items/account/schema.cljs | 2 +- .../components/list_items/account/style.cljs | 3 +- .../components/list_items/account/view.cljs | 1 + .../contexts/wallet/common/utils.cljs | 26 +++++- .../wallet/common/utils/networks.cljs | 9 +++ .../contexts/wallet/common/utils_test.cljs | 65 +++++++++++++++ .../contexts/wallet/swap/events.cljs | 81 ++++++++++++------- .../wallet/swap/select_account/style.cljs | 7 ++ .../wallet/swap/select_account/view.cljs | 52 ++++++++++++ src/status_im/navigation/screens.cljs | 7 ++ src/status_im/subs/wallet/networks.cljs | 9 +-- src/status_im/subs/wallet/swap.cljs | 21 +++++ 12 files changed, 242 insertions(+), 41 deletions(-) create mode 100644 src/status_im/contexts/wallet/swap/select_account/style.cljs create mode 100644 src/status_im/contexts/wallet/swap/select_account/view.cljs diff --git a/src/quo/components/list_items/account/schema.cljs b/src/quo/components/list_items/account/schema.cljs index dd55d91679..f7890355c3 100644 --- a/src/quo/components/list_items/account/schema.cljs +++ b/src/quo/components/list_items/account/schema.cljs @@ -23,7 +23,7 @@ [:map [:type {:optional true} [:enum :default :tag :action :balance-neutral :balance-negative :balance-positive]] - [:state {:optional true} [:enum :default :selected :active]] + [:state {:optional true} [:enum :default :selected :active :disabled]] [:blur? {:optional true} [:maybe :boolean]] [:customization-color {:optional true} [:maybe :schema.common/customization-color]] [:on-press {:optional true} [:maybe fn?]] diff --git a/src/quo/components/list_items/account/style.cljs b/src/quo/components/list_items/account/style.cljs index 0af1b1cb23..d94e1badc0 100644 --- a/src/quo/components/list_items/account/style.cljs +++ b/src/quo/components/list_items/account/style.cljs @@ -12,12 +12,13 @@ :else :transparent)) (defn container - [props] + [{:keys [state] :as props}] {:height 56 :border-radius 12 :background-color (background-color props) :flex-direction :row :align-items :center + :opacity (if (= state :disabled) 0.3 1) :padding-horizontal 12 :padding-vertical 6 :justify-content :space-between}) diff --git a/src/quo/components/list_items/account/view.cljs b/src/quo/components/list_items/account/view.cljs index 4d732f8c20..8df9828317 100644 --- a/src/quo/components/list_items/account/view.cljs +++ b/src/quo/components/list_items/account/view.cljs @@ -112,6 +112,7 @@ :pressed? pressed?}) :on-press-in on-press-in :on-press on-press + :disabled (= state :disabled) :on-press-out on-press-out :accessibility-label :container} [account-view props] diff --git a/src/status_im/contexts/wallet/common/utils.cljs b/src/status_im/contexts/wallet/common/utils.cljs index a51c3e6361..e4e856cf77 100644 --- a/src/status_im/contexts/wallet/common/utils.cljs +++ b/src/status_im/contexts/wallet/common/utils.cljs @@ -193,7 +193,7 @@ (defn token-balance-display-for-network "Formats a token balance for a specific chain and rounds it to a specified number of decimals. - If the balance is less than the smallest representable value based on rounding decimals, + If the balance is less than the smallest representable value based on rounding decimals, a threshold value is displayed instead." [token chain-id rounding-decimals] (let [token-decimals (:decimals token) @@ -548,3 +548,27 @@ (:less-than-three-minutes constants/wallet-transaction-estimation) "1-3" (:less-than-five-minutes constants/wallet-transaction-estimation) "3-5" ">5")) + +(defn get-accounts-with-token-balance + [accounts token] + (let [operable-account (filter :operable? (vals accounts)) + positive-balance-in-any-chain? (fn [{:keys [balances-per-chain]}] + (->> balances-per-chain + (map (comp :raw-balance val)) + (some pos?))) + addresses-tokens-with-positive-balance (as-> operable-account $ + (group-by :address $) + (update-vals $ + #(filter positive-balance-in-any-chain? + (:tokens (first %)))))] + (if-let [asset-symbol (:symbol token)] + (let [addresses-with-asset (as-> addresses-tokens-with-positive-balance $ + (update-vals $ #(set (map :symbol %))) + (keep (fn [[address token-symbols]] + (when (token-symbols asset-symbol) address)) + $) + (set $))] + (filter #(addresses-with-asset (:address %)) operable-account)) + (filter (fn [{:keys [tokens]}] + (some positive-balance-in-any-chain? tokens)) + operable-account)))) diff --git a/src/status_im/contexts/wallet/common/utils/networks.cljs b/src/status_im/contexts/wallet/common/utils/networks.cljs index 6d60bc8d46..b363d4f3d6 100644 --- a/src/status_im/contexts/wallet/common/utils/networks.cljs +++ b/src/status_im/contexts/wallet/common/utils/networks.cljs @@ -7,6 +7,7 @@ [utils.number])) (def ^:private last-comma-followed-by-text-to-end-regex #",\s(?=[^,]+$)") +(def ^:private max-network-prefixes 2) (def id->network {constants/ethereum-mainnet-chain-id constants/mainnet-network-name @@ -185,3 +186,11 @@ :related-chain-id related-chain-id :layer layer))) (sort-by (juxt :layer :short-name)))) + +(defn format-address + [address network-preferences] + (let [short-names (map network->short-name network-preferences) + prefix (when (<= (count short-names) max-network-prefixes) + (short-names->network-preference-prefix short-names)) + transformed-address (str prefix address)] + transformed-address)) diff --git a/src/status_im/contexts/wallet/common/utils_test.cljs b/src/status_im/contexts/wallet/common/utils_test.cljs index ae80a47b2e..f5c866b532 100644 --- a/src/status_im/contexts/wallet/common/utils_test.cljs +++ b/src/status_im/contexts/wallet/common/utils_test.cljs @@ -269,3 +269,68 @@ expected "0"] (is (= (utils/token-balance-display-for-network token chain-id rounding-decimals) expected))))) + +(def mock-accounts + {:0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5 + {:address "0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5" + :operable? true + :tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance 1000000 + :balance 1.0}}} + {:symbol "USDC" + :balances-per-chain {1 {:raw-balance 0 + :balance 0}}}]} + :0xbce36f66a8cd99f1d6489cb9585146e3f3b893be + {:address "0xbce36f66a8cd99f1d6489cb9585146e3f3b893be" + :operable? true + :tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance 0 + :balance 0}}}]}}) + +(deftest get-accounts-with-token-balance-test + (testing "Positive token balance for a specific token" + (let [accounts mock-accounts + token {:symbol "ETH"} + expected [{:address "0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5" + :operable? true + :tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance 1000000 + :balance 1.0}}} + {:symbol "USDC" + :balances-per-chain {1 {:raw-balance 0 + :balance 0}}}]}]] + (is (= (utils/get-accounts-with-token-balance accounts token) + expected)))) + + (testing "No token symbol provided, return all tokens with positive balance" + (let [accounts mock-accounts + token {} + expected [{:address "0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5" + :operable? true + :tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance 1000000 + :balance 1.0}}} + {:symbol "USDC" + :balances-per-chain {1 {:raw-balance 0 + :balance 0}}}]}]] + (is (= (utils/get-accounts-with-token-balance accounts token) + expected)))) + + (testing "No matching token found for a specific token" + (let [accounts mock-accounts + token {:symbol "DAI"} + expected []] + (is (= (utils/get-accounts-with-token-balance accounts token) + expected)))) + + (testing "No operable accounts" + (let [accounts {:0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5 + {:address "0xfc6327a092f6232e158a0dd1d6d967a2e1c65cd5" + :operable? false + :tokens [{:symbol "ETH" + :balances-per-chain {1 {:raw-balance 1000000 + :balance 1.0}}}]}} + token {} + expected []] + (is (= (utils/get-accounts-with-token-balance accounts token) + expected))))) diff --git a/src/status_im/contexts/wallet/swap/events.cljs b/src/status_im/contexts/wallet/swap/events.cljs index a0761665fa..6ce9638889 100644 --- a/src/status_im/contexts/wallet/swap/events.cljs +++ b/src/status_im/contexts/wallet/swap/events.cljs @@ -14,11 +14,12 @@ [utils.number :as number])) (rf/reg-event-fx :wallet.swap/start - (fn [{:keys [db]} [{:keys [network asset-to-receive open-new-screen?] :as data}]] + (fn [{:keys [db]} [{:keys [network asset-to-receive open-new-screen? from-account] :as data}]] (let [{:keys [wallet]} db test-networks-enabled? (get-in db [:profile/profile :test-networks-enabled?]) view-id (:view-id db) - account (swap-utils/wallet-account wallet) + root-screen? (or (= view-id :wallet-stack) (nil? view-id)) + account (or from-account (swap-utils/wallet-account wallet)) asset-to-pay (if (get-in data [:asset-to-pay :networks]) (:asset-to-pay data) (swap-utils/select-asset-to-pay-by-symbol @@ -26,6 +27,10 @@ :account account :test-networks-enabled? test-networks-enabled? :token-symbol (get-in data [:asset-to-pay :symbol])})) + multi-account-balance? (-> (utils/get-accounts-with-token-balance (:accounts wallet) + asset-to-pay) + (count) + (> 1)) network' (or network (swap-utils/select-network asset-to-pay)) start-point (if open-new-screen? :action-menu :swap-button)] @@ -35,34 +40,38 @@ (assoc-in [:wallet :ui :swap :network] network') (assoc-in [:wallet :ui :swap :launch-screen] view-id) (assoc-in [:wallet :ui :swap :start-point] start-point)) - :fx (if network' - [[:dispatch [:wallet/switch-current-viewing-account (:address account)]] - [:dispatch - (if open-new-screen? - [:open-modal :screen/wallet.setup-swap] - [:navigate-to-within-stack - [:screen/wallet.setup-swap :screen/wallet.swap-select-asset-to-pay]])] - [:dispatch - [:centralized-metrics/track :metric/swap-start - {:network (:chain-id network) - :pay_token (:symbol asset-to-pay) - :receive_token (:symbol asset-to-receive) - :start_point start-point - :launch_screen view-id}]] - [:dispatch [:wallet.swap/set-default-slippage]]] - [[:dispatch - [:show-bottom-sheet - {:content (fn [] - [network-selection/view - {:token-symbol (:symbol asset-to-pay) - :on-select-network (fn [network] - (rf/dispatch [:hide-bottom-sheet]) - (rf/dispatch - [:wallet.swap/start - {:asset-to-pay asset-to-pay - :asset-to-receive asset-to-receive - :network network - :open-new-screen? open-new-screen?}]))}])}]]])}))) + :fx (if (and multi-account-balance? root-screen? (not from-account)) + [[:dispatch [:open-modal :screen/wallet.swap-select-account]]] + (if network' + [[:dispatch [:wallet/switch-current-viewing-account (:address account)]] + [:dispatch + (if open-new-screen? + [:open-modal :screen/wallet.setup-swap] + [:navigate-to-within-stack + [:screen/wallet.setup-swap :screen/wallet.swap-select-asset-to-pay]])] + [:dispatch + [:centralized-metrics/track :metric/swap-start + {:network (:chain-id network) + :pay_token (:symbol asset-to-pay) + :receive_token (:symbol asset-to-receive) + :start_point start-point + :launch_screen view-id}]] + [:dispatch [:wallet.swap/set-default-slippage]]] + [[:dispatch + [:show-bottom-sheet + {:content (fn [] + [network-selection/view + {:token-symbol (:symbol asset-to-pay) + :on-select-network (fn [network] + (rf/dispatch [:hide-bottom-sheet]) + (rf/dispatch + [:wallet.swap/start + {:asset-to-pay asset-to-pay + :asset-to-receive asset-to-receive + :network network + :open-new-screen? + open-new-screen? + :from-account from-account}]))}])}]]]))}))) (rf/reg-event-fx :wallet.swap/select-asset-to-pay (fn [{:keys [db]} [{:keys [token]}]] @@ -527,3 +536,15 @@ [:dispatch [:wallet/navigate-to-account-within-stack address]]) [:dispatch [:wallet/fetch-activities-for-current-account]] [:dispatch [:wallet/select-account-tab :activity]]]}))) + +(rf/reg-event-fx :wallet.swap/start-from-account + (fn [{:keys [db]} [account]] + (let [asset-to-pay (get-in db [:wallet :ui :swap :asset-to-pay]) + asset-to-receive (get-in db [:wallet :ui :swap :asset-to-receive])] + {:fx [[:dispatch [:dismiss-modal :screen/wallet.swap-select-account]] + [:dispatch + [:wallet.swap/start + {:asset-to-pay asset-to-pay + :asset-to-receive asset-to-receive + :open-new-screen? true + :from-account account}]]]}))) diff --git a/src/status_im/contexts/wallet/swap/select_account/style.cljs b/src/status_im/contexts/wallet/swap/select_account/style.cljs new file mode 100644 index 0000000000..e32271095b --- /dev/null +++ b/src/status_im/contexts/wallet/swap/select_account/style.cljs @@ -0,0 +1,7 @@ +(ns status-im.contexts.wallet.swap.select-account.style) + +(def accounts-list + {:padding-bottom 12}) + +(def accounts-list-container + {:padding-horizontal 8}) diff --git a/src/status_im/contexts/wallet/swap/select_account/view.cljs b/src/status_im/contexts/wallet/swap/select_account/view.cljs new file mode 100644 index 0000000000..87582af80a --- /dev/null +++ b/src/status_im/contexts/wallet/swap/select_account/view.cljs @@ -0,0 +1,52 @@ +(ns status-im.contexts.wallet.swap.select-account.view + (:require + [quo.core :as quo] + [react-native.core :as rn] + [react-native.safe-area :as safe-area] + [status-im.common.events-helper :as events-helper] + [status-im.common.floating-button-page.view :as floating-button-page] + [status-im.contexts.wallet.swap.select-account.style :as style] + [utils.i18n :as i18n] + [utils.money :as money] + [utils.re-frame :as rf])) + +(defn- on-account-press + [account] + (rf/dispatch [:wallet.swap/start-from-account account])) + +(defn- render-fn + [item _ _] + (let [has-balance (money/above-zero? (:asset-pay-balance item))] + [quo/account-item + {:type (if has-balance :tag :default) + :on-press #(on-account-press item) + :state (if has-balance :default :disabled) + :token-props {:symbol (:asset-pay-symbol item) + :value (:asset-pay-balance item)} + :account-props (assoc item + :address (:formatted-address item) + :full-address? true)}])) + +(defn- on-close + [] + (rf/dispatch [:wallet/clean-current-viewing-account]) + (events-helper/navigate-back)) + +(defn view + [] + (let [accounts (rf/sub [:wallet/accounts-with-balances])] + [floating-button-page/view + {:footer-container-padding 0 + :header [quo/page-nav + {:margin-top (safe-area/get-top) + :icon-name :i/close + :on-press on-close}]} + [quo/page-top + {:title (i18n/label :t/from-label) + :title-accessibility-label :title-label}] + [rn/flat-list + {:style style/accounts-list + :content-container-style style/accounts-list-container + :data accounts + :render-fn render-fn + :shows-horizontal-scroll-indicator false}]])) diff --git a/src/status_im/navigation/screens.cljs b/src/status_im/navigation/screens.cljs index 9d3d494bbb..17bff0b10a 100644 --- a/src/status_im/navigation/screens.cljs +++ b/src/status_im/navigation/screens.cljs @@ -134,6 +134,7 @@ [status-im.contexts.wallet.send.send-amount.view :as wallet-send-input-amount] [status-im.contexts.wallet.send.transaction-confirmation.view :as wallet-transaction-confirmation] [status-im.contexts.wallet.send.transaction-progress.view :as wallet-transaction-progress] + [status-im.contexts.wallet.swap.select-account.view :as wallet-swap-select-account] [status-im.contexts.wallet.swap.select-asset-to-pay.view :as wallet-swap-select-asset-to-pay] [status-im.contexts.wallet.swap.set-spending-cap.view :as wallet-swap-set-spending-cap] [status-im.contexts.wallet.swap.setup-swap.view :as wallet-swap-setup-swap] @@ -635,6 +636,12 @@ :insets {:top? true}} :component wallet-swap-select-asset-to-pay/view} + {:name :screen/wallet.swap-select-account + :metrics {:track? true} + :options {:modalPresentationStyle :overCurrentContext + :insets {:bottom? true}} + :component wallet-swap-select-account/view} + {:name :screen/wallet.setup-swap :metrics {:track? true :alias-id :wallet-swap.input-amount-to-swap} diff --git a/src/status_im/subs/wallet/networks.cljs b/src/status_im/subs/wallet/networks.cljs index 28184a0756..3685d45fa0 100644 --- a/src/status_im/subs/wallet/networks.cljs +++ b/src/status_im/subs/wallet/networks.cljs @@ -4,8 +4,6 @@ [utils.money :as money] [utils.number :as number])) -(def max-network-prefixes 2) - (re-frame/reg-sub :wallet/networks :<- [:wallet] @@ -53,12 +51,7 @@ (re-frame/reg-sub :wallet/account-address (fn [_ [_ address network-preferences]] - (let [short-names (map network-utils/network->short-name network-preferences) - prefix (when (<= (count short-names) max-network-prefixes) - (network-utils/short-names->network-preference-prefix - short-names)) - transformed-address (str prefix address)] - transformed-address))) + (network-utils/format-address address network-preferences))) (re-frame/reg-sub :wallet/network-values diff --git a/src/status_im/subs/wallet/swap.cljs b/src/status_im/subs/wallet/swap.cljs index 1ddeafbad6..c040dcf909 100644 --- a/src/status_im/subs/wallet/swap.cljs +++ b/src/status_im/subs/wallet/swap.cljs @@ -3,6 +3,7 @@ [re-frame.core :as rf] [status-im.constants :as constants] [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.common.utils.networks :as network-utils] [status-im.contexts.wallet.send.utils :as send-utils] [utils.money :as money] [utils.number :as number])) @@ -315,3 +316,23 @@ :token token-for-fees})] fee-in-fiat))) +(rf/reg-sub + :wallet/accounts-with-balances + :<- [:wallet/operable-accounts] + :<- [:wallet/swap-asset-to-pay] + (fn [[accounts asset-to-pay]] + (let [token-symbol (:symbol asset-to-pay)] + (map + (fn [account] + (let [tokens (:tokens account) + filtered-tokens (filter #(= (:symbol %) token-symbol) tokens) + asset-pay-balance (utils/calculate-total-token-balance filtered-tokens) + formatted-address (network-utils/format-address (:address account) + (:network-preferences-names account))] + (assoc account + :formatted-address formatted-address + :asset-pay-balance (utils/sanitized-token-amount-to-display + asset-pay-balance + constants/min-token-decimals-to-display) + :asset-pay-symbol token-symbol))) + accounts))))