feat: support edge case flow in the wallet send flow when token is not available on receiver preferred networks (#19674)

feat: implement wallet send flow edge case when selected token is not supported on receiver's preferred networks

Signed-off-by: Brian Sztamfater <brian@status.im>
This commit is contained in:
Brian Sztamfater 2024-05-15 14:11:26 -03:00 committed by GitHub
parent 8f0840e913
commit 0145429852
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 829 additions and 402 deletions

View File

@ -229,15 +229,16 @@
(defn make-network-item (defn make-network-item
"This function generates props for quo/category component item" "This function generates props for quo/category component item"
[{:keys [network-name color on-change networks state label-props]}] [{:keys [network-name color on-change networks state label-props type]}]
(cond-> {:title (string/capitalize (name network-name)) (cond-> {:title (string/capitalize (name network-name))
:image :icon-avatar :image :icon-avatar
:image-props {:icon (resources/get-network network-name) :image-props {:icon (resources/get-network network-name)
:size :size-20} :size :size-20}
:action :selector :action :selector
:action-props {:type (if (= :default state) :action-props {:type (or type
:filled-checkbox (if (= :default state)
:checkbox) :filled-checkbox
:checkbox))
:customization-color color :customization-color color
:checked? (contains? networks network-name) :checked? (contains? networks network-name)
:on-change on-change}} :on-change on-change}}

View File

@ -3,6 +3,8 @@
[status-im.constants :as constants] [status-im.constants :as constants]
[utils.number])) [utils.number]))
(def ^:private last-comma-followed-by-text-to-end-regex #",\s(?=[^,]+$)")
(def id->network (def id->network
{constants/ethereum-mainnet-chain-id constants/mainnet-network-name {constants/ethereum-mainnet-chain-id constants/mainnet-network-name
constants/ethereum-goerli-chain-id constants/mainnet-network-name constants/ethereum-goerli-chain-id constants/mainnet-network-name
@ -113,3 +115,13 @@
(as-> prefix $ (as-> prefix $
(string/split $ ":") (string/split $ ":")
(map short-name->network $))) (map short-name->network $)))
(defn network-ids->formatted-text
[network-ids]
(let [network-names (->> network-ids
(map id->network)
(map name)
(map string/capitalize)
(string/join ", "))
formatted-text (string/replace network-names last-comma-followed-by-text-to-end-regex " and ")]
formatted-text))

View File

@ -43,3 +43,21 @@
(seq [:mainnet]) "eth" (seq [:mainnet]) "eth"
(seq [:mainnet :optimism]) "eth:opt" (seq [:mainnet :optimism]) "eth:opt"
(seq [:mainnet :optimism :arbitrum]) "eth:opt:arb1")) (seq [:mainnet :optimism :arbitrum]) "eth:opt:arb1"))
(deftest test-network-ids->formatted-text
(testing "Empty network-ids should return an empty string"
(is (= "" (utils/network-ids->formatted-text []))))
(testing "Single network-id should return the capitalized name of that network"
(is (= "Mainnet" (utils/network-ids->formatted-text [constants/ethereum-mainnet-chain-id]))))
(testing "Two network-ids should return a comma-separated string with 'and' for the last item"
(is (= "Mainnet and Optimism"
(utils/network-ids->formatted-text [constants/ethereum-mainnet-chain-id
constants/optimism-mainnet-chain-id]))))
(testing "Multiple network-ids should return a comma-separated string with 'and' for the last item"
(is (= "Mainnet, Optimism and Arbitrum"
(utils/network-ids->formatted-text [constants/ethereum-mainnet-chain-id
constants/optimism-mainnet-chain-id
constants/arbitrum-mainnet-chain-id])))))

View File

@ -39,32 +39,38 @@
token-decimals (if collectible 0 (:decimals token)) token-decimals (if collectible 0 (:decimals token))
native-token? (and token (= token-display-name "ETH")) native-token? (and token (= token-display-name "ETH"))
routes-available? (pos? (count chosen-route)) routes-available? (pos? (count chosen-route))
token-networks (:networks token)
token-networks-ids (when token-networks (mapv #(:chain-id %) token-networks))
from-network-amounts-by-chain (send-utils/network-amounts-by-chain {:route chosen-route from-network-amounts-by-chain (send-utils/network-amounts-by-chain {:route chosen-route
:token-decimals :token-decimals
token-decimals token-decimals
:native-token? :native-token?
native-token? native-token?
:to? false}) :receiver? false})
from-network-values-for-ui (send-utils/network-values-for-ui from-network-amounts-by-chain) from-network-values-for-ui (send-utils/network-values-for-ui from-network-amounts-by-chain)
to-network-amounts-by-chain (send-utils/network-amounts-by-chain {:route chosen-route to-network-amounts-by-chain (send-utils/network-amounts-by-chain {:route chosen-route
:token-decimals :token-decimals
token-decimals token-decimals
:native-token? :native-token?
native-token? native-token?
:to? true}) :receiver? true})
to-network-values-for-ui (send-utils/network-values-for-ui to-network-amounts-by-chain) to-network-values-for-ui (send-utils/network-values-for-ui to-network-amounts-by-chain)
sender-network-values (if routes-available? sender-network-values (if routes-available?
(send-utils/network-amounts from-network-values-for-ui (send-utils/network-amounts
disabled-from-chain-ids {:network-values from-network-values-for-ui
receiver-networks :disabled-chain-ids disabled-from-chain-ids
false) :receiver-networks receiver-networks
:token-networks-ids token-networks-ids
:receiver? false})
(send-utils/reset-network-amounts-to-zero (send-utils/reset-network-amounts-to-zero
sender-network-values)) sender-network-values))
receiver-network-values (if routes-available? receiver-network-values (if routes-available?
(send-utils/network-amounts to-network-values-for-ui (send-utils/network-amounts
disabled-from-chain-ids {:network-values to-network-values-for-ui
receiver-networks :disabled-chain-ids disabled-from-chain-ids
true) :receiver-networks receiver-networks
:token-networks-ids token-networks-ids
:receiver? true})
(send-utils/reset-network-amounts-to-zero (send-utils/reset-network-amounts-to-zero
receiver-network-values)) receiver-network-values))
network-links (when routes-available? network-links (when routes-available?
@ -147,6 +153,7 @@
(assoc-in [:wallet :ui :send :recipient] (or recipient address)) (assoc-in [:wallet :ui :send :recipient] (or recipient address))
(assoc-in [:wallet :ui :send :to-address] to-address) (assoc-in [:wallet :ui :send :to-address] to-address)
(assoc-in [:wallet :ui :send :address-prefix] prefix) (assoc-in [:wallet :ui :send :address-prefix] prefix)
(assoc-in [:wallet :ui :send :receiver-preferred-networks] receiver-networks)
(assoc-in [:wallet :ui :send :receiver-networks] receiver-networks)) (assoc-in [:wallet :ui :send :receiver-networks] receiver-networks))
:fx [(when (and collectible-tx? one-collectible?) :fx [(when (and collectible-tx? one-collectible?)
[:dispatch [:wallet/get-suggested-routes {:amount 1}]]) [:dispatch [:wallet/get-suggested-routes {:amount 1}]])
@ -176,26 +183,44 @@
;; `token` is a map extracted from the sender, but in the wallet home page we don't know the ;; `token` is a map extracted from the sender, but in the wallet home page we don't know the
;; sender yet, so we only provide the `token-symbol`, later in ;; sender yet, so we only provide the `token-symbol`, later in
;; `:wallet/select-from-account` the `token` key will be set. ;; `:wallet/select-from-account` the `token` key will be set.
{:db (cond-> db (let [{token-networks :networks} token
:always (update-in [:wallet :ui :send] dissoc :collectible) receiver-networks (get-in db [:wallet :ui :send :receiver-networks])
:always (assoc-in [:wallet :ui :send :token-display-name] (:symbol token)) token-networks-ids (mapv #(:chain-id %) token-networks)
token (assoc-in [:wallet :ui :send :token] token) token-not-supported-in-receiver-networks? (not (some (set receiver-networks)
token-symbol (assoc-in [:wallet :ui :send :token-symbol] token-symbol)) token-networks-ids))]
:fx [[:dispatch [:wallet/clean-suggested-routes]] {:db (cond-> db
[:dispatch :always (update-in [:wallet :ui :send] dissoc :collectible)
[:wallet/wizard-navigate-forward :always (assoc-in [:wallet :ui :send :token-display-name]
{:current-screen stack-id (:symbol token))
:start-flow? start-flow? :always (assoc-in
:flow-id :wallet-send-flow}]]]})) [:wallet :ui :send
:token-not-supported-in-receiver-networks?]
token-not-supported-in-receiver-networks?)
token (assoc-in [:wallet :ui :send :token] token)
token-symbol (assoc-in [:wallet :ui :send :token-symbol]
token-symbol))
:fx [[:dispatch [:wallet/clean-suggested-routes]]
[:dispatch
[:wallet/wizard-navigate-forward
{:current-screen stack-id
:start-flow? start-flow?
:flow-id :wallet-send-flow}]]]})))
(rf/reg-event-fx (rf/reg-event-fx
:wallet/edit-token-to-send :wallet/edit-token-to-send
(fn [{:keys [db]} [token]] (fn [{:keys [db]} [token]]
{:db (-> db (let [{token-networks :networks} token
(assoc-in [:wallet :ui :send :token] token) receiver-networks (get-in db [:wallet :ui :send :receiver-networks])
(assoc-in [:wallet :ui :send :token-display-name] token)) token-networks-ids (mapv #(:chain-id %) token-networks)
:fx [[:dispatch [:hide-bottom-sheet]] token-not-supported-in-receiver-networks? (not (some (set receiver-networks)
[:dispatch [:wallet/clean-suggested-routes]]]})) token-networks-ids))]
{:db (-> db
(assoc-in [:wallet :ui :send :token] token)
(assoc-in [:wallet :ui :send :token-display-name] token)
(assoc-in [:wallet :ui :send :token-not-supported-in-receiver-networks?]
token-not-supported-in-receiver-networks?))
:fx [[:dispatch [:hide-bottom-sheet]]
[:dispatch [:wallet/clean-suggested-routes]]]})))
(rf/reg-event-fx :wallet/clean-selected-token (rf/reg-event-fx :wallet/clean-selected-token
(fn [{:keys [db]}] (fn [{:keys [db]}]
@ -307,18 +332,21 @@
balances-per-chain balances-per-chain
:disabled-chain-ids :disabled-chain-ids
disabled-from-chain-ids})) disabled-from-chain-ids}))
token-networks-ids (when token (mapv #(:chain-id %) (:networks token)))
sender-network-values (when token-available-networks-for-suggested-routes sender-network-values (when token-available-networks-for-suggested-routes
(send-utils/loading-network-amounts (send-utils/loading-network-amounts
token-available-networks-for-suggested-routes {:valid-networks token-available-networks-for-suggested-routes
disabled-from-chain-ids :disabled-chain-ids disabled-from-chain-ids
receiver-networks :receiver-networks receiver-networks
false)) :token-networks-ids token-networks-ids
:receiver? false}))
receiver-network-values (when token-available-networks-for-suggested-routes receiver-network-values (when token-available-networks-for-suggested-routes
(send-utils/loading-network-amounts (send-utils/loading-network-amounts
token-available-networks-for-suggested-routes {:valid-networks token-available-networks-for-suggested-routes
disabled-from-chain-ids :disabled-chain-ids disabled-from-chain-ids
receiver-networks :receiver-networks receiver-networks
true)) :token-networks-ids token-networks-ids
:receiver? true}))
request-params [transaction-type-param request-params [transaction-type-param
from-address from-address
to-address to-address

View File

@ -76,7 +76,8 @@
:wallet/wallet-send-to-values-by-chain {1 (money/bignumber "250")} :wallet/wallet-send-to-values-by-chain {1 (money/bignumber "250")}
:wallet/wallet-send-sender-network-values nil :wallet/wallet-send-sender-network-values nil
:wallet/wallet-send-receiver-network-values nil :wallet/wallet-send-receiver-network-values nil
:wallet/wallet-send-network-links nil}) :wallet/wallet-send-network-links nil
:wallet/wallet-send-receiver-preferred-networks [1]})
(h/describe "Send > input amount screen" (h/describe "Send > input amount screen"
(h/setup-restorable-re-frame) (h/setup-restorable-re-frame)

View File

@ -1,4 +1,5 @@
(ns status-im.contexts.wallet.send.input-amount.style) (ns status-im.contexts.wallet.send.input-amount.style
(:require [quo.foundations.colors :as colors]))
(def screen (def screen
{:flex 1}) {:flex 1})
@ -38,3 +39,24 @@
{:height 40 {:height 40
:width "100%" :width "100%"
:align-items :center}) :align-items :center})
(defn token-not-available-container
[theme]
{:height 90
:flex-direction :row
:background-color (colors/resolve-color :danger theme 5)
:border-color (colors/resolve-color :danger theme 10)
:border-width 1
:border-radius 12
:margin-horizontal 20
:padding 12})
(def token-not-available-content-container
{:margin-left 8
:align-items :flex-start})
(defn token-not-available-text
[theme]
{:height 36
:flex 1
:color (colors/resolve-color :danger theme)})

View File

@ -2,6 +2,8 @@
(:require (:require
[clojure.string :as string] [clojure.string :as string]
[quo.core :as quo] [quo.core :as quo]
[quo.foundations.colors :as colors]
[quo.theme]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.safe-area :as safe-area] [react-native.safe-area :as safe-area]
[reagent.core :as reagent] [reagent.core :as reagent]
@ -12,6 +14,7 @@
[status-im.contexts.wallet.send.input-amount.style :as style] [status-im.contexts.wallet.send.input-amount.style :as style]
[status-im.contexts.wallet.send.routes.view :as routes] [status-im.contexts.wallet.send.routes.view :as routes]
[status-im.contexts.wallet.send.utils :as send-utils] [status-im.contexts.wallet.send.utils :as send-utils]
[status-im.contexts.wallet.sheets.unpreferred-networks-alert.view :as unpreferred-networks-alert]
[utils.address :as address] [utils.address :as address]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.money :as money] [utils.money :as money]
@ -26,7 +29,7 @@
string/upper-case))) string/upper-case)))
(defn- estimated-fees (defn- estimated-fees
[{:keys [loading-suggested-routes? fees amount receiver]}] [{:keys [loading-routes? fees amount receiver]}]
[rn/view {:style style/estimated-fees-container} [rn/view {:style style/estimated-fees-container}
[rn/view {:style style/estimated-fees-content-container} [rn/view {:style style/estimated-fees-content-container}
[quo/button [quo/button
@ -35,20 +38,20 @@
:size 32 :size 32
:inner-style {:opacity 1} :inner-style {:opacity 1}
:accessibility-label :advanced-button :accessibility-label :advanced-button
:disabled? loading-suggested-routes? :disabled? loading-routes?
:on-press #(js/alert "Not implemented yet")} :on-press #(js/alert "Not implemented yet")}
:i/advanced]] :i/advanced]]
[quo/data-item [quo/data-item
{:container-style style/fees-data-item {:container-style style/fees-data-item
:label :none :label :none
:status (if loading-suggested-routes? :loading :default) :status (if loading-routes? :loading :default)
:size :small :size :small
:title (i18n/label :t/fees) :title (i18n/label :t/fees)
:subtitle fees}] :subtitle fees}]
[quo/data-item [quo/data-item
{:container-style style/amount-data-item {:container-style style/amount-data-item
:label :none :label :none
:status (if loading-suggested-routes? :loading :default) :status (if loading-routes? :loading :default)
:size :small :size :small
:title (i18n/label :t/user-gets {:name receiver}) :title (i18n/label :t/user-gets {:name receiver})
:subtitle amount}]]) :subtitle amount}]])
@ -77,6 +80,36 @@
(rf/dispatch [:wallet/edit-token-to-send token]) (rf/dispatch [:wallet/edit-token-to-send token])
(clear-input!))}]])) (clear-input!))}]]))
(defn- token-not-available
[token-symbol receiver-networks token-networks]
(let [theme (quo.theme/use-theme)
add-token-networks (fn []
(let [chain-ids (concat receiver-networks
(mapv #(:chain-id %) token-networks))]
(rf/dispatch [:wallet/update-receiver-networks chain-ids])))]
[rn/view {:style (style/token-not-available-container theme)}
[rn/view
[quo/icon :i/alert
{:size 16
:color colors/danger-50}]]
[rn/view {:style style/token-not-available-content-container}
[quo/text
{:style (style/token-not-available-text theme)
:size :paragraph-2}
(i18n/label :t/token-not-available-on-receiver-networks {:token-symbol token-symbol})]
[quo/button
{:size 24
:customization-color colors/danger-50
:on-press add-token-networks}
(i18n/label :t/add-networks-token-can-be-sent-to {:token-symbol token-symbol})]]]))
(defn- show-unpreferred-networks-alert
[on-confirm]
(rf/dispatch
[:show-bottom-sheet
{:content (fn []
[unpreferred-networks-alert/view
{:on-confirm on-confirm}])}]))
(defn view (defn view
;; crypto-decimals, limit-crypto and initial-crypto-currency? args are needed ;; crypto-decimals, limit-crypto and initial-crypto-currency? args are needed
;; for component tests only ;; for component tests only
@ -94,91 +127,112 @@
crypto-currency? (reagent/atom initial-crypto-currency?) crypto-currency? (reagent/atom initial-crypto-currency?)
on-navigate-back on-navigate-back] on-navigate-back on-navigate-back]
(fn [] (fn []
(let [[input-state set-input-state] (rn/use-state controlled-input/init-state) (let [[input-state set-input-state] (rn/use-state controlled-input/init-state)
clear-input! #(set-input-state controlled-input/delete-all) clear-input! #(set-input-state controlled-input/delete-all)
handle-on-confirm (fn [] handle-on-confirm (fn []
(rf/dispatch [:wallet/set-token-amount-to-send (rf/dispatch [:wallet/set-token-amount-to-send
{:amount (controlled-input/input-value {:amount
input-state) (controlled-input/input-value
:stack-id current-screen-id}])) input-state)
{fiat-currency :currency} (rf/sub [:profile/profile]) :stack-id current-screen-id}]))
{fiat-currency :currency} (rf/sub [:profile/profile])
{token-symbol :symbol {token-symbol :symbol
token-networks :networks} (rf/sub [:wallet/wallet-send-token]) token-networks :networks} (rf/sub [:wallet/wallet-send-token])
{token-balance :total-balance {token-balance :total-balance
:as :as
token} (rf/sub token} (rf/sub
[:wallet/current-viewing-account-tokens-filtered [:wallet/current-viewing-account-tokens-filtered
(str token-symbol)]) (str token-symbol)])
conversion-rate (-> token :market-values-per-currency :usd :price) conversion-rate (-> token :market-values-per-currency :usd :price)
loading-routes? (rf/sub loading-routes? (rf/sub
[:wallet/wallet-send-loading-suggested-routes?]) [:wallet/wallet-send-loading-suggested-routes?])
route (rf/sub [:wallet/wallet-send-route]) route (rf/sub [:wallet/wallet-send-route])
to-address (rf/sub [:wallet/wallet-send-to-address]) to-address (rf/sub [:wallet/wallet-send-to-address])
on-confirm (or default-on-confirm handle-on-confirm) on-confirm (or default-on-confirm handle-on-confirm)
crypto-decimals (or default-crypto-decimals crypto-decimals (or default-crypto-decimals
(utils/get-crypto-decimals-count token)) (utils/get-crypto-decimals-count token))
crypto-limit (or default-limit-crypto crypto-limit (or default-limit-crypto
(utils/get-standard-crypto-format (utils/get-standard-crypto-format
token token
token-balance)) token-balance))
fiat-limit (.toFixed (* token-balance conversion-rate) 2) fiat-limit (.toFixed (* token-balance conversion-rate) 2)
current-limit (if @crypto-currency? crypto-limit fiat-limit) current-limit (if @crypto-currency? crypto-limit fiat-limit)
valid-input? (not (or (string/blank? (controlled-input/input-value valid-input? (not (or (string/blank?
input-state)) (controlled-input/input-value
(<= (controlled-input/numeric-value input-state) 0) input-state))
(> (controlled-input/numeric-value input-state) (<= (controlled-input/numeric-value
current-limit))) input-state)
current-currency (if @crypto-currency? token-symbol fiat-currency) 0)
input-num-value (controlled-input/numeric-value input-state) (> (controlled-input/numeric-value
confirm-disabled? (or (nil? route) input-state)
(empty? route) current-limit)))
(string/blank? (controlled-input/input-value input-state)) current-currency (if @crypto-currency? token-symbol fiat-currency)
(<= input-num-value 0) input-num-value (controlled-input/numeric-value input-state)
(> input-num-value current-limit)) confirm-disabled? (or (nil? route)
amount-text (str (controlled-input/input-value input-state) (empty? route)
" " (string/blank? (controlled-input/input-value
token-symbol) input-state))
first-route (first route) (<= input-num-value 0)
native-currency-symbol (when-not confirm-disabled? (> input-num-value current-limit))
(get-in first-route [:from :native-currency-symbol])) amount-text (str (controlled-input/input-value input-state)
native-token (when native-currency-symbol " "
(rf/sub [:wallet/token-by-symbol token-symbol)
native-currency-symbol])) first-route (first route)
fee-in-native-token (when-not confirm-disabled? native-currency-symbol (when-not confirm-disabled?
(send-utils/calculate-full-route-gas-fee route)) (get-in first-route
fee-in-crypto-formatted (when fee-in-native-token [:from :native-currency-symbol]))
(utils/get-standard-crypto-format native-token (when native-currency-symbol
native-token (rf/sub [:wallet/token-by-symbol
fee-in-native-token)) native-currency-symbol]))
fee-in-fiat (when-not confirm-disabled? fee-in-native-token (when-not confirm-disabled?
(utils/calculate-token-fiat-value (send-utils/calculate-full-route-gas-fee route))
{:currency fiat-currency fee-in-crypto-formatted (when fee-in-native-token
:balance fee-in-native-token (utils/get-standard-crypto-format
:token native-token})) native-token
currency-symbol (rf/sub [:profile/currency-symbol]) fee-in-native-token))
fee-formatted (when fee-in-fiat fee-in-fiat (when-not confirm-disabled?
(utils/get-standard-fiat-format (utils/calculate-token-fiat-value
fee-in-crypto-formatted {:currency fiat-currency
currency-symbol :balance fee-in-native-token
fee-in-fiat)) :token native-token}))
show-select-asset-sheet #(rf/dispatch currency-symbol (rf/sub [:profile/currency-symbol])
[:show-bottom-sheet fee-formatted (when fee-in-fiat
{:content (fn [] (utils/get-standard-fiat-format
[select-asset-bottom-sheet fee-in-crypto-formatted
clear-input!])}]) currency-symbol
loading-suggested-routes? (rf/sub fee-in-fiat))
[:wallet/wallet-send-loading-suggested-routes?]) show-select-asset-sheet #(rf/dispatch
sender-network-values (rf/sub [:show-bottom-sheet
[:wallet/wallet-send-sender-network-values]) {:content (fn []
suggested-routes (rf/sub [:wallet/wallet-send-suggested-routes]) [select-asset-bottom-sheet
routes (when suggested-routes clear-input!])}])
(or (:best suggested-routes) [])) sender-network-values (rf/sub
no-routes-found? (and [:wallet/wallet-send-sender-network-values])
(every-network-value-is-zero? sender-network-values) receiver-network-values (rf/sub
(not (nil? routes)) [:wallet/wallet-send-receiver-network-values])
(not loading-suggested-routes?))] token-not-supported-in-receiver-networks? (every? #(= (:type %) :not-available)
(filter #(not= (:type %) :add)
receiver-network-values))
suggested-routes (rf/sub [:wallet/wallet-send-suggested-routes])
routes (when suggested-routes
(or (:best suggested-routes) []))
no-routes-found? (and
(every-network-value-is-zero?
sender-network-values)
(not (nil? routes))
(not loading-routes?)
(not token-not-supported-in-receiver-networks?))
receiver-networks (rf/sub [:wallet/wallet-send-receiver-networks])
receiver-preferred-networks (rf/sub
[:wallet/wallet-send-receiver-preferred-networks])
receiver-preferred-networks-set (set receiver-preferred-networks)
sending-to-unpreferred-networks? (not (every? (fn [receiver-selected-network]
(contains?
receiver-preferred-networks-set
receiver-selected-network))
receiver-networks))]
(rn/use-mount (rn/use-mount
(fn [] (fn []
(let [dismiss-keyboard-fn #(when (= % "active") (rn/dismiss-keyboard!)) (let [dismiss-keyboard-fn #(when (= % "active") (rn/dismiss-keyboard!))
@ -211,16 +265,21 @@
:on-swap #(reset! crypto-currency? %) :on-swap #(reset! crypto-currency? %)
:on-token-press show-select-asset-sheet}] :on-token-press show-select-asset-sheet}]
[routes/view [routes/view
{:token token {:token token
:input-value (controlled-input/input-value input-state) :input-value (controlled-input/input-value input-state)
:valid-input? valid-input? :valid-input? valid-input?
:current-screen-id current-screen-id}] :token-not-supported-in-receiver-networks? token-not-supported-in-receiver-networks?
:current-screen-id current-screen-id}]
(when (and (not loading-routes?)
sender-network-values
token-not-supported-in-receiver-networks?)
[token-not-available token-symbol receiver-networks token-networks])
(when (or loading-routes? (seq route)) (when (or loading-routes? (seq route))
[estimated-fees [estimated-fees
{:loading-suggested-routes? loading-routes? {:loading-routes? loading-routes?
:fees fee-formatted :fees fee-formatted
:amount amount-text :amount amount-text
:receiver (address/get-shortened-key to-address)}]) :receiver (address/get-shortened-key to-address)}])
(when no-routes-found? (when no-routes-found?
[rn/view {:style style/no-routes-found-container} [rn/view {:style style/no-routes-found-container}
[quo/info-message [quo/info-message
@ -236,10 +295,14 @@
button-one-label) button-one-label)
:button-one-props (merge button-one-props :button-one-props (merge button-one-props
{:disabled? (and (not no-routes-found?) confirm-disabled?) {:disabled? (and (not no-routes-found?) confirm-disabled?)
:on-press (if no-routes-found? :on-press (cond
no-routes-found?
#(rf/dispatch [:wallet/get-suggested-routes #(rf/dispatch [:wallet/get-suggested-routes
{:amount (controlled-input/input-value {:amount (controlled-input/input-value
input-state)}]) input-state)}])
sending-to-unpreferred-networks?
#(show-unpreferred-networks-alert on-confirm)
:else
on-confirm)} on-confirm)}
(when no-routes-found? (when no-routes-found?
{:type :grey}))}] {:type :grey}))}]

View File

@ -1,14 +1,11 @@
(ns status-im.contexts.wallet.send.routes.view (ns status-im.contexts.wallet.send.routes.view
(:require (:require
[clojure.string :as string]
[quo.core :as quo] [quo.core :as quo]
[quo.foundations.colors :as colors]
[quo.foundations.resources :as resources]
[react-native.core :as rn] [react-native.core :as rn]
[reagent.core :as reagent]
[status-im.contexts.wallet.common.utils.networks :as network-utils] [status-im.contexts.wallet.common.utils.networks :as network-utils]
[status-im.contexts.wallet.send.routes.style :as style] [status-im.contexts.wallet.send.routes.style :as style]
[status-im.contexts.wallet.send.utils :as send-utils] [status-im.contexts.wallet.send.utils :as send-utils]
[status-im.contexts.wallet.sheets.network-preferences.view :as network-preferences]
[utils.debounce :as debounce] [utils.debounce :as debounce]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
@ -19,19 +16,6 @@
(def network-link-1x-height 56) (def network-link-1x-height 56)
(def network-link-2x-height 111) (def network-link-2x-height 111)
(defn- make-network-item
[{:keys [network-name chain-id] :as _network}
{:keys [title color on-change network-preferences] :as _options}]
{:title (or title (string/capitalize (name network-name)))
:image :icon-avatar
:image-props {:icon (resources/get-network network-name)
:size :size-20}
:action :selector
:action-props {:type :checkbox
:customization-color color
:checked? (some #(= % chain-id) @network-preferences)
:on-change on-change}})
(defn fetch-routes (defn fetch-routes
[amount valid-input? bounce-duration-ms] [amount valid-input? bounce-duration-ms]
(if valid-input? (if valid-input?
@ -40,82 +24,93 @@
bounce-duration-ms) bounce-duration-ms)
(rf/dispatch [:wallet/clean-suggested-routes]))) (rf/dispatch [:wallet/clean-suggested-routes])))
(defn networks-drawer (defn- open-preferences
[{:keys [on-save theme]}] []
(let [network-details (rf/sub [:wallet/network-details]) (rf/dispatch
{:keys [color]} (rf/sub [:wallet/current-viewing-account]) [:show-bottom-sheet
selected-networks (rf/sub [:wallet/wallet-send-receiver-networks]) {:content
prefix (rf/sub [:wallet/wallet-send-address-prefix]) (fn []
prefix-seq (string/split prefix #":") (let [receiver-networks (rf/sub [:wallet/wallet-send-receiver-networks])
grouped-details (group-by #(contains? (set prefix-seq) (:short-name %)) network-details) receiver-preferred-networks (rf/sub
preferred (get grouped-details true []) [:wallet/wallet-send-receiver-preferred-networks])
not-preferred (get grouped-details false []) {token-symbol :symbol
network-preferences (reagent/atom selected-networks) token-networks :networks} (rf/sub [:wallet/wallet-send-token])
toggle-network (fn [{:keys [chain-id]}] token-chain-ids-set (set (mapv #(:chain-id %) token-networks))
(swap! network-preferences [selected-receiver-networks
(fn [preferences] set-selected-receiver-networks] (rn/use-state receiver-networks)
(if (some #(= % chain-id) preferences) receiver-preferred-networks-set (set receiver-preferred-networks)
(vec (remove #(= % chain-id) preferences)) receiver-selected-preferred-networks (filter #(contains?
(conj preferences chain-id)))))] receiver-preferred-networks-set
(fn [] %)
[rn/view selected-receiver-networks)
[quo/drawer-top {:title (i18n/label :t/edit-receiver-networks)}] receiver-selected-non-preferred-networks (filter #(not (contains?
[quo/category receiver-preferred-networks-set
{:list-type :settings %))
:label (i18n/label :t/preferred-by-receiver) selected-receiver-networks)
:data (mapv (fn [network] not-available-preferred-networks (filter (fn [preferred-chain-id]
(make-network-item network (not (contains? token-chain-ids-set
{:color color preferred-chain-id)))
:network-preferences network-preferences receiver-selected-preferred-networks)
:on-change #(toggle-network network)})) not-available-non-preferred-networks (filter (fn [preferred-chain-id]
preferred)}] (not (contains?
(when (pos? (count not-preferred)) token-chain-ids-set
[quo/category preferred-chain-id)))
{:list-type :settings receiver-selected-non-preferred-networks)
:label (i18n/label :t/not-preferred-by-receiver) first-section-warning-label (when (not-empty not-available-preferred-networks)
:data (mapv (fn [network] (i18n/label
(make-network-item network :t/token-not-available-on-networks
{:color color {:token-symbol token-symbol
:network-preferences network-preferences :networks
:on-change #(toggle-network network)})) (network-utils/network-ids->formatted-text
not-preferred)}]) not-available-preferred-networks)}))
(when (not= selected-networks @network-preferences) second-section-warning-label (when (not-empty
[rn/view {:style (style/warning-container color theme)} not-available-non-preferred-networks)
[quo/icon :i/info {:color (colors/resolve-color color theme)}] (i18n/label
[quo/text :t/token-not-available-on-networks
{:size :paragraph-2 {:token-symbol token-symbol
:style style/warning-text} (i18n/label :t/receiver-networks-warning)]]) :networks
[quo/bottom-actions (network-utils/network-ids->formatted-text
{:actions :one-action not-available-non-preferred-networks)}))]
:button-one-label (i18n/label :t/apply-changes) [network-preferences/view
:button-one-props {:disabled? (or (= selected-networks @network-preferences) {:title (i18n/label :t/edit-receiver-networks)
(empty? @network-preferences)) :first-section-label (i18n/label :t/preferred-by-receiver)
:on-press (fn [] :second-section-label (i18n/label :t/not-preferred-by-receiver)
(rf/dispatch [:wallet/update-receiver-networks :selected-networks (set (map network-utils/id->network
@network-preferences]) receiver-networks))
(rf/dispatch [:hide-bottom-sheet]) :receiver-preferred-networks receiver-preferred-networks
(on-save)) :button-label (i18n/label :t/apply-changes)
:customization-color color}}]]))) :first-section-warning-label first-section-warning-label
:second-section-warning-label second-section-warning-label
:on-change #(set-selected-receiver-networks %)
:on-save (fn [chain-ids]
(rf/dispatch [:hide-bottom-sheet])
(rf/dispatch [:wallet/update-receiver-networks
chain-ids]))}]))}]))
(defn render-network-values (defn render-network-values
[{:keys [network-values token-symbol on-press theme on-save to? loading-suggested-routes?]}] [{:keys [network-values token-symbol on-press receiver? loading-routes?
token-not-supported-in-receiver-networks?]}]
[rn/view [rn/view
(map-indexed (fn [index {:keys [chain-id total-amount type]}] (map-indexed (fn [index {:keys [chain-id total-amount type]}]
[rn/view [rn/view
{:key (str (if to? "to" "from") "-" chain-id) {:key (str (if receiver? "to" "from") "-" chain-id)
:style {:margin-top (if (pos? index) 11 7.5)}} :style {:margin-top (if (pos? index) 11 7.5)}}
[quo/network-bridge [quo/network-bridge
{:amount (str total-amount " " token-symbol) {:amount (if (= type :not-available)
(i18n/label :t/not-available)
(str total-amount " " token-symbol))
:network (network-utils/id->network chain-id) :network (network-utils/id->network chain-id)
:status type :status (cond (and (= type :not-available)
:on-press #(when (not loading-suggested-routes?) loading-routes?
token-not-supported-in-receiver-networks?)
:loading
(= type :not-available)
:disabled
:else type)
:on-press #(when (not loading-routes?)
(cond (cond
(= type :add) (= type :add)
(rf/dispatch [:show-bottom-sheet (open-preferences)
{:content (fn []
[networks-drawer
{:theme theme
:on-save on-save}])}])
on-press (on-press chain-id total-amount)))}]]) on-press (on-press chain-id total-amount)))}]])
network-values)]) network-values)])
@ -180,12 +175,13 @@
(defn view (defn view
[{:keys [token theme input-value valid-input? [{:keys [token theme input-value valid-input?
on-press-to-network current-screen-id]}] on-press-to-network current-screen-id
token-not-supported-in-receiver-networks?]}]
(let [token-symbol (:symbol token) (let [token-symbol (:symbol token)
nav-current-screen-id (rf/sub [:view-id]) nav-current-screen-id (rf/sub [:view-id])
active-screen? (= nav-current-screen-id current-screen-id) active-screen? (= nav-current-screen-id current-screen-id)
loading-suggested-routes? (rf/sub loading-routes? (rf/sub
[:wallet/wallet-send-loading-suggested-routes?]) [:wallet/wallet-send-loading-suggested-routes?])
sender-network-values (rf/sub sender-network-values (rf/sub
[:wallet/wallet-send-sender-network-values]) [:wallet/wallet-send-sender-network-values])
receiver-network-values (rf/sub receiver-network-values (rf/sub
@ -220,23 +216,27 @@
:container-style style/section-label-right}]]) :container-style style/section-label-right}]])
[rn/view {:style style/routes-inner-container} [rn/view {:style style/routes-inner-container}
[render-network-values [render-network-values
{:token-symbol token-symbol {:token-symbol token-symbol
:network-values sender-network-values :network-values sender-network-values
:on-press #(disable-chain %1 :on-press (fn [chain-id-to-disable]
disabled-from-chain-ids (disable-chain
token-available-networks-for-suggested-routes) chain-id-to-disable
:to? false disabled-from-chain-ids
:theme theme token-available-networks-for-suggested-routes))
:loading-suggested-routes? loading-suggested-routes?}] :receiver? false
:theme theme
:loading-routes? loading-routes?
:token-not-supported-in-receiver-networks? false}]
[render-network-links [render-network-links
{:network-links network-links {:network-links network-links
:sender-network-values sender-network-values}] :sender-network-values sender-network-values}]
[render-network-values [render-network-values
{:token-symbol token-symbol {:token-symbol token-symbol
:network-values receiver-network-values :network-values receiver-network-values
:on-press on-press-to-network :on-press on-press-to-network
:to? true :receiver? true
:loading-suggested-routes? loading-suggested-routes? :loading-routes? loading-routes?
:theme theme :theme theme
:on-save #(fetch-routes input-value valid-input? 0)}]]])) :token-not-supported-in-receiver-networks? token-not-supported-in-receiver-networks?
:on-save #(fetch-routes input-value valid-input? 0)}]]]))

View File

@ -43,21 +43,22 @@
(money/wei->ether (reduce money/add (map calculate-gas-fee route)))) (money/wei->ether (reduce money/add (map calculate-gas-fee route))))
(defn network-amounts-by-chain (defn network-amounts-by-chain
[{:keys [route token-decimals native-token? to?]}] [{:keys [route token-decimals native-token? receiver?]}]
(reduce (fn [acc path] (reduce
(let [amount-hex (if to? (:amount-in path) (:amount-out path)) (fn [acc path]
amount-units (native-module/hex-to-number (let [amount-hex (if receiver? (:amount-in path) (:amount-out path))
(utils.hex/normalize-hex amount-hex)) amount-units (native-module/hex-to-number
amount (money/with-precision (utils.hex/normalize-hex amount-hex))
(if native-token? amount (money/with-precision
(money/wei->ether amount-units) (if native-token?
(money/token->unit amount-units (money/wei->ether amount-units)
token-decimals)) (money/token->unit amount-units
6) token-decimals))
chain-id (if to? (get-in path [:to :chain-id]) (get-in path [:from :chain-id]))] 6)
(update acc chain-id money/add amount))) chain-id (if receiver? (get-in path [:to :chain-id]) (get-in path [:from :chain-id]))]
{} (update acc chain-id money/add amount)))
route)) {}
route))
(defn network-values-for-ui (defn network-values-for-ui
[amounts] [amounts]
@ -93,62 +94,103 @@
network-amounts)) network-amounts))
(defn network-amounts (defn network-amounts
[network-values disabled-chain-ids receiver-networks to?] [{:keys [network-values disabled-chain-ids receiver-networks token-networks-ids receiver?]}]
(let [disabled-set (set disabled-chain-ids) (let [disabled-set (set disabled-chain-ids)
receiver-networks-set (set receiver-networks) receiver-networks-set (set receiver-networks)
network-values-keys (set (keys network-values)) network-values-keys (set (keys network-values))
routes-found? (pos? (count network-values-keys)) routes-found? (pos? (count network-values-keys))
updated-network-values (when routes-found? token-networks-ids-set (set token-networks-ids)
(reduce (fn [acc k] not-available-networks (if receiver?
(if (or (contains? network-values-keys k) (filter #(not (token-networks-ids-set %))
(and to? receiver-networks)
(not (contains? receiver-networks-set k)))) [])
acc not-available-networks-set (set not-available-networks)
(assoc acc k (money/bignumber "0")))) network-values-with-disabled-chains (when routes-found?
network-values (reduce
disabled-chain-ids))] (fn [acc k]
(cond-> (->> updated-network-values (if (or (contains? network-values-keys k)
(map (and receiver?
(fn [[k v]] (not (contains? receiver-networks-set
{:chain-id k k))))
:total-amount v acc
:type (if (or to? (not (contains? disabled-set k))) :default :disabled)})) (assoc acc k (money/bignumber "0"))))
(sort-by #(get network-priority-score (network-utils/id->network (:chain-id %)))) network-values
(filter disabled-chain-ids))
#(or (and to? network-values-with-not-available-chains (if (and receiver? routes-found?)
(or (contains? receiver-networks-set (:chain-id %)) (let [network-values-keys
(money/greater-than (:total-amount %) (money/bignumber "0")))) (set (keys
(not to?))) network-values-with-disabled-chains))]
(vec)) (reduce
(and to? (fn [acc k]
(if (not (contains? network-values-keys k))
(assoc acc k nil)
acc))
network-values-with-disabled-chains
not-available-networks))
network-values-with-disabled-chains)]
(cond-> (->>
network-values-with-not-available-chains
(map
(fn [[chain-id amount]]
{:chain-id chain-id
:total-amount amount
:type (cond
(contains? not-available-networks-set chain-id) :not-available
(or receiver? (not (contains? disabled-set chain-id))) :default
(and (not receiver?) (contains? disabled-set chain-id)) :disabled)}))
(sort-by (fn [network-amount]
(get network-priority-score
(network-utils/id->network (:chain-id network-amount)))))
(filter
(fn [network-amount]
(or (and receiver?
(or (contains? receiver-networks-set (:chain-id network-amount))
(money/greater-than (:total-amount network-amount) (money/bignumber "0"))))
(not receiver?))))
(vec))
(and receiver?
routes-found? routes-found?
(< (count updated-network-values) available-networks-count)) (< (count network-values-with-not-available-chains) available-networks-count))
(conj {:type :add})))) (conj {:type :add}))))
(defn loading-network-amounts (defn loading-network-amounts
[valid-networks disabled-chain-ids receiver-networks to?] [{:keys [valid-networks disabled-chain-ids receiver-networks token-networks-ids receiver?]}]
(let [disabled-set (set disabled-chain-ids) (let [disabled-set (set disabled-chain-ids)
receiver-networks-set (set receiver-networks) receiver-networks-set (set receiver-networks)
receiver-networks-count (count receiver-networks) receiver-networks-count (count receiver-networks)
valid-networks (concat valid-networks disabled-chain-ids)] token-networks-ids-set (set token-networks-ids)
valid-networks-set (set valid-networks)
not-available-networks (if receiver?
(filter #(not (token-networks-ids-set %)) receiver-networks)
[])
not-available-networks-set (set not-available-networks)
valid-networks (concat valid-networks
disabled-chain-ids
(when receiver?
(filter #(not (valid-networks-set %))
not-available-networks)))]
(cond-> (->> valid-networks (cond-> (->> valid-networks
(map (fn [k] (map
(cond-> {:chain-id k (fn [chain-id]
:type (if (or to? (cond->
(not (contains? disabled-set k))) {:chain-id chain-id
:loading :type (cond
:disabled)} (contains? not-available-networks-set chain-id) :not-available
(and (not to?) (contains? disabled-set k)) (or receiver?
(assoc :total-amount (money/bignumber "0"))))) (not (contains? disabled-set chain-id))) :loading
(sort-by (fn [item] (and (not receiver?) (contains? disabled-set chain-id)) :disabled)}
(and (not receiver?) (contains? disabled-set chain-id))
(assoc :total-amount (money/bignumber "0")))))
(sort-by (fn [network-amount]
(get network-priority-score (get network-priority-score
(network-utils/id->network (:chain-id item))))) (network-utils/id->network (:chain-id network-amount)))))
(filter (filter
#(or (and to? (contains? receiver-networks-set (:chain-id %))) (fn [network-amount]
(and (not to?) (or (and receiver? (contains? receiver-networks-set (:chain-id network-amount)))
(not (contains? disabled-chain-ids (:chain-id %)))))) (and (not receiver?)
(not (contains? disabled-chain-ids (:chain-id network-amount)))))))
(vec)) (vec))
(and to? (< receiver-networks-count available-networks-count)) (conj {:type :add})))) (and receiver? (< receiver-networks-count available-networks-count)) (conj {:type :add}))))
(defn network-links (defn network-links
[route from-values-by-chain to-values-by-chain] [route from-values-by-chain to-values-by-chain]

View File

@ -38,11 +38,11 @@
:to {:chain-id "2"}}] :to {:chain-id "2"}}]
token-decimals 18 token-decimals 18
native-token? true native-token? true
to? true receiver? true
result (utils/network-amounts-by-chain {:route route result (utils/network-amounts-by-chain {:route route
:token-decimals token-decimals :token-decimals token-decimals
:native-token? native-token? :native-token? native-token?
:to? to?}) :receiver? receiver?})
expected {"1" (money/bignumber "1") expected {"1" (money/bignumber "1")
"2" (money/bignumber "1")}] "2" (money/bignumber "1")}]
(doseq [[chain-id exp-value] expected] (doseq [[chain-id exp-value] expected]
@ -56,11 +56,11 @@
:to {:chain-id "1"}}] :to {:chain-id "1"}}]
token-decimals 18 token-decimals 18
native-token? true native-token? true
to? true receiver? true
result (utils/network-amounts-by-chain {:route route result (utils/network-amounts-by-chain {:route route
:token-decimals token-decimals :token-decimals token-decimals
:native-token? native-token? :native-token? native-token?
:to? to?}) :receiver? receiver?})
expected {"1" (money/bignumber "2")}] expected {"1" (money/bignumber "2")}]
(doseq [[chain-id exp-value] expected] (doseq [[chain-id exp-value] expected]
(is (money/equal-to (get result chain-id) exp-value))))) (is (money/equal-to (get result chain-id) exp-value)))))
@ -72,11 +72,11 @@
:from {:chain-id "2"}}] :from {:chain-id "2"}}]
token-decimals 6 token-decimals 6
native-token? false native-token? false
to? false receiver? false
result (utils/network-amounts-by-chain {:route route result (utils/network-amounts-by-chain {:route route
:token-decimals token-decimals :token-decimals token-decimals
:native-token? native-token? :native-token? native-token?
:to? to?}) :receiver? receiver?})
expected {"1" (money/bignumber "2") expected {"1" (money/bignumber "2")
"2" (money/bignumber "2")}] "2" (money/bignumber "2")}]
(doseq [[chain-id exp-value] expected] (doseq [[chain-id exp-value] expected]
@ -268,12 +268,13 @@
(is (every? identity comparisons))))) (is (every? identity comparisons)))))
(deftest test-network-amounts (deftest test-network-amounts
(testing "Handles disabled and receiver networks correctly when to? is true" (testing "Handles disabled and receiver networks correctly when receiver? is true"
(let [network-values {"1" (money/bignumber "100") (let [network-values {"1" (money/bignumber "100")
"10" (money/bignumber "200")} "10" (money/bignumber "200")}
disabled-chain-ids ["1"] disabled-chain-ids ["1"]
receiver-networks ["10"] receiver-networks ["10"]
to? true token-networks-ids ["1" "10" "42161"]
receiver? true
expected [{:chain-id "1" expected [{:chain-id "1"
:total-amount (money/bignumber "100") :total-amount (money/bignumber "100")
:type :default} :type :default}
@ -281,39 +282,44 @@
:total-amount (money/bignumber "200") :total-amount (money/bignumber "200")
:type :default} :type :default}
{:type :add}] {:type :add}]
result (utils/network-amounts network-values result (utils/network-amounts {:network-values network-values
disabled-chain-ids :disabled-chain-ids disabled-chain-ids
receiver-networks :receiver-networks receiver-networks
to?)] :token-networks-ids token-networks-ids
:receiver? receiver?})]
(is (every? identity (map #(map/deep-compare %1 %2) expected result))))) (is (every? identity (map #(map/deep-compare %1 %2) expected result)))))
(testing "Adds default amount for non-disabled non-receiver networks when to? is false" (testing "Adds default amount for non-disabled non-receiver networks when receiver? is false"
(let [network-values {"1" (money/bignumber "100")} (let [network-values {"1" (money/bignumber "100")}
disabled-chain-ids ["10"] disabled-chain-ids ["10"]
receiver-networks [] receiver-networks []
to? false token-networks-ids ["1" "10" "42161"]
receiver? false
expected [{:chain-id "1" expected [{:chain-id "1"
:total-amount (money/bignumber "100") :total-amount (money/bignumber "100")
:type :default} :type :default}
{:chain-id "10" {:chain-id "10"
:total-amount (money/bignumber "0") :total-amount (money/bignumber "0")
:type :disabled}] :type :disabled}]
result (utils/network-amounts network-values result (utils/network-amounts {:network-values network-values
disabled-chain-ids :disabled-chain-ids disabled-chain-ids
receiver-networks :receiver-networks receiver-networks
to?)] :token-networks-ids token-networks-ids
:receiver? receiver?})]
(is (every? identity (map #(map/deep-compare %1 %2) expected result))))) (is (every? identity (map #(map/deep-compare %1 %2) expected result)))))
(testing "Handles empty inputs correctly" (testing "Handles empty inputs correctly"
(let [network-values {} (let [network-values {}
disabled-chain-ids [] disabled-chain-ids []
receiver-networks [] receiver-networks []
to? true token-networks-ids []
receiver? true
expected [] expected []
result (utils/network-amounts network-values result (utils/network-amounts {:network-values network-values
disabled-chain-ids :disabled-chain-ids disabled-chain-ids
receiver-networks :receiver-networks receiver-networks
to?)] :token-networks-ids token-networks-ids
:receiver? receiver?})]
(is (= expected result)))) (is (= expected result))))
(testing "Processes case with multiple network interactions" (testing "Processes case with multiple network interactions"
@ -322,7 +328,8 @@
"42161" (money/bignumber "500")} "42161" (money/bignumber "500")}
disabled-chain-ids ["1" "42161"] disabled-chain-ids ["1" "42161"]
receiver-networks ["10"] receiver-networks ["10"]
to? true token-networks-ids ["1" "10" "42161"]
receiver? true
expected [{:chain-id "1" expected [{:chain-id "1"
:total-amount (money/bignumber "300") :total-amount (money/bignumber "300")
:type :default} :type :default}
@ -332,10 +339,50 @@
{:chain-id "42161" {:chain-id "42161"
:total-amount (money/bignumber "500") :total-amount (money/bignumber "500")
:type :default}] :type :default}]
result (utils/network-amounts network-values result (utils/network-amounts {:network-values network-values
disabled-chain-ids :disabled-chain-ids disabled-chain-ids
receiver-networks :receiver-networks receiver-networks
to?)] :token-networks-ids token-networks-ids
:receiver? receiver?})]
(is (every? identity (map #(map/deep-compare %1 %2) expected result)))))
(testing "Does not assign :not-available type when receiver? is false"
(let [network-values {"1" (money/bignumber "100")}
disabled-chain-ids ["10"]
receiver-networks ["1"]
token-networks-ids ["1" "10"]
receiver? false
expected [{:chain-id "1"
:total-amount (money/bignumber "100")
:type :default}
{:chain-id "10"
:total-amount (money/bignumber "0")
:type :disabled}]
result (utils/network-amounts {:network-values network-values
:disabled-chain-ids disabled-chain-ids
:receiver-networks receiver-networks
:token-networks-ids token-networks-ids
:receiver? receiver?})]
(is (every? identity (map #(map/deep-compare %1 %2) expected result)))))
(testing
"Assigns :not-available type to networks not available in token-networks-ids when receiver? is true"
(let [network-values {"1" (money/bignumber "100")}
disabled-chain-ids []
receiver-networks ["1" "10"]
token-networks-ids ["1"]
receiver? false
expected [{:chain-id "1"
:total-amount (money/bignumber "100")
:type :default}
{:chain-id "10"
:total-amount nil
:type :not-available}]
result (utils/network-amounts {:network-values network-values
:disabled-chain-ids disabled-chain-ids
:receiver-networks receiver-networks
:token-networks-ids token-networks-ids
:receiver? receiver?})]
(is (every? identity (map #(map/deep-compare %1 %2) expected result)))))) (is (every? identity (map #(map/deep-compare %1 %2) expected result))))))
(deftest test-loading-network-amounts (deftest test-loading-network-amounts
@ -343,63 +390,109 @@
(let [valid-networks ["1" "10" "42161"] (let [valid-networks ["1" "10" "42161"]
disabled-chain-ids ["42161"] disabled-chain-ids ["42161"]
receiver-networks ["1" "10"] receiver-networks ["1" "10"]
to? true token-networks-ids ["1" "10" "42161"]
receiver? true
expected [{:chain-id "1" :type :loading} expected [{:chain-id "1" :type :loading}
{:chain-id "10" :type :loading} {:chain-id "10" :type :loading}
{:type :add}] {:type :add}]
result (utils/loading-network-amounts valid-networks result (utils/loading-network-amounts {:valid-networks valid-networks
disabled-chain-ids :disabled-chain-ids disabled-chain-ids
receiver-networks :receiver-networks receiver-networks
to?) :token-networks-ids token-networks-ids
:receiver? receiver?})
comparisons (map #(map/deep-compare %1 %2) comparisons (map #(map/deep-compare %1 %2)
expected expected
result)] result)]
(is (every? identity comparisons)))) (is (every? identity comparisons))))
(testing "Assigns :disabled type with zero total-amount to disabled networks when to? is false" (testing "Assigns :disabled type with zero total-amount to disabled networks when receiver? is false"
(let [valid-networks ["1" "10" "42161"] (let [valid-networks ["1" "10" "42161"]
disabled-chain-ids ["10" "42161"] disabled-chain-ids ["10" "42161"]
receiver-networks ["1"] receiver-networks ["1"]
to? false token-networks-ids ["1" "10" "42161"]
receiver? false
expected [{:chain-id "1" :type :loading} expected [{:chain-id "1" :type :loading}
{:chain-id "10" :type :disabled :total-amount (money/bignumber "0")} {:chain-id "10" :type :disabled :total-amount (money/bignumber "0")}
{:chain-id "42161" :type :disabled :total-amount (money/bignumber "0")}] {:chain-id "42161" :type :disabled :total-amount (money/bignumber "0")}]
result (utils/loading-network-amounts valid-networks result (utils/loading-network-amounts {:valid-networks valid-networks
disabled-chain-ids :disabled-chain-ids disabled-chain-ids
receiver-networks :receiver-networks receiver-networks
to?) :token-networks-ids token-networks-ids
:receiver? receiver?})
comparisons (map #(map/deep-compare %1 %2) comparisons (map #(map/deep-compare %1 %2)
expected expected
result)] result)]
(is (every? identity comparisons)))) (is (every? identity comparisons))))
(testing "Filters out networks not in receiver networks when to? is true" (testing "Filters out networks not in receiver networks when receiver? is true"
(let [valid-networks ["1" "10" "42161" "59144"] (let [valid-networks ["1" "10" "42161" "59144"]
disabled-chain-ids ["10"] disabled-chain-ids ["10"]
receiver-networks ["1" "42161"] receiver-networks ["1" "42161"]
to? true token-networks-ids ["1" "10" "42161"]
receiver? true
expected [{:chain-id "1" :type :loading} expected [{:chain-id "1" :type :loading}
{:chain-id "42161" :type :loading}] {:chain-id "42161" :type :loading}]
result (utils/loading-network-amounts valid-networks result (utils/loading-network-amounts {:valid-networks valid-networks
disabled-chain-ids :disabled-chain-ids disabled-chain-ids
receiver-networks :receiver-networks receiver-networks
to?) :token-networks-ids token-networks-ids
:receiver? receiver?})
comparisons (map #(map/deep-compare %1 %2) comparisons (map #(map/deep-compare %1 %2)
expected expected
result)] result)]
(is (every? identity comparisons)))) (is (every? identity comparisons))))
(testing "Appends :add type if receiver network count is less than available networks and to? is true" (testing
"Appends :add type if receiver network count is less than available networks and receiver? is true"
(let [valid-networks ["1" "10" "42161"] (let [valid-networks ["1" "10" "42161"]
disabled-chain-ids ["10"] disabled-chain-ids ["10"]
receiver-networks ["1"] receiver-networks ["1"]
to? true token-networks-ids ["1" "10" "42161"]
receiver? true
expected [{:chain-id "1" :type :loading} expected [{:chain-id "1" :type :loading}
{:type :add}] {:type :add}]
result (utils/loading-network-amounts valid-networks result (utils/loading-network-amounts {:valid-networks valid-networks
disabled-chain-ids :disabled-chain-ids disabled-chain-ids
receiver-networks :receiver-networks receiver-networks
to?) :token-networks-ids token-networks-ids
:receiver? receiver?})
comparisons (map #(map/deep-compare %1 %2)
expected
result)]
(is (every? identity comparisons))))
(testing
"Assigns :not-available type to networks not available in token-networks-ids when receiver? is false"
(let [valid-networks ["42161"]
disabled-chain-ids []
receiver-networks ["1"]
token-networks-ids ["42161"]
receiver? false
expected [{:chain-id "42161" :type :loading}]
result (utils/loading-network-amounts {:valid-networks valid-networks
:disabled-chain-ids disabled-chain-ids
:receiver-networks receiver-networks
:token-networks-ids token-networks-ids
:receiver? receiver?})
comparisons (map #(map/deep-compare %1 %2)
expected
result)]
(is (every? identity comparisons))))
(testing
"Assigns :not-available type to networks not available in token-networks-ids when receiver? is true"
(let [valid-networks ["42161"]
disabled-chain-ids []
receiver-networks ["1"]
token-networks-ids ["42161"]
receiver? true
expected [{:chain-id "1" :type :not-available}
{:type :add}]
result (utils/loading-network-amounts {:valid-networks valid-networks
:disabled-chain-ids disabled-chain-ids
:receiver-networks receiver-networks
:token-networks-ids token-networks-ids
:receiver? receiver?})
comparisons (map #(map/deep-compare %1 %2) comparisons (map #(map/deep-compare %1 %2)
expected expected
result)] result)]

View File

@ -12,3 +12,26 @@
(def data-item (def data-item
{:margin-horizontal 20 {:margin-horizontal 20
:margin-vertical 8}) :margin-vertical 8})
(defn warning-container
[sending-to-unpreferred-networks?]
{:flex-direction :row
:margin-horizontal 20
:margin-bottom (when sending-to-unpreferred-networks? 8)
:align-items :center})
(defn sending-to-unpreferred-networks-alert-container
[theme]
{:height 76
:flex-direction :row
:background-color (colors/resolve-color :blue theme 5)
:border-color (colors/resolve-color :blue theme 10)
:border-width 1
:border-radius 12
:margin-horizontal 20
:padding 10})
(def sending-to-unpreferred-networks-text
{:flex 1
:height 54.6
:margin-left 8})

View File

@ -3,6 +3,7 @@
[quo.foundations.colors :as colors] [quo.foundations.colors :as colors]
[quo.theme :as quo.theme] [quo.theme :as quo.theme]
[react-native.blur :as blur] [react-native.blur :as blur]
[react-native.core :as rn]
[reagent.core :as reagent] [reagent.core :as reagent]
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.contexts.wallet.common.utils :as utils] [status-im.contexts.wallet.common.utils :as utils]
@ -11,12 +12,14 @@
[utils.re-frame :as rf])) [utils.re-frame :as rf]))
(defn view (defn view
[{:keys [selected-networks account watch-only?]}] [{:keys [title first-section-label second-section-label selected-networks
receiver-preferred-networks account watch-only?]}]
(let [state (reagent/atom :default) (let [state (reagent/atom :default)
{:keys [color address {:keys [color address
network-preferences-names]} (or account (rf/sub [:wallet/current-viewing-account])) network-preferences-names]} (or account (rf/sub [:wallet/current-viewing-account]))
initial-network-preferences-names (or selected-networks network-preferences-names) initial-network-preferences-names (or selected-networks network-preferences-names)
network-preferences-names-state (reagent/atom #{}) receiver? (boolean (not-empty receiver-preferred-networks))
network-preferences-names-state (reagent/atom (if receiver? selected-networks #{}))
toggle-network (fn [network-name] toggle-network (fn [network-name]
(reset! state :changed) (reset! state :changed)
(let [contains-network? (contains? (let [contains-network? (contains?
@ -34,15 +37,30 @@
(if (= @state :default) (if (= @state :default)
initial-network-preferences-names initial-network-preferences-names
@network-preferences-names-state))] @network-preferences-names-state))]
(fn [{:keys [on-save blur? button-label]}] (fn [{:keys [on-save on-change blur? button-label first-section-warning-label
(let [theme (quo.theme/use-theme) second-section-warning-label]}]
network-details (rf/sub [:wallet/network-details]) (let [theme (quo.theme/use-theme)
mainnet (first network-details) network-details (rf/sub [:wallet/network-details])
layer-2-networks (rest network-details) first-section-networks (filter (fn [network]
current-networks (filter (fn [network] (if receiver-preferred-networks
(contains? (get-current-preferences-names) (some (fn [chain-id]
(:network-name network))) (= (:chain-id network) chain-id))
network-details)] receiver-preferred-networks)
(= (:network-name network) :mainnet)))
network-details)
second-section-networks (remove (fn [network]
(some (fn [chain-id]
(= (:chain-id network) chain-id))
(map :chain-id first-section-networks)))
network-details)
current-networks (filter (fn [network]
(contains? (get-current-preferences-names)
(:network-name network)))
network-details)
sending-to-unpreferred-networks? (and receiver?
(some #(contains? @network-preferences-names-state
(:network-name %))
second-section-networks))]
[:<> [:<>
;; quo/overlay isn't compatible with sheets ;; quo/overlay isn't compatible with sheets
(when blur? (when blur?
@ -51,52 +69,103 @@
:blur-amount 20 :blur-amount 20
:blur-radius 25}]) :blur-radius 25}])
[quo/drawer-top [quo/drawer-top
{:title (i18n/label :t/network-preferences) {:title (or title (i18n/label :t/network-preferences))
:description (if watch-only? :description (when-not receiver?
(i18n/label :t/network-preferences-desc-1) (if watch-only?
(i18n/label :t/network-preferences-desc-2)) (i18n/label :t/network-preferences-desc-1)
(i18n/label :t/network-preferences-desc-2)))
:blur? blur?}] :blur? blur?}]
[quo/data-item (when-not receiver?
{:status :default [quo/data-item
:size :default {:status :default
:description :default :size :default
:label :none :description :default
:blur? blur? :label :none
:card? true :blur? blur?
:title (i18n/label :t/address) :card? true
:custom-subtitle (fn [] :title (i18n/label :t/address)
[quo/address-text :custom-subtitle (fn []
{:networks current-networks [quo/address-text
:address address {:networks current-networks
:blur? blur? :address address
:format :long}]) :blur? blur?
:container-style (merge style/data-item :format :long}])
{:background-color (colors/theme-colors colors/neutral-2_5 :container-style (merge style/data-item
colors/neutral-90 {:background-color (colors/theme-colors colors/neutral-2_5
theme)})}] colors/neutral-90
theme)})}])
[quo/category [quo/category
{:list-type :settings {:list-type :settings
:blur? blur? :blur? blur?
:data [(utils/make-network-item {:state @state :label (when first-section-label first-section-label)
:network-name (:network-name mainnet)
:color color
:blur? blur?
:networks (get-current-preferences-names)
:on-change #(toggle-network (:network-name
mainnet))})]}]
[quo/category
{:list-type :settings
:blur? blur?
:label (i18n/label :t/layer-2)
:data (mapv (fn [network] :data (mapv (fn [network]
(utils/make-network-item {:state @state (utils/make-network-item
:network-name (:network-name network) {:state @state
:color color :network-name (:network-name network)
:blur? blur? :color color
:networks (get-current-preferences-names) :normal-checkbox? receiver?
:on-change #(toggle-network (:network-name :networks (get-current-preferences-names)
network))})) :type :checkbox
layer-2-networks)}] :on-change (fn []
(toggle-network (:network-name
network))
(when on-change
(let [chain-ids (map :chain-id current-networks)]
(on-change chain-ids))))}))
first-section-networks)}]
(when first-section-warning-label
[rn/view
{:style (style/warning-container false)}
[quo/icon :i/alert
{:size 16
:color colors/danger-50}]
[quo/text
{:size :paragraph-2
:style {:margin-left 4
:color colors/danger-50}}
first-section-warning-label]])
(when (not-empty second-section-networks)
[quo/category
{:list-type :settings
:blur? blur?
:label (or second-section-label (i18n/label :t/layer-2))
:data (mapv (fn [network]
(utils/make-network-item
{:state @state
:network-name (:network-name network)
:color color
:normal-checkbox? receiver?
:networks (get-current-preferences-names)
:type :checkbox
:on-change (fn []
(toggle-network (:network-name
network))
(when on-change
(let [chain-ids (map :chain-id current-networks)]
(on-change chain-ids))))}))
second-section-networks)}])
(when second-section-warning-label
[rn/view
{:style (style/warning-container sending-to-unpreferred-networks?)}
[quo/icon :i/alert
{:size 16
:color colors/danger-50}]
[quo/text
{:size :paragraph-2
:style {:margin-left 4
:color colors/danger-50}}
second-section-warning-label]])
(when sending-to-unpreferred-networks?
[rn/view {:style (style/sending-to-unpreferred-networks-alert-container theme)}
[rn/view
[quo/icon :i/alert
{:size 16
:color (colors/resolve-color :blue theme)
:container-style {:margin-top 2}}]]
[quo/text
{:style style/sending-to-unpreferred-networks-text
:size :paragraph-2}
(i18n/label :t/sending-to-networks-the-receiver-does-not-prefer)]])
[quo/bottom-actions [quo/bottom-actions
{:actions :one-action {:actions :one-action
:blur? blur? :blur? blur?

View File

@ -0,0 +1,10 @@
(ns status-im.contexts.wallet.sheets.unpreferred-networks-alert.style)
(def sending-to-unpreferred-networks-title
{:margin-horizontal 20})
(def sending-to-unpreferred-networks-text
{:flex 1
:height 66
:margin-horizontal 20
:margin-vertical 4})

View File

@ -0,0 +1,28 @@
(ns status-im.contexts.wallet.sheets.unpreferred-networks-alert.view
(:require [quo.core :as quo]
[re-frame.core :as rf]
[status-im.contexts.wallet.sheets.unpreferred-networks-alert.style :as style]
[utils.i18n :as i18n]))
(defn view
[{:keys [on-confirm]}]
[:<>
[quo/text
{:style style/sending-to-unpreferred-networks-title
:size :heading-2
:weight :semi-bold}
(i18n/label :t/sending-to-unpreferred-networks)]
[quo/text
{:style style/sending-to-unpreferred-networks-text
:size :paragraph-1}
(i18n/label :t/sending-to-networks-the-receiver-does-not-prefer)]
[quo/bottom-actions
{:actions :two-actions
:button-two-label (i18n/label :t/cancel)
:button-two-props {:on-press #(rf/dispatch [:hide-bottom-sheet])
:type :grey}
:button-one-label (i18n/label :t/proceed-anyway)
:button-one-props {:on-press (fn []
(rf/dispatch [:hide-bottom-sheet])
(when on-confirm (on-confirm)))
:customization-color :danger}}]])

View File

@ -42,3 +42,8 @@
(= sender current-viewing-account-address)) (= sender current-viewing-account-address))
activities)] activities)]
(set (map :recipient users-sent-transactions))))) (set (map :recipient users-sent-transactions)))))
(rf/reg-sub
:wallet/send-token-not-supported-in-receiver-networks?
:<- [:wallet/wallet-send]
:-> :token-not-supported-in-receiver-networks?)

View File

@ -88,6 +88,11 @@
:<- [:wallet/wallet-send] :<- [:wallet/wallet-send]
:-> :receiver-networks) :-> :receiver-networks)
(rf/reg-sub
:wallet/wallet-send-receiver-preferred-networks
:<- [:wallet/wallet-send]
:-> :receiver-preferred-networks)
(rf/reg-sub (rf/reg-sub
:wallet/wallet-send-route :wallet/wallet-send-route
:<- [:wallet/wallet-send] :<- [:wallet/wallet-send]

View File

@ -2624,7 +2624,14 @@
"this-account-has-no-activity": "This account has no activity", "this-account-has-no-activity": "This account has no activity",
"this-address-has-activity": "This address has activity", "this-address-has-activity": "This address has activity",
"scanning-for-activity": "Scanning for activity...", "scanning-for-activity": "Scanning for activity...",
"send-community-link": "Send community link",
"at-least-one-network-must-be-activated": "At least 1 network must be activated", "at-least-one-network-must-be-activated": "At least 1 network must be activated",
"status-is-a-secure-messaging-app": "Status is a secure messaging app, crypto wallet and web3 browser built with the state of the art technology" "send-community-link": "Send community link",
"status-is-a-secure-messaging-app": "Status is a secure messaging app, crypto wallet and web3 browser built with the state of the art technology",
"token-not-available-on-receiver-networks": "{{token-symbol}} is not available on the recipients desired networks. Proceed with caution.",
"add-networks-token-can-be-sent-to": "Add networks {{token-symbol}} can be sent to",
"not-available": "Not available",
"token-not-available-on-networks": "{{token-symbol}} is not available on {{networks}}.",
"sending-to-networks-the-receiver-does-not-prefer": "Sending to networks the receiver does not prefer may result in recipient having difficulty accessing the sent tokens.",
"proceed-anyway": "Proceed anyway",
"sending-to-unpreferred-networks": "Sending to unpreferred networks"
} }