diff --git a/src/status_im/contexts/wallet/common/utils.cljs b/src/status_im/contexts/wallet/common/utils.cljs index 4bf5846a1e..7d4d68f238 100644 --- a/src/status_im/contexts/wallet/common/utils.cljs +++ b/src/status_im/contexts/wallet/common/utils.cljs @@ -229,15 +229,16 @@ (defn make-network-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)) :image :icon-avatar :image-props {:icon (resources/get-network network-name) :size :size-20} :action :selector - :action-props {:type (if (= :default state) - :filled-checkbox - :checkbox) + :action-props {:type (or type + (if (= :default state) + :filled-checkbox + :checkbox)) :customization-color color :checked? (contains? networks network-name) :on-change on-change}} diff --git a/src/status_im/contexts/wallet/common/utils/networks.cljs b/src/status_im/contexts/wallet/common/utils/networks.cljs index 6233d14570..bb4c55dd97 100644 --- a/src/status_im/contexts/wallet/common/utils/networks.cljs +++ b/src/status_im/contexts/wallet/common/utils/networks.cljs @@ -3,6 +3,8 @@ [status-im.constants :as constants] [utils.number])) +(def ^:private last-comma-followed-by-text-to-end-regex #",\s(?=[^,]+$)") + (def id->network {constants/ethereum-mainnet-chain-id constants/mainnet-network-name constants/ethereum-goerli-chain-id constants/mainnet-network-name @@ -113,3 +115,13 @@ (as-> prefix $ (string/split $ ":") (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)) diff --git a/src/status_im/contexts/wallet/common/utils/networks_test.cljs b/src/status_im/contexts/wallet/common/utils/networks_test.cljs index cf1903d316..e3f20eb00e 100644 --- a/src/status_im/contexts/wallet/common/utils/networks_test.cljs +++ b/src/status_im/contexts/wallet/common/utils/networks_test.cljs @@ -43,3 +43,21 @@ (seq [:mainnet]) "eth" (seq [:mainnet :optimism]) "eth:opt" (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]))))) diff --git a/src/status_im/contexts/wallet/send/events.cljs b/src/status_im/contexts/wallet/send/events.cljs index a57777faf8..76d8a99871 100644 --- a/src/status_im/contexts/wallet/send/events.cljs +++ b/src/status_im/contexts/wallet/send/events.cljs @@ -39,32 +39,38 @@ token-decimals (if collectible 0 (:decimals token)) native-token? (and token (= token-display-name "ETH")) 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 :token-decimals token-decimals :native-token? native-token? - :to? false}) + :receiver? false}) 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 :token-decimals token-decimals :native-token? native-token? - :to? true}) + :receiver? true}) to-network-values-for-ui (send-utils/network-values-for-ui to-network-amounts-by-chain) sender-network-values (if routes-available? - (send-utils/network-amounts from-network-values-for-ui - disabled-from-chain-ids - receiver-networks - false) + (send-utils/network-amounts + {:network-values from-network-values-for-ui + :disabled-chain-ids disabled-from-chain-ids + :receiver-networks receiver-networks + :token-networks-ids token-networks-ids + :receiver? false}) (send-utils/reset-network-amounts-to-zero sender-network-values)) receiver-network-values (if routes-available? - (send-utils/network-amounts to-network-values-for-ui - disabled-from-chain-ids - receiver-networks - true) + (send-utils/network-amounts + {:network-values to-network-values-for-ui + :disabled-chain-ids disabled-from-chain-ids + :receiver-networks receiver-networks + :token-networks-ids token-networks-ids + :receiver? true}) (send-utils/reset-network-amounts-to-zero receiver-network-values)) network-links (when routes-available? @@ -147,6 +153,7 @@ (assoc-in [:wallet :ui :send :recipient] (or recipient address)) (assoc-in [:wallet :ui :send :to-address] to-address) (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)) :fx [(when (and collectible-tx? one-collectible?) [: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 ;; sender yet, so we only provide the `token-symbol`, later in ;; `:wallet/select-from-account` the `token` key will be set. - {:db (cond-> db - :always (update-in [:wallet :ui :send] dissoc :collectible) - :always (assoc-in [:wallet :ui :send :token-display-name] (:symbol token)) - 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}]]]})) + (let [{token-networks :networks} token + receiver-networks (get-in db [:wallet :ui :send :receiver-networks]) + token-networks-ids (mapv #(:chain-id %) token-networks) + token-not-supported-in-receiver-networks? (not (some (set receiver-networks) + token-networks-ids))] + {:db (cond-> db + :always (update-in [:wallet :ui :send] dissoc :collectible) + :always (assoc-in [:wallet :ui :send :token-display-name] + (:symbol token)) + :always (assoc-in + [: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 :wallet/edit-token-to-send (fn [{:keys [db]} [token]] - {:db (-> db - (assoc-in [:wallet :ui :send :token] token) - (assoc-in [:wallet :ui :send :token-display-name] token)) - :fx [[:dispatch [:hide-bottom-sheet]] - [:dispatch [:wallet/clean-suggested-routes]]]})) + (let [{token-networks :networks} token + receiver-networks (get-in db [:wallet :ui :send :receiver-networks]) + token-networks-ids (mapv #(:chain-id %) token-networks) + token-not-supported-in-receiver-networks? (not (some (set receiver-networks) + 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 (fn [{:keys [db]}] @@ -307,18 +332,21 @@ balances-per-chain :disabled-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 (send-utils/loading-network-amounts - token-available-networks-for-suggested-routes - disabled-from-chain-ids - receiver-networks - false)) + {:valid-networks token-available-networks-for-suggested-routes + :disabled-chain-ids disabled-from-chain-ids + :receiver-networks receiver-networks + :token-networks-ids token-networks-ids + :receiver? false})) receiver-network-values (when token-available-networks-for-suggested-routes (send-utils/loading-network-amounts - token-available-networks-for-suggested-routes - disabled-from-chain-ids - receiver-networks - true)) + {:valid-networks token-available-networks-for-suggested-routes + :disabled-chain-ids disabled-from-chain-ids + :receiver-networks receiver-networks + :token-networks-ids token-networks-ids + :receiver? true})) request-params [transaction-type-param from-address to-address diff --git a/src/status_im/contexts/wallet/send/input_amount/component_spec.cljs b/src/status_im/contexts/wallet/send/input_amount/component_spec.cljs index 55fd5c88c7..db18247a5a 100644 --- a/src/status_im/contexts/wallet/send/input_amount/component_spec.cljs +++ b/src/status_im/contexts/wallet/send/input_amount/component_spec.cljs @@ -76,7 +76,8 @@ :wallet/wallet-send-to-values-by-chain {1 (money/bignumber "250")} :wallet/wallet-send-sender-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/setup-restorable-re-frame) diff --git a/src/status_im/contexts/wallet/send/input_amount/style.cljs b/src/status_im/contexts/wallet/send/input_amount/style.cljs index 34124d6b8d..28441f4956 100644 --- a/src/status_im/contexts/wallet/send/input_amount/style.cljs +++ b/src/status_im/contexts/wallet/send/input_amount/style.cljs @@ -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 {:flex 1}) @@ -38,3 +39,24 @@ {:height 40 :width "100%" :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)}) diff --git a/src/status_im/contexts/wallet/send/input_amount/view.cljs b/src/status_im/contexts/wallet/send/input_amount/view.cljs index 727542c0c3..c5c9ecbdab 100644 --- a/src/status_im/contexts/wallet/send/input_amount/view.cljs +++ b/src/status_im/contexts/wallet/send/input_amount/view.cljs @@ -2,6 +2,8 @@ (:require [clojure.string :as string] [quo.core :as quo] + [quo.foundations.colors :as colors] + [quo.theme] [react-native.core :as rn] [react-native.safe-area :as safe-area] [reagent.core :as reagent] @@ -12,6 +14,7 @@ [status-im.contexts.wallet.send.input-amount.style :as style] [status-im.contexts.wallet.send.routes.view :as routes] [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.i18n :as i18n] [utils.money :as money] @@ -26,7 +29,7 @@ string/upper-case))) (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-content-container} [quo/button @@ -35,20 +38,20 @@ :size 32 :inner-style {:opacity 1} :accessibility-label :advanced-button - :disabled? loading-suggested-routes? + :disabled? loading-routes? :on-press #(js/alert "Not implemented yet")} :i/advanced]] [quo/data-item {:container-style style/fees-data-item :label :none - :status (if loading-suggested-routes? :loading :default) + :status (if loading-routes? :loading :default) :size :small :title (i18n/label :t/fees) :subtitle fees}] [quo/data-item {:container-style style/amount-data-item :label :none - :status (if loading-suggested-routes? :loading :default) + :status (if loading-routes? :loading :default) :size :small :title (i18n/label :t/user-gets {:name receiver}) :subtitle amount}]]) @@ -77,6 +80,36 @@ (rf/dispatch [:wallet/edit-token-to-send token]) (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 ;; crypto-decimals, limit-crypto and initial-crypto-currency? args are needed ;; for component tests only @@ -94,91 +127,112 @@ crypto-currency? (reagent/atom initial-crypto-currency?) on-navigate-back on-navigate-back] (fn [] - (let [[input-state set-input-state] (rn/use-state controlled-input/init-state) - clear-input! #(set-input-state controlled-input/delete-all) - handle-on-confirm (fn [] - (rf/dispatch [:wallet/set-token-amount-to-send - {:amount (controlled-input/input-value - input-state) - :stack-id current-screen-id}])) - {fiat-currency :currency} (rf/sub [:profile/profile]) + (let [[input-state set-input-state] (rn/use-state controlled-input/init-state) + clear-input! #(set-input-state controlled-input/delete-all) + handle-on-confirm (fn [] + (rf/dispatch [:wallet/set-token-amount-to-send + {:amount + (controlled-input/input-value + input-state) + :stack-id current-screen-id}])) + {fiat-currency :currency} (rf/sub [:profile/profile]) {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 :as - token} (rf/sub - [:wallet/current-viewing-account-tokens-filtered - (str token-symbol)]) - conversion-rate (-> token :market-values-per-currency :usd :price) - loading-routes? (rf/sub - [:wallet/wallet-send-loading-suggested-routes?]) + token} (rf/sub + [:wallet/current-viewing-account-tokens-filtered + (str token-symbol)]) + conversion-rate (-> token :market-values-per-currency :usd :price) + loading-routes? (rf/sub + [:wallet/wallet-send-loading-suggested-routes?]) - route (rf/sub [:wallet/wallet-send-route]) - to-address (rf/sub [:wallet/wallet-send-to-address]) + route (rf/sub [:wallet/wallet-send-route]) + to-address (rf/sub [:wallet/wallet-send-to-address]) - on-confirm (or default-on-confirm handle-on-confirm) - crypto-decimals (or default-crypto-decimals - (utils/get-crypto-decimals-count token)) - crypto-limit (or default-limit-crypto - (utils/get-standard-crypto-format - token - token-balance)) - fiat-limit (.toFixed (* token-balance conversion-rate) 2) - current-limit (if @crypto-currency? crypto-limit fiat-limit) - valid-input? (not (or (string/blank? (controlled-input/input-value - input-state)) - (<= (controlled-input/numeric-value input-state) 0) - (> (controlled-input/numeric-value input-state) - current-limit))) - current-currency (if @crypto-currency? token-symbol fiat-currency) - input-num-value (controlled-input/numeric-value input-state) - confirm-disabled? (or (nil? route) - (empty? route) - (string/blank? (controlled-input/input-value input-state)) - (<= input-num-value 0) - (> input-num-value current-limit)) - amount-text (str (controlled-input/input-value input-state) - " " - token-symbol) - first-route (first route) - native-currency-symbol (when-not confirm-disabled? - (get-in first-route [:from :native-currency-symbol])) - native-token (when native-currency-symbol - (rf/sub [:wallet/token-by-symbol - native-currency-symbol])) - fee-in-native-token (when-not confirm-disabled? - (send-utils/calculate-full-route-gas-fee route)) - fee-in-crypto-formatted (when fee-in-native-token - (utils/get-standard-crypto-format - native-token - fee-in-native-token)) - fee-in-fiat (when-not confirm-disabled? - (utils/calculate-token-fiat-value - {:currency fiat-currency - :balance fee-in-native-token - :token native-token})) - currency-symbol (rf/sub [:profile/currency-symbol]) - fee-formatted (when fee-in-fiat - (utils/get-standard-fiat-format - fee-in-crypto-formatted - currency-symbol - fee-in-fiat)) - show-select-asset-sheet #(rf/dispatch - [:show-bottom-sheet - {:content (fn [] - [select-asset-bottom-sheet - clear-input!])}]) - loading-suggested-routes? (rf/sub - [:wallet/wallet-send-loading-suggested-routes?]) - sender-network-values (rf/sub - [:wallet/wallet-send-sender-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-suggested-routes?))] + on-confirm (or default-on-confirm handle-on-confirm) + crypto-decimals (or default-crypto-decimals + (utils/get-crypto-decimals-count token)) + crypto-limit (or default-limit-crypto + (utils/get-standard-crypto-format + token + token-balance)) + fiat-limit (.toFixed (* token-balance conversion-rate) 2) + current-limit (if @crypto-currency? crypto-limit fiat-limit) + valid-input? (not (or (string/blank? + (controlled-input/input-value + input-state)) + (<= (controlled-input/numeric-value + input-state) + 0) + (> (controlled-input/numeric-value + input-state) + current-limit))) + current-currency (if @crypto-currency? token-symbol fiat-currency) + input-num-value (controlled-input/numeric-value input-state) + confirm-disabled? (or (nil? route) + (empty? route) + (string/blank? (controlled-input/input-value + input-state)) + (<= input-num-value 0) + (> input-num-value current-limit)) + amount-text (str (controlled-input/input-value input-state) + " " + token-symbol) + first-route (first route) + native-currency-symbol (when-not confirm-disabled? + (get-in first-route + [:from :native-currency-symbol])) + native-token (when native-currency-symbol + (rf/sub [:wallet/token-by-symbol + native-currency-symbol])) + fee-in-native-token (when-not confirm-disabled? + (send-utils/calculate-full-route-gas-fee route)) + fee-in-crypto-formatted (when fee-in-native-token + (utils/get-standard-crypto-format + native-token + fee-in-native-token)) + fee-in-fiat (when-not confirm-disabled? + (utils/calculate-token-fiat-value + {:currency fiat-currency + :balance fee-in-native-token + :token native-token})) + currency-symbol (rf/sub [:profile/currency-symbol]) + fee-formatted (when fee-in-fiat + (utils/get-standard-fiat-format + fee-in-crypto-formatted + currency-symbol + fee-in-fiat)) + show-select-asset-sheet #(rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [select-asset-bottom-sheet + clear-input!])}]) + sender-network-values (rf/sub + [:wallet/wallet-send-sender-network-values]) + receiver-network-values (rf/sub + [:wallet/wallet-send-receiver-network-values]) + 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 (fn [] (let [dismiss-keyboard-fn #(when (= % "active") (rn/dismiss-keyboard!)) @@ -211,16 +265,21 @@ :on-swap #(reset! crypto-currency? %) :on-token-press show-select-asset-sheet}] [routes/view - {:token token - :input-value (controlled-input/input-value input-state) - :valid-input? valid-input? - :current-screen-id current-screen-id}] + {:token token + :input-value (controlled-input/input-value input-state) + :valid-input? valid-input? + :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)) [estimated-fees - {:loading-suggested-routes? loading-routes? - :fees fee-formatted - :amount amount-text - :receiver (address/get-shortened-key to-address)}]) + {:loading-routes? loading-routes? + :fees fee-formatted + :amount amount-text + :receiver (address/get-shortened-key to-address)}]) (when no-routes-found? [rn/view {:style style/no-routes-found-container} [quo/info-message @@ -236,10 +295,14 @@ button-one-label) :button-one-props (merge button-one-props {: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 {:amount (controlled-input/input-value input-state)}]) + sending-to-unpreferred-networks? + #(show-unpreferred-networks-alert on-confirm) + :else on-confirm)} (when no-routes-found? {:type :grey}))}] diff --git a/src/status_im/contexts/wallet/send/routes/view.cljs b/src/status_im/contexts/wallet/send/routes/view.cljs index 596ea394fd..c1b0165aaa 100644 --- a/src/status_im/contexts/wallet/send/routes/view.cljs +++ b/src/status_im/contexts/wallet/send/routes/view.cljs @@ -1,14 +1,11 @@ (ns status-im.contexts.wallet.send.routes.view (:require - [clojure.string :as string] [quo.core :as quo] - [quo.foundations.colors :as colors] - [quo.foundations.resources :as resources] [react-native.core :as rn] - [reagent.core :as reagent] [status-im.contexts.wallet.common.utils.networks :as network-utils] [status-im.contexts.wallet.send.routes.style :as style] [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.i18n :as i18n] [utils.re-frame :as rf])) @@ -19,19 +16,6 @@ (def network-link-1x-height 56) (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 [amount valid-input? bounce-duration-ms] (if valid-input? @@ -40,82 +24,93 @@ bounce-duration-ms) (rf/dispatch [:wallet/clean-suggested-routes]))) -(defn networks-drawer - [{:keys [on-save theme]}] - (let [network-details (rf/sub [:wallet/network-details]) - {:keys [color]} (rf/sub [:wallet/current-viewing-account]) - selected-networks (rf/sub [:wallet/wallet-send-receiver-networks]) - prefix (rf/sub [:wallet/wallet-send-address-prefix]) - prefix-seq (string/split prefix #":") - grouped-details (group-by #(contains? (set prefix-seq) (:short-name %)) network-details) - preferred (get grouped-details true []) - not-preferred (get grouped-details false []) - network-preferences (reagent/atom selected-networks) - toggle-network (fn [{:keys [chain-id]}] - (swap! network-preferences - (fn [preferences] - (if (some #(= % chain-id) preferences) - (vec (remove #(= % chain-id) preferences)) - (conj preferences chain-id)))))] - (fn [] - [rn/view - [quo/drawer-top {:title (i18n/label :t/edit-receiver-networks)}] - [quo/category - {:list-type :settings - :label (i18n/label :t/preferred-by-receiver) - :data (mapv (fn [network] - (make-network-item network - {:color color - :network-preferences network-preferences - :on-change #(toggle-network network)})) - preferred)}] - (when (pos? (count not-preferred)) - [quo/category - {:list-type :settings - :label (i18n/label :t/not-preferred-by-receiver) - :data (mapv (fn [network] - (make-network-item network - {:color color - :network-preferences network-preferences - :on-change #(toggle-network network)})) - not-preferred)}]) - (when (not= selected-networks @network-preferences) - [rn/view {:style (style/warning-container color theme)} - [quo/icon :i/info {:color (colors/resolve-color color theme)}] - [quo/text - {:size :paragraph-2 - :style style/warning-text} (i18n/label :t/receiver-networks-warning)]]) - [quo/bottom-actions - {:actions :one-action - :button-one-label (i18n/label :t/apply-changes) - :button-one-props {:disabled? (or (= selected-networks @network-preferences) - (empty? @network-preferences)) - :on-press (fn [] - (rf/dispatch [:wallet/update-receiver-networks - @network-preferences]) - (rf/dispatch [:hide-bottom-sheet]) - (on-save)) - :customization-color color}}]]))) +(defn- open-preferences + [] + (rf/dispatch + [:show-bottom-sheet + {:content + (fn [] + (let [receiver-networks (rf/sub [:wallet/wallet-send-receiver-networks]) + receiver-preferred-networks (rf/sub + [:wallet/wallet-send-receiver-preferred-networks]) + {token-symbol :symbol + token-networks :networks} (rf/sub [:wallet/wallet-send-token]) + token-chain-ids-set (set (mapv #(:chain-id %) token-networks)) + [selected-receiver-networks + set-selected-receiver-networks] (rn/use-state receiver-networks) + receiver-preferred-networks-set (set receiver-preferred-networks) + receiver-selected-preferred-networks (filter #(contains? + receiver-preferred-networks-set + %) + selected-receiver-networks) + receiver-selected-non-preferred-networks (filter #(not (contains? + receiver-preferred-networks-set + %)) + selected-receiver-networks) + not-available-preferred-networks (filter (fn [preferred-chain-id] + (not (contains? token-chain-ids-set + preferred-chain-id))) + receiver-selected-preferred-networks) + not-available-non-preferred-networks (filter (fn [preferred-chain-id] + (not (contains? + token-chain-ids-set + preferred-chain-id))) + receiver-selected-non-preferred-networks) + first-section-warning-label (when (not-empty not-available-preferred-networks) + (i18n/label + :t/token-not-available-on-networks + {:token-symbol token-symbol + :networks + (network-utils/network-ids->formatted-text + not-available-preferred-networks)})) + second-section-warning-label (when (not-empty + not-available-non-preferred-networks) + (i18n/label + :t/token-not-available-on-networks + {:token-symbol token-symbol + :networks + (network-utils/network-ids->formatted-text + not-available-non-preferred-networks)}))] + [network-preferences/view + {:title (i18n/label :t/edit-receiver-networks) + :first-section-label (i18n/label :t/preferred-by-receiver) + :second-section-label (i18n/label :t/not-preferred-by-receiver) + :selected-networks (set (map network-utils/id->network + receiver-networks)) + :receiver-preferred-networks receiver-preferred-networks + :button-label (i18n/label :t/apply-changes) + :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 - [{: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 (map-indexed (fn [index {:keys [chain-id total-amount type]}] [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)}} [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) - :status type - :on-press #(when (not loading-suggested-routes?) + :status (cond (and (= type :not-available) + loading-routes? + token-not-supported-in-receiver-networks?) + :loading + (= type :not-available) + :disabled + :else type) + :on-press #(when (not loading-routes?) (cond (= type :add) - (rf/dispatch [:show-bottom-sheet - {:content (fn [] - [networks-drawer - {:theme theme - :on-save on-save}])}]) + (open-preferences) on-press (on-press chain-id total-amount)))}]]) network-values)]) @@ -180,12 +175,13 @@ (defn view [{: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) nav-current-screen-id (rf/sub [:view-id]) active-screen? (= nav-current-screen-id current-screen-id) - loading-suggested-routes? (rf/sub - [:wallet/wallet-send-loading-suggested-routes?]) + loading-routes? (rf/sub + [:wallet/wallet-send-loading-suggested-routes?]) sender-network-values (rf/sub [:wallet/wallet-send-sender-network-values]) receiver-network-values (rf/sub @@ -220,23 +216,27 @@ :container-style style/section-label-right}]]) [rn/view {:style style/routes-inner-container} [render-network-values - {:token-symbol token-symbol - :network-values sender-network-values - :on-press #(disable-chain %1 - disabled-from-chain-ids - token-available-networks-for-suggested-routes) - :to? false - :theme theme - :loading-suggested-routes? loading-suggested-routes?}] + {:token-symbol token-symbol + :network-values sender-network-values + :on-press (fn [chain-id-to-disable] + (disable-chain + chain-id-to-disable + disabled-from-chain-ids + token-available-networks-for-suggested-routes)) + :receiver? false + :theme theme + :loading-routes? loading-routes? + :token-not-supported-in-receiver-networks? false}] [render-network-links {:network-links network-links :sender-network-values sender-network-values}] [render-network-values - {:token-symbol token-symbol - :network-values receiver-network-values - :on-press on-press-to-network - :to? true - :loading-suggested-routes? loading-suggested-routes? - :theme theme - :on-save #(fetch-routes input-value valid-input? 0)}]]])) + {:token-symbol token-symbol + :network-values receiver-network-values + :on-press on-press-to-network + :receiver? true + :loading-routes? loading-routes? + :theme theme + :token-not-supported-in-receiver-networks? token-not-supported-in-receiver-networks? + :on-save #(fetch-routes input-value valid-input? 0)}]]])) diff --git a/src/status_im/contexts/wallet/send/utils.cljs b/src/status_im/contexts/wallet/send/utils.cljs index 5930bfd61d..dfbc6efe16 100644 --- a/src/status_im/contexts/wallet/send/utils.cljs +++ b/src/status_im/contexts/wallet/send/utils.cljs @@ -43,21 +43,22 @@ (money/wei->ether (reduce money/add (map calculate-gas-fee route)))) (defn network-amounts-by-chain - [{:keys [route token-decimals native-token? to?]}] - (reduce (fn [acc path] - (let [amount-hex (if to? (:amount-in path) (:amount-out path)) - amount-units (native-module/hex-to-number - (utils.hex/normalize-hex amount-hex)) - amount (money/with-precision - (if native-token? - (money/wei->ether amount-units) - (money/token->unit amount-units - token-decimals)) - 6) - chain-id (if to? (get-in path [:to :chain-id]) (get-in path [:from :chain-id]))] - (update acc chain-id money/add amount))) - {} - route)) + [{:keys [route token-decimals native-token? receiver?]}] + (reduce + (fn [acc path] + (let [amount-hex (if receiver? (:amount-in path) (:amount-out path)) + amount-units (native-module/hex-to-number + (utils.hex/normalize-hex amount-hex)) + amount (money/with-precision + (if native-token? + (money/wei->ether amount-units) + (money/token->unit amount-units + token-decimals)) + 6) + chain-id (if receiver? (get-in path [:to :chain-id]) (get-in path [:from :chain-id]))] + (update acc chain-id money/add amount))) + {} + route)) (defn network-values-for-ui [amounts] @@ -93,62 +94,103 @@ network-amounts)) (defn network-amounts - [network-values disabled-chain-ids receiver-networks to?] - (let [disabled-set (set disabled-chain-ids) - receiver-networks-set (set receiver-networks) - network-values-keys (set (keys network-values)) - routes-found? (pos? (count network-values-keys)) - updated-network-values (when routes-found? - (reduce (fn [acc k] - (if (or (contains? network-values-keys k) - (and to? - (not (contains? receiver-networks-set k)))) - acc - (assoc acc k (money/bignumber "0")))) - network-values - disabled-chain-ids))] - (cond-> (->> updated-network-values - (map - (fn [[k v]] - {:chain-id k - :total-amount v - :type (if (or to? (not (contains? disabled-set k))) :default :disabled)})) - (sort-by #(get network-priority-score (network-utils/id->network (:chain-id %)))) - (filter - #(or (and to? - (or (contains? receiver-networks-set (:chain-id %)) - (money/greater-than (:total-amount %) (money/bignumber "0")))) - (not to?))) - (vec)) - (and to? + [{:keys [network-values disabled-chain-ids receiver-networks token-networks-ids receiver?]}] + (let [disabled-set (set disabled-chain-ids) + receiver-networks-set (set receiver-networks) + network-values-keys (set (keys network-values)) + routes-found? (pos? (count network-values-keys)) + token-networks-ids-set (set token-networks-ids) + not-available-networks (if receiver? + (filter #(not (token-networks-ids-set %)) + receiver-networks) + []) + not-available-networks-set (set not-available-networks) + network-values-with-disabled-chains (when routes-found? + (reduce + (fn [acc k] + (if (or (contains? network-values-keys k) + (and receiver? + (not (contains? receiver-networks-set + k)))) + acc + (assoc acc k (money/bignumber "0")))) + network-values + disabled-chain-ids)) + network-values-with-not-available-chains (if (and receiver? routes-found?) + (let [network-values-keys + (set (keys + network-values-with-disabled-chains))] + (reduce + (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? - (< (count updated-network-values) available-networks-count)) + (< (count network-values-with-not-available-chains) available-networks-count)) (conj {:type :add})))) (defn loading-network-amounts - [valid-networks disabled-chain-ids receiver-networks to?] - (let [disabled-set (set disabled-chain-ids) - receiver-networks-set (set receiver-networks) - receiver-networks-count (count receiver-networks) - valid-networks (concat valid-networks disabled-chain-ids)] + [{:keys [valid-networks disabled-chain-ids receiver-networks token-networks-ids receiver?]}] + (let [disabled-set (set disabled-chain-ids) + receiver-networks-set (set receiver-networks) + receiver-networks-count (count receiver-networks) + 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 - (map (fn [k] - (cond-> {:chain-id k - :type (if (or to? - (not (contains? disabled-set k))) - :loading - :disabled)} - (and (not to?) (contains? disabled-set k)) - (assoc :total-amount (money/bignumber "0"))))) - (sort-by (fn [item] + (map + (fn [chain-id] + (cond-> + {:chain-id chain-id + :type (cond + (contains? not-available-networks-set chain-id) :not-available + (or receiver? + (not (contains? disabled-set chain-id))) :loading + (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 - (network-utils/id->network (:chain-id item))))) + (network-utils/id->network (:chain-id network-amount))))) (filter - #(or (and to? (contains? receiver-networks-set (:chain-id %))) - (and (not to?) - (not (contains? disabled-chain-ids (:chain-id %)))))) + (fn [network-amount] + (or (and receiver? (contains? receiver-networks-set (:chain-id network-amount))) + (and (not receiver?) + (not (contains? disabled-chain-ids (:chain-id network-amount))))))) (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 [route from-values-by-chain to-values-by-chain] diff --git a/src/status_im/contexts/wallet/send/utils_test.cljs b/src/status_im/contexts/wallet/send/utils_test.cljs index 097ec3d1f3..c72082aa28 100644 --- a/src/status_im/contexts/wallet/send/utils_test.cljs +++ b/src/status_im/contexts/wallet/send/utils_test.cljs @@ -38,11 +38,11 @@ :to {:chain-id "2"}}] token-decimals 18 native-token? true - to? true + receiver? true result (utils/network-amounts-by-chain {:route route :token-decimals token-decimals :native-token? native-token? - :to? to?}) + :receiver? receiver?}) expected {"1" (money/bignumber "1") "2" (money/bignumber "1")}] (doseq [[chain-id exp-value] expected] @@ -56,11 +56,11 @@ :to {:chain-id "1"}}] token-decimals 18 native-token? true - to? true + receiver? true result (utils/network-amounts-by-chain {:route route :token-decimals token-decimals :native-token? native-token? - :to? to?}) + :receiver? receiver?}) expected {"1" (money/bignumber "2")}] (doseq [[chain-id exp-value] expected] (is (money/equal-to (get result chain-id) exp-value))))) @@ -72,11 +72,11 @@ :from {:chain-id "2"}}] token-decimals 6 native-token? false - to? false + receiver? false result (utils/network-amounts-by-chain {:route route :token-decimals token-decimals :native-token? native-token? - :to? to?}) + :receiver? receiver?}) expected {"1" (money/bignumber "2") "2" (money/bignumber "2")}] (doseq [[chain-id exp-value] expected] @@ -268,12 +268,13 @@ (is (every? identity comparisons))))) (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") "10" (money/bignumber "200")} disabled-chain-ids ["1"] receiver-networks ["10"] - to? true + token-networks-ids ["1" "10" "42161"] + receiver? true expected [{:chain-id "1" :total-amount (money/bignumber "100") :type :default} @@ -281,39 +282,44 @@ :total-amount (money/bignumber "200") :type :default} {:type :add}] - result (utils/network-amounts network-values - disabled-chain-ids - receiver-networks - to?)] + 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 "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")} disabled-chain-ids ["10"] receiver-networks [] - to? false + token-networks-ids ["1" "10" "42161"] + 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 - disabled-chain-ids - receiver-networks - to?)] + 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 "Handles empty inputs correctly" (let [network-values {} disabled-chain-ids [] receiver-networks [] - to? true + token-networks-ids [] + receiver? true expected [] - result (utils/network-amounts network-values - disabled-chain-ids - receiver-networks - to?)] + 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 (= expected result)))) (testing "Processes case with multiple network interactions" @@ -322,7 +328,8 @@ "42161" (money/bignumber "500")} disabled-chain-ids ["1" "42161"] receiver-networks ["10"] - to? true + token-networks-ids ["1" "10" "42161"] + receiver? true expected [{:chain-id "1" :total-amount (money/bignumber "300") :type :default} @@ -332,10 +339,50 @@ {:chain-id "42161" :total-amount (money/bignumber "500") :type :default}] - result (utils/network-amounts network-values - disabled-chain-ids - receiver-networks - to?)] + 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 "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)))))) (deftest test-loading-network-amounts @@ -343,63 +390,109 @@ (let [valid-networks ["1" "10" "42161"] disabled-chain-ids ["42161"] receiver-networks ["1" "10"] - to? true + token-networks-ids ["1" "10" "42161"] + receiver? true expected [{:chain-id "1" :type :loading} {:chain-id "10" :type :loading} {:type :add}] - result (utils/loading-network-amounts valid-networks - disabled-chain-ids - receiver-networks - to?) + 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 :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"] disabled-chain-ids ["10" "42161"] receiver-networks ["1"] - to? false + token-networks-ids ["1" "10" "42161"] + receiver? false expected [{:chain-id "1" :type :loading} {:chain-id "10" :type :disabled :total-amount (money/bignumber "0")} {:chain-id "42161" :type :disabled :total-amount (money/bignumber "0")}] - result (utils/loading-network-amounts valid-networks - disabled-chain-ids - receiver-networks - to?) + 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 "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"] disabled-chain-ids ["10"] receiver-networks ["1" "42161"] - to? true + token-networks-ids ["1" "10" "42161"] + receiver? true expected [{:chain-id "1" :type :loading} {:chain-id "42161" :type :loading}] - result (utils/loading-network-amounts valid-networks - disabled-chain-ids - receiver-networks - to?) + 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 "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"] disabled-chain-ids ["10"] receiver-networks ["1"] - to? true + token-networks-ids ["1" "10" "42161"] + receiver? true expected [{:chain-id "1" :type :loading} {:type :add}] - result (utils/loading-network-amounts valid-networks - disabled-chain-ids - receiver-networks - to?) + 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 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) expected result)] diff --git a/src/status_im/contexts/wallet/sheets/network_preferences/style.cljs b/src/status_im/contexts/wallet/sheets/network_preferences/style.cljs index 1d40a1bd32..39d4249c73 100644 --- a/src/status_im/contexts/wallet/sheets/network_preferences/style.cljs +++ b/src/status_im/contexts/wallet/sheets/network_preferences/style.cljs @@ -12,3 +12,26 @@ (def data-item {:margin-horizontal 20 :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}) diff --git a/src/status_im/contexts/wallet/sheets/network_preferences/view.cljs b/src/status_im/contexts/wallet/sheets/network_preferences/view.cljs index 37ec3ba0cf..423d6f8381 100644 --- a/src/status_im/contexts/wallet/sheets/network_preferences/view.cljs +++ b/src/status_im/contexts/wallet/sheets/network_preferences/view.cljs @@ -3,6 +3,7 @@ [quo.foundations.colors :as colors] [quo.theme :as quo.theme] [react-native.blur :as blur] + [react-native.core :as rn] [reagent.core :as reagent] [status-im.constants :as constants] [status-im.contexts.wallet.common.utils :as utils] @@ -11,12 +12,14 @@ [utils.re-frame :as rf])) (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) {:keys [color address network-preferences-names]} (or account (rf/sub [:wallet/current-viewing-account])) 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] (reset! state :changed) (let [contains-network? (contains? @@ -34,15 +37,30 @@ (if (= @state :default) initial-network-preferences-names @network-preferences-names-state))] - (fn [{:keys [on-save blur? button-label]}] - (let [theme (quo.theme/use-theme) - network-details (rf/sub [:wallet/network-details]) - mainnet (first network-details) - layer-2-networks (rest network-details) - current-networks (filter (fn [network] - (contains? (get-current-preferences-names) - (:network-name network))) - network-details)] + (fn [{:keys [on-save on-change blur? button-label first-section-warning-label + second-section-warning-label]}] + (let [theme (quo.theme/use-theme) + network-details (rf/sub [:wallet/network-details]) + first-section-networks (filter (fn [network] + (if receiver-preferred-networks + (some (fn [chain-id] + (= (:chain-id network) chain-id)) + 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 (when blur? @@ -51,52 +69,103 @@ :blur-amount 20 :blur-radius 25}]) [quo/drawer-top - {:title (i18n/label :t/network-preferences) - :description (if watch-only? - (i18n/label :t/network-preferences-desc-1) - (i18n/label :t/network-preferences-desc-2)) + {:title (or title (i18n/label :t/network-preferences)) + :description (when-not receiver? + (if watch-only? + (i18n/label :t/network-preferences-desc-1) + (i18n/label :t/network-preferences-desc-2))) :blur? blur?}] - [quo/data-item - {:status :default - :size :default - :description :default - :label :none - :blur? blur? - :card? true - :title (i18n/label :t/address) - :custom-subtitle (fn [] - [quo/address-text - {:networks current-networks - :address address - :blur? blur? - :format :long}]) - :container-style (merge style/data-item - {:background-color (colors/theme-colors colors/neutral-2_5 - colors/neutral-90 - theme)})}] + (when-not receiver? + [quo/data-item + {:status :default + :size :default + :description :default + :label :none + :blur? blur? + :card? true + :title (i18n/label :t/address) + :custom-subtitle (fn [] + [quo/address-text + {:networks current-networks + :address address + :blur? blur? + :format :long}]) + :container-style (merge style/data-item + {:background-color (colors/theme-colors colors/neutral-2_5 + colors/neutral-90 + theme)})}]) [quo/category {:list-type :settings :blur? blur? - :data [(utils/make-network-item {:state @state - :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) + :label (when first-section-label first-section-label) :data (mapv (fn [network] - (utils/make-network-item {:state @state - :network-name (:network-name network) - :color color - :blur? blur? - :networks (get-current-preferences-names) - :on-change #(toggle-network (:network-name - network))})) - layer-2-networks)}] + (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))))})) + 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 {:actions :one-action :blur? blur? diff --git a/src/status_im/contexts/wallet/sheets/unpreferred_networks_alert/style.cljs b/src/status_im/contexts/wallet/sheets/unpreferred_networks_alert/style.cljs new file mode 100644 index 0000000000..505ca2dd1a --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/unpreferred_networks_alert/style.cljs @@ -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}) diff --git a/src/status_im/contexts/wallet/sheets/unpreferred_networks_alert/view.cljs b/src/status_im/contexts/wallet/sheets/unpreferred_networks_alert/view.cljs new file mode 100644 index 0000000000..95358a71f3 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/unpreferred_networks_alert/view.cljs @@ -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}}]]) diff --git a/src/status_im/subs/wallet/send.cljs b/src/status_im/subs/wallet/send.cljs index d12797eeaa..465d096f7b 100644 --- a/src/status_im/subs/wallet/send.cljs +++ b/src/status_im/subs/wallet/send.cljs @@ -42,3 +42,8 @@ (= sender current-viewing-account-address)) activities)] (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?) diff --git a/src/status_im/subs/wallet/wallet.cljs b/src/status_im/subs/wallet/wallet.cljs index 93a3ebc102..9e31f43e6c 100644 --- a/src/status_im/subs/wallet/wallet.cljs +++ b/src/status_im/subs/wallet/wallet.cljs @@ -88,6 +88,11 @@ :<- [:wallet/wallet-send] :-> :receiver-networks) +(rf/reg-sub + :wallet/wallet-send-receiver-preferred-networks + :<- [:wallet/wallet-send] + :-> :receiver-preferred-networks) + (rf/reg-sub :wallet/wallet-send-route :<- [:wallet/wallet-send] diff --git a/translations/en.json b/translations/en.json index 9dcdd69267..86c18c6b18 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2624,7 +2624,14 @@ "this-account-has-no-activity": "This account has no activity", "this-address-has-activity": "This address has 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", - "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 recipient’s 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" }