diff --git a/resources/images/icons2/20x20/percentage@2x.png b/resources/images/icons2/20x20/percentage@2x.png new file mode 100644 index 0000000000..020f6d6566 Binary files /dev/null and b/resources/images/icons2/20x20/percentage@2x.png differ diff --git a/resources/images/icons2/20x20/percentage@3x.png b/resources/images/icons2/20x20/percentage@3x.png new file mode 100644 index 0000000000..9511f0c810 Binary files /dev/null and b/resources/images/icons2/20x20/percentage@3x.png differ diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index b508a1e36a..530d2824d1 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -555,3 +555,8 @@ (def ^:const default-telemetry-server-url "https://telemetry.status.im") (def ^:const contact-item-height 56) + +(def ^:const slippages [0.1 0.5 1]) +(def ^:const default-slippage 0.5) +(def ^:const max-recommended-slippage 5) +(def ^:const max-slippage-decimal-places 2) diff --git a/src/status_im/contexts/wallet/sheets/slippage_settings/style.cljs b/src/status_im/contexts/wallet/sheets/slippage_settings/style.cljs new file mode 100644 index 0000000000..b29ac5ca6c --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/slippage_settings/style.cljs @@ -0,0 +1,13 @@ +(ns status-im.contexts.wallet.sheets.slippage-settings.style) + +(def slippages + {:padding-horizontal 8}) + +(def info-message + {:margin-vertical 8 + :margin-horizontal 20}) + +(defn percentage-icon + [variant-colors] + {:size 20 + :color (:icon variant-colors)}) diff --git a/src/status_im/contexts/wallet/sheets/slippage_settings/view.cljs b/src/status_im/contexts/wallet/sheets/slippage_settings/view.cljs new file mode 100644 index 0000000000..c5e9c3fdc5 --- /dev/null +++ b/src/status_im/contexts/wallet/sheets/slippage_settings/view.cljs @@ -0,0 +1,118 @@ +(ns status-im.contexts.wallet.sheets.slippage-settings.view + (:require [clojure.string :as string] + [quo.core :as quo] + [react-native.core :as rn] + [status-im.constants :as constants] + [status-im.contexts.wallet.sheets.slippage-settings.style :as style] + [utils.i18n :as i18n] + [utils.number] + [utils.re-frame :as rf])) + +(defn- validate-slippage + [slippage] + (let [slippage-value (utils.number/parse-float slippage)] + (cond + (<= slippage-value 0) + {:message (i18n/label :t/slippage-should-be-more-than-0) + :type :error} + (> (count (second (string/split slippage "."))) + constants/max-slippage-decimal-places) + {:message (i18n/label :t/max-2-decimals) + :type :error} + (> slippage-value constants/max-recommended-slippage) + {:message (i18n/label :t/slippage-may-be-higher-than-necessary) + :type :warning}))) + +(defn- on-cancel + [] + (rf/dispatch [:hide-bottom-sheet])) + +(defn- update-string-on-keypress + [k s] + (if (= :i/backspace k) + (subs s 0 (dec (count s))) + (str s k))) + +(defn view + [] + (let [current-slippage (rf/sub [:wallet/swap-max-slippage]) + account-color (rf/sub [:wallet/current-viewing-account-color]) + [max-slippage set-max-slippage] (rn/use-state (str current-slippage)) + [error set-error] (rn/use-state nil) + [custom? set-custom?] (rn/use-state (not-any? #{current-slippage} + constants/slippages)) + handle-slippage-change (rn/use-callback + (fn [value] + (let [new-slippage (update-string-on-keypress value + max-slippage)] + (set-max-slippage new-slippage) + (set-error (validate-slippage new-slippage)))) + [max-slippage set-max-slippage set-error]) + on-select-slippage (rn/use-callback (fn [slippage] + (set-max-slippage (str slippage)) + (set-custom? (not slippage))) + [set-max-slippage set-custom?]) + save-disabled? (rn/use-memo (fn [] + (or (= max-slippage (str current-slippage)) + (= (:type error) :error) + (and custom? + (empty? max-slippage)))) + [max-slippage current-slippage error custom?]) + on-save (rn/use-callback (fn [] + (rf/dispatch [:wallet.swap/set-max-slippage + max-slippage]) + (rf/dispatch [:hide-bottom-sheet])) + [max-slippage])] + [:<> + [quo/drawer-top + {:title (i18n/label :t/slippage-settings) + :description (i18n/label :t/slippage-settings-description)}] + [rn/view {:style style/slippages} + (map (fn [slippage] + ^{:key slippage} + [quo/drawer-action + (cond-> {:title (str slippage "%") + :on-press #(on-select-slippage slippage)} + (= (str slippage) max-slippage) (assoc :state :selected))]) + constants/slippages) + [quo/drawer-action + (cond-> {:title (i18n/label :t/custom) + :action :input + :on-press #(on-select-slippage nil) + :input-props {:auto-focus true + :customization-color account-color + :placeholder (i18n/label :t/type-slippage) + :right-icon {:icon-name :i/percentage + :on-press identity + :style-fn style/percentage-icon} + :value max-slippage}} + custom? (assoc :state :selected))]] + (when (and custom? error) + [quo/info-message + {:status (:type error) + :size :default + :container-style style/info-message + :icon :i/alert} + (:message error)]) + [quo/bottom-actions + {:actions :two-actions + :button-one-label (i18n/label :t/save-changes) + :button-one-props {:disabled? save-disabled? + :customization-color account-color + :on-press on-save} + :button-two-label (i18n/label :t/cancel) + :button-two-props {:on-press on-cancel + :customization-color account-color + :type :grey} + :description :top + :context-tag-props {:size 24 + :type :token + :token "USDT" + :amount "99.97"} ;; will be replaced with real data later + :description-top-text (i18n/label :t/receive-at-least)}] + (when custom? + [quo/numbered-keyboard + {:left-action :dot + :delete-key? true + :on-press handle-slippage-change + :on-delete handle-slippage-change}])])) diff --git a/src/status_im/contexts/wallet/swap/events.cljs b/src/status_im/contexts/wallet/swap/events.cljs index c495b90c81..ff16116a4f 100644 --- a/src/status_im/contexts/wallet/swap/events.cljs +++ b/src/status_im/contexts/wallet/swap/events.cljs @@ -1,6 +1,8 @@ (ns status-im.contexts.wallet.swap.events (:require [re-frame.core :as rf] - [status-im.contexts.wallet.sheets.network-selection.view :as network-selection])) + [status-im.constants :as constants] + [status-im.contexts.wallet.sheets.network-selection.view :as network-selection] + [utils.number])) (rf/reg-event-fx :wallet.swap/start (fn [{:keys [_db]}] @@ -11,13 +13,10 @@ {:db (-> db (assoc-in [:wallet :ui :swap :asset-to-pay] token) (assoc-in [:wallet :ui :swap :network] network)) - :fx [(if network - [:dispatch - [:toasts/upsert - {:id :swap-error - :type :negative - :text "Not implemented yet"}]] - [:dispatch + :fx (if network + [[:dispatch [:navigate-to :screen/wallet.swap-propasal]] + [:dispatch [:wallet.swap/set-default-slippage]]] + [[:dispatch [:show-bottom-sheet {:content (fn [] [network-selection/view @@ -29,8 +28,17 @@ {:token token :network network :stack-id - :screen/wallet.swap-select-asset-to-pay}]))}])}]])]})) + :screen/wallet.swap-select-asset-to-pay}]))}])}]]])})) (rf/reg-event-fx :wallet.swap/clean-asset-to-pay (fn [{:keys [db]}] {:db (update-in db [:wallet :ui :swap] dissoc :asset-to-pay)})) + +(rf/reg-event-fx :wallet.swap/set-default-slippage + (fn [{:keys [db]}] + {:db + (assoc-in db [:wallet :ui :swap :max-slippage] constants/default-slippage)})) + +(rf/reg-event-fx :wallet.swap/set-max-slippage + (fn [{:keys [db]} [max-slippage]] + {:db (assoc-in db [:wallet :ui :swap :max-slippage] (utils.number/parse-float max-slippage))})) diff --git a/src/status_im/contexts/wallet/swap/swap_proposal/style.cljs b/src/status_im/contexts/wallet/swap/swap_proposal/style.cljs new file mode 100644 index 0000000000..47e48e5cd8 --- /dev/null +++ b/src/status_im/contexts/wallet/swap/swap_proposal/style.cljs @@ -0,0 +1,4 @@ +(ns status-im.contexts.wallet.swap.swap-proposal.style) + +(def container + {:flex 1}) diff --git a/src/status_im/contexts/wallet/swap/swap_proposal/view.cljs b/src/status_im/contexts/wallet/swap/swap_proposal/view.cljs new file mode 100644 index 0000000000..e5ffc0940e --- /dev/null +++ b/src/status_im/contexts/wallet/swap/swap_proposal/view.cljs @@ -0,0 +1,15 @@ +(ns status-im.contexts.wallet.swap.swap-proposal.view + (:require [quo.core :as quo] + [react-native.core :as rn] + [status-im.contexts.wallet.sheets.slippage-settings.view :as slippage-settings] + [status-im.contexts.wallet.swap.swap-proposal.style :as style] + [utils.re-frame :as rf])) + +(defn view + [] + (let [max-slippage (rf/sub [:wallet/swap-max-slippage])] + [rn/view {:style style/container} + [quo/button + {:on-press #(rf/dispatch [:show-bottom-sheet + {:content slippage-settings/view}])} + (str "Edit Slippage: " max-slippage "%")]])) diff --git a/src/status_im/navigation/screens.cljs b/src/status_im/navigation/screens.cljs index c31cd7cbee..f6c0dcadf1 100644 --- a/src/status_im/navigation/screens.cljs +++ b/src/status_im/navigation/screens.cljs @@ -116,6 +116,7 @@ [status-im.contexts.wallet.send.transaction-confirmation.view :as wallet-transaction-confirmation] [status-im.contexts.wallet.send.transaction-progress.view :as wallet-transaction-progress] [status-im.contexts.wallet.swap.select-asset-to-pay.view :as wallet-swap-select-asset-to-pay] + [status-im.contexts.wallet.swap.swap-proposal.view :as wallet-swap-propasal] [status-im.contexts.wallet.wallet-connect.modals.send-transaction.view :as wallet-connect-send-transaction] [status-im.contexts.wallet.wallet-connect.modals.sign-message.view :as wallet-connect-sign-message] @@ -519,6 +520,10 @@ :insets {:top? true}} :component wallet-swap-select-asset-to-pay/view} + {:name :screen/wallet.swap-propasal + :options {:insets {:top? true}} + :component wallet-swap-propasal/view} + {:name :scan-profile-qr-code :options (merge options/dark-screen diff --git a/src/status_im/subs/wallet/swap.cljs b/src/status_im/subs/wallet/swap.cljs index 93523c33d1..8e42bb96a1 100644 --- a/src/status_im/subs/wallet/swap.cljs +++ b/src/status_im/subs/wallet/swap.cljs @@ -52,3 +52,7 @@ fiat-value)] {:crypto (str crypto-formatted " " token-symbol) :fiat fiat-formatted}))) +(rf/reg-sub + :wallet/swap-max-slippage + :<- [:wallet/swap] + :-> :max-slippage) diff --git a/src/utils/number.cljs b/src/utils/number.cljs index 392498ca8e..6e5d758530 100644 --- a/src/utils/number.cljs +++ b/src/utils/number.cljs @@ -25,6 +25,16 @@ maybe-int default)))) +(defn parse-float + "Parses `n` as a float. Defaults to zero or `default` instead of NaN." + ([n] + (parse-float n 0)) + ([n default] + (let [maybe-float (js/parseFloat n 10)] + (if (js/Number.isNaN maybe-float) + default + maybe-float)))) + (defn value-in-range "Returns `num` if is in the range [`lower-bound` `upper-bound`] if `num` exceeds a given bound, then returns the bound exceeded." diff --git a/src/utils/number_test.cljs b/src/utils/number_test.cljs index 5cb964ee51..ebfd9a2abe 100644 --- a/src/utils/number_test.cljs +++ b/src/utils/number_test.cljs @@ -16,3 +16,17 @@ (is (= 6 (utils.number/parse-int "6" 0))) (is (= 6 (utils.number/parse-int "6.99" 0))) (is (= -6 (utils.number/parse-int "-6" 0))))) + +(deftest parse-float-test + (testing "defaults to zero" + (is (= 0 (utils.number/parse-float nil)))) + + (testing "accepts any other default value" + (is (= 3 (utils.number/parse-float "" 3))) + (is (= :invalid-float (utils.number/parse-float "" :invalid-float)))) + + (testing "valid numbers" + (is (= -6 (utils.number/parse-float "-6a"))) + (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))))) diff --git a/translations/en.json b/translations/en.json index 186b35ba9e..cc12be4486 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1296,6 +1296,7 @@ "reveal-qr-code": "Reveal QR code", "revoke-access": "Revoke access", "save": "Save", + "save-changes": "Save changes", "save-address": "Save address", "save-bio": "Save bio", "save-colour": "Save colour", @@ -1998,6 +1999,13 @@ "status-inactive-subtitle": "Hides your online status", "two-minutes": "two minutes", "swap": "Swap", + "slippage-settings": "Slippage settings", + "slippage-settings-description": "Your transaction will revert if the price changes more than the slippage percentage", + "slippage-may-be-higher-than-necessary": "Slippage may be higher than necessary", + "slippage-should-be-more-than-0": "Slippage should be more than 0", + "max-2-decimals": "Max. 2 decimals", + "type-slippage": "Type slippage", + "receive-at-least": "Receive at least", "select-token-to-swap": "Select token to Swap", "select-token-to-receive": "Select token to receive", "slide-to-request-to-join": "Slide to request to join",