diff --git a/src/quo/components/wallet/swap_input/style.cljs b/src/quo/components/wallet/swap_input/style.cljs index 19928831add..b3f8c8bad6a 100644 --- a/src/quo/components/wallet/swap_input/style.cljs +++ b/src/quo/components/wallet/swap_input/style.cljs @@ -75,3 +75,19 @@ (def fiat-amount {:color colors/neutral-50}) + +(def gradient-start + {:width 64 + :position :absolute + :top 0 + :left 0 + :bottom 0 + :z-index 1}) + +(def gradient-end + {:width 64 + :position :absolute + :top 0 + :right 0 + :bottom 0 + :z-index 1}) diff --git a/src/quo/components/wallet/swap_input/view.cljs b/src/quo/components/wallet/swap_input/view.cljs index bacf3c1c484..9d35c9e012b 100644 --- a/src/quo/components/wallet/swap_input/view.cljs +++ b/src/quo/components/wallet/swap_input/view.cljs @@ -11,6 +11,7 @@ [quo.foundations.colors :as colors] quo.theme [react-native.core :as rn] + [react-native.linear-gradient :as linear-gradient] [schema.core :as schema])) (def ?schema @@ -42,26 +43,62 @@ [:container-style {:optional true} [:maybe :map]]]]] :any]) +(def icon-size 32) +(def container-padding 24) +(def max-cursor-position 5) + (defn view-internal [{:keys [type status token value fiat-value show-approval-label? error? network-tag-props approval-label-props default-value auto-focus? input-disabled? enable-swap? currency-symbol on-change-text show-keyboard? container-style on-swap-press on-token-press on-max-press on-input-focus]}] - (let [theme (quo.theme/use-theme) - pay? (= type :pay) - disabled? (= status :disabled) - loading? (= status :loading) - typing? (= status :typing) - controlled-input? (some? value) - input-ref (rn/use-ref-atom nil) - set-input-ref (rn/use-callback (fn [ref] (reset! input-ref ref)) []) - focus-input (rn/use-callback (fn [] - (some-> @input-ref - (oops/ocall "focus"))) - [input-ref])] + (let [theme (quo.theme/use-theme) + pay? (= type :pay) + disabled? (= status :disabled) + loading? (= status :loading) + typing? (= status :typing) + controlled-input? (some? value) + [container-width + set-container-width] (rn/use-state) + [overflow? + set-overflow?] (rn/use-state false) + [cursor-close-to-start? + set-cursor-close-to-start?] (rn/use-state false) + [label-width + set-label-width] (rn/use-state 0) + input-ref (rn/use-ref-atom nil) + set-input-ref (rn/use-callback (fn [ref] (reset! input-ref ref)) []) + focus-input (rn/use-callback (fn [] + (some-> @input-ref + (oops/ocall "focus"))) + [input-ref]) + on-layout-container (rn/use-callback + (fn [e] + (let [width (oops/oget e "nativeEvent.layout.width")] + (set-container-width width)))) + on-layout-text-input (rn/use-callback + (fn [e] + (let [width (oops/oget e "nativeEvent.layout.width") + max-width (- container-width + icon-size + container-padding + (* label-width 2))] + (set-overflow? (> width max-width)))) + [container-width label-width]) + on-layout-label (rn/use-callback + (fn [e] + (let [width (oops/oget e "nativeEvent.layout.width")] + (set-label-width width)))) + on-selection-change (rn/use-callback + (fn [e] + (let [selection-start (oops/oget e + "nativeEvent.selection.start")] + (set-cursor-close-to-start? + (< selection-start max-cursor-position)))))] [rn/view {:style container-style - :accessibility-label :swap-input} + :accessibility-label :swap-input + :on-layout on-layout-container} [rn/view {:style (style/content typing? theme)} [rn/view {:style (style/row-1 loading?)} @@ -75,25 +112,43 @@ [rn/pressable {:style style/input-container :on-press focus-input} - [rn/text-input - (cond-> {:ref set-input-ref - :style (style/input disabled? error? theme) - :placeholder-text-color (colors/theme-colors colors/neutral-40 - colors/neutral-50 - theme) - :keyboard-type :numeric - :editable (not input-disabled?) - :auto-focus auto-focus? - :on-focus on-input-focus - :on-change-text on-change-text - :show-soft-input-on-focus show-keyboard? - :default-value default-value - :placeholder "0"} - controlled-input? (assoc :value value))] + [rn/view {:style {:flex-shrink 1}} + (when (and overflow? typing? (not cursor-close-to-start?)) + [linear-gradient/linear-gradient + {:start {:x 0 :y 0} + :end {:x 1 :y 0} + :colors [(colors/theme-colors colors/white colors/neutral-100 theme) + (colors/theme-colors colors/white-opa-10 colors/neutral-100-opa-10 theme)] + :style style/gradient-start}]) + (when (and overflow? disabled?) + [linear-gradient/linear-gradient + {:start {:x 0 :y 0} + :end {:x 1 :y 0} + :colors [(colors/theme-colors colors/white-opa-10 colors/neutral-100-opa-10 theme) + (colors/theme-colors colors/white colors/neutral-100 theme)] + :style style/gradient-end}]) + [rn/text-input + (cond-> {:ref set-input-ref + :style (style/input disabled? error? theme) + :placeholder-text-color (colors/theme-colors colors/neutral-40 + colors/neutral-50 + theme) + :keyboard-type :numeric + :editable (not input-disabled?) + :auto-focus auto-focus? + :on-focus on-input-focus + :on-change-text on-change-text + :on-layout on-layout-text-input + :on-selection-change on-selection-change + :show-soft-input-on-focus show-keyboard? + :default-value default-value + :placeholder "0"} + controlled-input? (assoc :value value))]] [text/text - {:size :paragraph-2 - :weight :semi-bold - :style (style/token-symbol theme)} + {:size :paragraph-2 + :weight :semi-bold + :style (style/token-symbol theme) + :on-layout on-layout-label} token]] (when (and pay? enable-swap?) [buttons/button diff --git a/src/status_im/contexts/wallet/swap/select_asset_to_pay/view.cljs b/src/status_im/contexts/wallet/swap/select_asset_to_pay/view.cljs index 16c3c8b3bd4..8b913f6fb41 100644 --- a/src/status_im/contexts/wallet/swap/select_asset_to_pay/view.cljs +++ b/src/status_im/contexts/wallet/swap/select_asset_to_pay/view.cljs @@ -21,9 +21,11 @@ (defn- assets-view [search-text on-change-text] (let [on-token-press (fn [token] - (rf/dispatch [:wallet.swap/start - {:asset-to-pay token - :open-new-screen? false}]))] + (let [asset-to-receive (rf/sub [:wallet/token-by-symbol "SNT"])] + (rf/dispatch [:wallet.swap/start + {:asset-to-pay token + :asset-to-receive asset-to-receive + :open-new-screen? false}])))] [:<> [search-input search-text on-change-text] [asset-list/view diff --git a/src/status_im/contexts/wallet/swap/setup_swap/view.cljs b/src/status_im/contexts/wallet/swap/setup_swap/view.cljs index bbd9c3c82b5..7f24ae43cb3 100644 --- a/src/status_im/contexts/wallet/swap/setup_swap/view.cljs +++ b/src/status_im/contexts/wallet/swap/setup_swap/view.cljs @@ -100,10 +100,12 @@ pay-input-amount (controlled-input/input-value input-state) pay-token-symbol (:symbol asset-to-pay) pay-token-decimals (:decimals asset-to-pay) - pay-token-balance-selected-chain (get-in asset-to-pay - [:balances-per-chain - (:chain-id network) :balance] - 0) + pay-token-balance-selected-chain (number/convert-to-whole-number + (get-in asset-to-pay + [:balances-per-chain + (:chain-id network) :raw-balance] + 0) + pay-token-decimals) pay-token-fiat-value (str (utils/calculate-token-fiat-value {:currency currency @@ -111,10 +113,15 @@ :token asset-to-pay})) available-crypto-limit (money/bignumber pay-token-balance-selected-chain) + display-decimals (min pay-token-decimals + constants/min-token-decimals-to-display) available-crypto-limit-display (number/remove-trailing-zeroes - (.toFixed available-crypto-limit - (min pay-token-decimals - constants/min-token-decimals-to-display))) + (.toFixed available-crypto-limit display-decimals)) + available-crypto-limit-display (if (and (= available-crypto-limit-display "0") + (money/greater-than available-crypto-limit + (money/bignumber 0))) + (number/small-number-threshold display-decimals) + available-crypto-limit-display) approval-amount-required-num (when approval-amount-required (str (number/hex->whole approval-amount-required pay-token-decimals))) @@ -122,8 +129,7 @@ (money/greater-than (money/bignumber pay-input-amount) available-crypto-limit)) - (money/equal-to (money/bignumber - available-crypto-limit-display) + (money/equal-to available-crypto-limit (money/bignumber 0))) valid-pay-input? (and (not (string/blank? @@ -156,7 +162,7 @@ :else :disabled) :currency-symbol currency-symbol :on-token-press on-token-press - :on-max-press #(on-max-press (str available-crypto-limit)) + :on-max-press #(on-max-press (str pay-token-balance-selected-chain)) :on-input-focus on-input-focus :value pay-input-amount :fiat-value pay-token-fiat-value @@ -310,10 +316,12 @@ network (rf/sub [:wallet/swap-network]) pay-input-amount (controlled-input/input-value pay-input-state) pay-token-decimals (:decimals asset-to-pay) - pay-token-balance-selected-chain (get-in asset-to-pay - [:balances-per-chain - (:chain-id network) :balance] - 0) + pay-token-balance-selected-chain (number/convert-to-whole-number + (get-in asset-to-pay + [:balances-per-chain + (:chain-id network) :raw-balance] + 0) + pay-token-decimals) pay-input-error? (and (not (string/blank? pay-input-amount)) (money/greater-than (money/bignumber pay-input-amount) @@ -415,13 +423,17 @@ (when (and swap-amount refetch-interval) (js/clearTimeout @refetch-interval) (reset! refetch-interval nil)) - (if (and swap-amount (not= swap-amount pay-input-amount)) - (set-pay-input-state - (fn [input-state] - (controlled-input/set-input-value - input-state - swap-amount))) - (refetch-swap-proposal))))) + (cond (and swap-amount (not= swap-amount pay-input-amount)) + (set-pay-input-state + (fn [input-state] + (controlled-input/set-input-value + input-state + swap-amount))) + (and pay-input-amount + (not (number/valid-decimal-count? pay-input-amount (:decimals asset-to-pay)))) + (set-pay-input-state controlled-input/delete-all) + :else + (refetch-swap-proposal))))) [asset-to-pay]) (rn/use-effect refetch-swap-proposal diff --git a/src/utils/number.cljs b/src/utils/number.cljs index 8e2c7496f26..2f556907295 100644 --- a/src/utils/number.cljs +++ b/src/utils/number.cljs @@ -2,7 +2,8 @@ (:require [clojure.string :as string] [native-module.core :as native-module] [utils.hex :as utils.hex] - [utils.money :as utils.money])) + [utils.money :as utils.money] + [utils.money :as money])) (defn naive-round "Quickly and naively round number `n` up to `decimal-places`. @@ -18,17 +19,6 @@ (/ (Math/round (* n scale)) scale))) -(defn convert-to-whole-number - "Converts a fractional `amount` to its corresponding whole number representation - by dividing it by 10 raised to the power of `decimals`. This is often used in financial - calculations where amounts are stored in their smallest units (e.g., cents) and need - to be converted to their whole number equivalents (e.g., dollars). - - Example usage: - (convert-to-whole-number 12345 2) ; => 123.45" - [amount decimals] - (/ amount (Math/pow 10 decimals))) - (defn parse-int "Parses `n` as an integer. Defaults to zero or `default` instead of NaN." ([n] @@ -65,15 +55,44 @@ "") "")))) +(defn convert-to-whole-number + "Converts a fractional `amount` to its corresponding whole number representation + by dividing it by 10 raised to the power of `decimals`. This is often used in financial + calculations where amounts are stored in their smallest units (e.g., cents) and need + to be converted to their whole number equivalents (e.g., dollars). + + Example usage: + (convert-to-whole-number 12345 2) ; => 123.45" + [amount decimals] + (-> amount + (/ (Math/pow 10 decimals)) + (.toFixed decimals) + remove-trailing-zeroes)) + (defn hex->whole [num decimals] (-> num utils.hex/normalize-hex native-module/hex-to-number - (convert-to-whole-number decimals))) + (convert-to-whole-number decimals) + money/bignumber)) (defn to-fixed [num decimals] (-> num (utils.money/to-fixed decimals) remove-trailing-zeroes)) + +(defn small-number-threshold + "Receives a decimal count and returns a string like '<0.001' if the decimal count is 3, + '<0.000001' if the decimal count is 6, etc." + [decimal-count] + (if (> decimal-count 0) + (str "<0." (apply str (repeat (dec decimal-count) "0")) "1") + "0")) + +(defn valid-decimal-count? + "Returns false if the number has more decimals than the decimal count, otherwise true." + [num decimal-count] + (let [decimal-part (second (string/split (str num) #"\."))] + (or (nil? decimal-part) (<= (count decimal-part) decimal-count)))) diff --git a/src/utils/number_test.cljs b/src/utils/number_test.cljs index 57c3e3c7839..9d8f426d6a6 100644 --- a/src/utils/number_test.cljs +++ b/src/utils/number_test.cljs @@ -5,23 +5,23 @@ (deftest convert-to-whole-number-test (testing "correctly converts fractional amounts to whole numbers" - (is (= 123.45 (utils.number/convert-to-whole-number 12345 2))) - (is (= 1.2345 (utils.number/convert-to-whole-number 12345 4))) - (is (= 12345.0 (utils.number/convert-to-whole-number 1234500 2))) - (is (= 0.123 (utils.number/convert-to-whole-number 123 3))) - (is (= 1000.0 (utils.number/convert-to-whole-number 1000000 3)))) + (is (= "123.45" (utils.number/convert-to-whole-number 12345 2))) + (is (= "1.2345" (utils.number/convert-to-whole-number 12345 4))) + (is (= "12345" (utils.number/convert-to-whole-number 1234500 2))) + (is (= "0.123" (utils.number/convert-to-whole-number 123 3))) + (is (= "1000" (utils.number/convert-to-whole-number 1000000 3)))) (testing "handles zero decimals" - (is (= 12345 (utils.number/convert-to-whole-number 12345 0)))) + (is (= "12345" (utils.number/convert-to-whole-number 12345 0)))) (testing "handles negative amounts" - (is (= -123.45 (utils.number/convert-to-whole-number -12345 2))) - (is (= -1.2345 (utils.number/convert-to-whole-number -12345 4))) - (is (= -0.123 (utils.number/convert-to-whole-number -123 3)))) + (is (= "-123.45" (utils.number/convert-to-whole-number -12345 2))) + (is (= "-1.2345" (utils.number/convert-to-whole-number -12345 4))) + (is (= "-0.123" (utils.number/convert-to-whole-number -123 3)))) (testing "handles zero amount" - (is (= 0 (utils.number/convert-to-whole-number 0 2))) - (is (= 0 (utils.number/convert-to-whole-number 0 0))))) + (is (= "0" (utils.number/convert-to-whole-number 0 2))) + (is (= "0" (utils.number/convert-to-whole-number 0 0))))) (deftest parse-int-test (testing "defaults to zero" @@ -50,3 +50,36 @@ (is (= 6 (utils.number/parse-float "6"))) (is (= 6.99 (utils.number/parse-float "6.99" 0))) (is (= -6.9 (utils.number/parse-float "-6.9" 0))))) + +(deftest small-number-threshold-test + (testing "correctly generates threshold strings based on decimal count" + (is (= "<0.1" (utils.number/small-number-threshold 1))) + (is (= "<0.01" (utils.number/small-number-threshold 2))) + (is (= "<0.001" (utils.number/small-number-threshold 3))) + (is (= "<0.000001" (utils.number/small-number-threshold 6))) + (is (= "<0.0000000001" (utils.number/small-number-threshold 10))) + (is (= "<0.000000000000000001" (utils.number/small-number-threshold 18))) + (is (= "<0.000000000000000000001" (utils.number/small-number-threshold 21)))) + + (testing "handles edge cases for decimal count" + (is (= "0" (utils.number/small-number-threshold 0))) + (is (= "0" (utils.number/small-number-threshold -1))))) + +(deftest valid-decimal-count?-test + (testing "valid decimal count check for numbers with varying decimals" + (is (true? (utils.number/valid-decimal-count? 123 2))) + (is (true? (utils.number/valid-decimal-count? 123 0))) + + (is (true? (utils.number/valid-decimal-count? 123.45 2))) + (is (true? (utils.number/valid-decimal-count? 123.4 2))) + (is (true? (utils.number/valid-decimal-count? 123.456 3))) + + (is (false? (utils.number/valid-decimal-count? 123.456 2))) + (is (false? (utils.number/valid-decimal-count? 123.456789 4))) + + (is (true? (utils.number/valid-decimal-count? 123.0 1))) + (is (true? (utils.number/valid-decimal-count? -123.45 2))) + (is (false? (utils.number/valid-decimal-count? -123.4567 3))) + + (is (true? (utils.number/valid-decimal-count? 1234567890.12 2))) + (is (false? (utils.number/valid-decimal-count? 1234567890.12345 3)))))