diff --git a/resources/icons/sliders.svg b/resources/icons/main/sliders.svg similarity index 100% rename from resources/icons/sliders.svg rename to resources/icons/main/sliders.svg diff --git a/resources/icons/time.svg b/resources/icons/main/time.svg similarity index 100% rename from resources/icons/time.svg rename to resources/icons/main/time.svg diff --git a/resources/icons/paste.svg b/resources/icons/paste.svg index ea53e87edf9..a5352e75f8f 100644 --- a/resources/icons/paste.svg +++ b/resources/icons/paste.svg @@ -1,3 +1,4 @@ - - + + + diff --git a/src/status_im/accounts/core.cljs b/src/status_im/accounts/core.cljs index 4694c5ee5ca..ecf5f020b5c 100644 --- a/src/status_im/accounts/core.cljs +++ b/src/status_im/accounts/core.cljs @@ -23,21 +23,19 @@ (defn- chat-send? [transaction] (and (seq transaction) - (not (:in-progress? transaction)) (:from-chat? transaction))) -(fx/defn continue-after-wallet-onboarding [{:keys [db] :as cofx} modal?] - (let [transaction (get-in db [:wallet :send-transaction])] - (if modal? - {:dispatch [:navigate-to-clean :wallet-send-transaction-modal]} - (if-not (chat-send? transaction) - (navigation/navigate-to-clean cofx :wallet nil) - (navigation/navigate-to-cofx cofx :wallet-send-transaction-modal nil))))) +(fx/defn continue-after-wallet-onboarding [{:keys [db] :as cofx} modal? {:keys [flow] :as screen-params}] + (if modal? + {:dispatch [:navigate-to :wallet-send-transaction-modal screen-params]} + (if (= :chat flow) + (navigation/navigate-to-cofx cofx :wallet-txn-overview screen-params) + (navigation/navigate-to-clean cofx :wallet nil)))) (fx/defn confirm-wallet-set-up - [{:keys [db] :as cofx} modal?] + [{:keys [db] :as cofx} modal? screen-params] (fx/merge cofx - (continue-after-wallet-onboarding modal?) + (continue-after-wallet-onboarding modal? screen-params) (wallet.settings.models/wallet-autoconfig-tokens) (accounts.update/account-update {:wallet-set-up-passed? true} {}))) diff --git a/src/status_im/chat/commands/impl/transactions.cljs b/src/status_im/chat/commands/impl/transactions.cljs index f08c6a6fe79..13eb8fbf1e9 100644 --- a/src/status_im/chat/commands/impl/transactions.cljs +++ b/src/status_im/chat/commands/impl/transactions.cljs @@ -236,14 +236,6 @@ (when network-mismatch? [react/text send-network])]]))) -;; TODO(goranjovic) - update to include tokens in https://github.com/status-im/status-react/issues/3233 -(defn- transaction-details [contact symbol] - (-> contact - (select-keys [:name :address :public-key]) - (assoc :symbol symbol - :gas (ethereum/estimate-gas symbol) - :from-chat? true))) - (defn- inject-network-info [parameters {:keys [db]}] (assoc parameters :network (:chain db))) @@ -307,26 +299,14 @@ {:keys [symbol decimals]} (tokens/asset-for all-tokens chain symbol-param) {:keys [value error]} (wallet.db/parse-amount amount decimals) next-view-id (if (:wallet-set-up-passed? sender-account) - :wallet-send-transaction-modal + :wallet-txn-overview :wallet-onboarding-setup)] - (fx/merge cofx - {:db (-> db - (update-in [:wallet :send-transaction] - assoc - :amount (money/formatted->internal value symbol decimals) - :amount-text amount - :amount-error error) - #_(choose-recipient.events/fill-request-details - (transaction-details recipient-contact symbol)) - (update-in [:wallet :send-transaction] - dissoc :id :password :wrong-password?)) - ;; TODO(janherich) - refactor wallet send events, updating gas price - ;; is generic thing which shouldn't be defined in wallet.send, then - ;; we can include the utility helper without running into circ-dep problem - :update-gas-price {:web3 (:web3 db) - :success-event :wallet/update-gas-price-success - :edit? false}} - (navigation/navigate-to-cofx next-view-id {})))) + (let [transaction {:amount (money/formatted->internal value symbol decimals) + :symbol symbol + :to (:address recipient-contact)}] + (navigation/navigate-to-cofx cofx next-view-id {:transaction transaction + :contact recipient-contact + :flow :chat})))) protocol/EnhancedParameters (enhance-send-parameters [_ parameters cofx] (-> parameters diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index 212f1a8a8e1..02a8e31e59b 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -188,8 +188,8 @@ (handlers/register-handler-fx :accounts.ui/wallet-set-up-confirmed - (fn [cofx [_ modal?]] - (accounts/confirm-wallet-set-up cofx modal?))) + (fn [cofx [_ modal? screen-params]] + (accounts/confirm-wallet-set-up cofx modal? screen-params))) ;; accounts create module diff --git a/src/status_im/extensions/ethereum.cljs b/src/status_im/extensions/ethereum.cljs index b4fa11df387..0b8c6ae11a4 100644 --- a/src/status_im/extensions/ethereum.cljs +++ b/src/status_im/extensions/ethereum.cljs @@ -70,8 +70,8 @@ (defn- execute-send-transaction [db {:keys [method params on-success on-failure] :as arguments}] (let [tx-object (assoc (select-keys arguments [:to :gas :gas-price :value :nonce]) :data (when (and method params) (abi-spec/encode method params))) - transaction (prepare-extension-transaction tx-object (:contacts/contacts db) on-success on-failure)] - (models.wallet/open-modal-wallet-for-transaction db transaction tx-object))) + transaction (prepare-extension-transaction tx-object (:contacts/contacts db) on-success on-failure)] + (models.wallet/open-modal-wallet-for-transaction db transaction))) (handlers/register-handler-fx :extensions/ethereum-send-transaction diff --git a/src/status_im/models/wallet.cljs b/src/status_im/models/wallet.cljs index 5bb99308787..80cbf881e03 100644 --- a/src/status_im/models/wallet.cljs +++ b/src/status_im/models/wallet.cljs @@ -91,12 +91,12 @@ (assoc :nonce nonce)))) ;; SEND TRANSACTION -> RPC TRANSACTION -(defn prepare-send-transaction [from {:keys [amount to gas gas-price data nonce]}] +(defn prepare-send-transaction [from {:keys [amount to gas gas-price optimal-gas optimal-gas-price data nonce]}] (cond-> {:from (ethereum/normalized-address from) :to (ethereum/normalized-address to) :value (ethereum/int->hex amount) - :gas (ethereum/int->hex gas) - :gasPrice (ethereum/int->hex gas-price)} + :gas (ethereum/int->hex (or gas optimal-gas)) + :gasPrice (ethereum/int->hex (or gas-price optimal-gas-price))} data (assoc :data data) nonce @@ -145,9 +145,8 @@ (when on-error {:dispatch (conj on-error "transaction was cancelled by user")})))) -(defn prepare-unconfirmed-transaction [db now hash] - (let [transaction (get-in db [:wallet :send-transaction]) - all-tokens (:wallet/all-tokens db)] +(defn prepare-unconfirmed-transaction [db now transaction hash] + (let [all-tokens (:wallet/all-tokens db)] (let [chain (:chain db) token (tokens/symbol->token all-tokens (keyword chain) (:symbol transaction))] (-> transaction @@ -237,18 +236,14 @@ (assoc-in [:wallet :balance-loading?] true) (assoc :prices-loading? true))}))) -(defn open-modal-wallet-for-transaction [db transaction tx-object] - (let [{:keys [gas gas-price]} transaction +(defn open-modal-wallet-for-transaction [db transaction] + (let [{:keys [gas]} transaction {:keys [wallet-set-up-passed?]} (:account/account db)] - {:db (-> db - (assoc-in [:wallet :send-transaction] transaction) - (assoc-in [:wallet :send-transaction :original-gas] gas)) + {:db db :dispatch-n [[:update-wallet] - (when-not gas - [:wallet/update-estimated-gas tx-object]) - (when-not gas-price - [:wallet/update-gas-price]) [:navigate-to (if wallet-set-up-passed? - :wallet-send-modal-stack - :wallet-send-modal-stack-with-onboarding)]]})) + :wallet-send-transaction-modal + :wallet-onboarding-setup-modal) + {:transaction (assoc transaction :original-gas gas) + :flow :dapp}]]})) diff --git a/src/status_im/ui/components/colors.cljs b/src/status_im/ui/components/colors.cljs index 572a21113cc..3e7dd83aa21 100644 --- a/src/status_im/ui/components/colors.cljs +++ b/src/status_im/ui/components/colors.cljs @@ -33,8 +33,10 @@ ;; ACCENT BLUE (def blue "#4360df") ;; Accent blue, used as main wallet color, and ios home add button + ;; LIGHT BLUE (def blue-light "#ECEFFC") ;; Light Blue +(def blue-shadow "#8FA2EA") (def gray-background blue-light) ;; TODO (andrey) should be refactored later by Dmitry ;; RED diff --git a/src/status_im/ui/components/icons/vector_icons.cljs b/src/status_im/ui/components/icons/vector_icons.cljs index e7dad7fdbf2..6e351104578 100644 --- a/src/status_im/ui/components/icons/vector_icons.cljs +++ b/src/status_im/ui/components/icons/vector_icons.cljs @@ -193,8 +193,10 @@ :main-icons/send (components.svg/slurp-svg "./resources/icons/main/send.svg") :main-icons/settings (components.svg/slurp-svg "./resources/icons/main/settings.svg") :main-icons/share (components.svg/slurp-svg "./resources/icons/main/share.svg") + :main-icons/sliders (components.svg/slurp-svg "./resources/icons/main/sliders.svg") :main-icons/stickers (components.svg/slurp-svg "./resources/icons/main/stickers.svg") :main-icons/text (components.svg/slurp-svg "./resources/icons/main/text.svg") + :main-icons/time (components.svg/slurp-svg "./resources/icons/main/time.svg") :main-icons/tribute-to-talk (components.svg/slurp-svg "./resources/icons/main/tribute_to_talk.svg") :main-icons/two-arrows (components.svg/slurp-svg "./resources/icons/main/two_arrows.svg") :main-icons/user-profile (components.svg/slurp-svg "./resources/icons/main/user_profile.svg") diff --git a/src/status_im/ui/components/tooltip/styles.cljs b/src/status_im/ui/components/tooltip/styles.cljs index dd4a0e2b6fa..2b6429fae51 100644 --- a/src/status_im/ui/components/tooltip/styles.cljs +++ b/src/status_im/ui/components/tooltip/styles.cljs @@ -41,9 +41,10 @@ :background-color colors/gray :border-radius 8}) -(defn tooltip-text [font-size] - {:color colors/red - :font-size font-size}) +(defn tooltip-text [font-size text-color] + {:color (or text-color colors/red) + :font-size font-size + :text-align :center}) (def bottom-tooltip-text {:color colors/white diff --git a/src/status_im/ui/components/tooltip/views.cljs b/src/status_im/ui/components/tooltip/views.cljs index 7aa39a90747..eef37104483 100644 --- a/src/status_im/ui/components/tooltip/views.cljs +++ b/src/status_im/ui/components/tooltip/views.cljs @@ -8,14 +8,14 @@ [status-im.ui.components.colors :as colors] [reagent.core :as reagent])) -(views/defview tooltip [label & [{:keys [bottom-value color font-size] :or {bottom-value -30 color :white font-size 15}}]] +(views/defview tooltip [label & [{:keys [bottom-value color text-color font-size] :or {bottom-value -30 color :white font-size 15}}]] (views/letsubs [bottom-anim-value (animation/create-value bottom-value) opacity-value (animation/create-value 0)] {:component-did-mount (animations/animate-tooltip bottom-value bottom-anim-value opacity-value 10)} [react/view styles/tooltip-container [react/animated-view {:style (styles/tooltip-animated bottom-anim-value opacity-value)} [react/view (styles/tooltip-text-container color) - [react/text {:style (styles/tooltip-text font-size)} label]] + [react/text {:style (styles/tooltip-text font-size text-color)} label]] [vector-icons/icon :icons/tooltip-triangle {:color color :style styles/tooltip-triangle}]]])) (views/defview bottom-tooltip-info [label on-close] diff --git a/src/status_im/ui/screens/routing/screens.cljs b/src/status_im/ui/screens/routing/screens.cljs index 16334f1f60b..c8b94d8a318 100644 --- a/src/status_im/ui/screens/routing/screens.cljs +++ b/src/status_im/ui/screens/routing/screens.cljs @@ -21,8 +21,9 @@ [status-im.ui.screens.wallet.send.views :as send] [status-im.ui.screens.wallet.request.views :as request] [status-im.ui.screens.wallet.components.views :as wallet.components] + [status-im.ui.screens.wallet.send.views.amount :as wallet.send.amount] [status-im.ui.screens.wallet.onboarding.views :as wallet.onboarding] - [status-im.ui.screens.wallet.transaction-fee.views :as wallet.transaction-fee] + [status-im.ui.screens.wallet.sign-message.views :as wallet.sign-message] [status-im.ui.screens.wallet.settings.views :as wallet-settings] [status-im.ui.screens.wallet.send.views :as send.views] [status-im.ui.screens.wallet.transactions.views :as wallet-transactions] @@ -93,20 +94,19 @@ :show-extension-modal [:modal extensions.add/show-extension-modal] :wallet-send-transaction-modal [:modal send/send-transaction-modal] :wallet-transaction-sent-modal [:modal transaction-sent/transaction-sent-modal] - :wallet-transaction-fee [:modal wallet.transaction-fee/transaction-fee] :wallet-onboarding-setup-modal [:modal wallet.onboarding/modal] - :wallet-sign-message-modal [:modal send/sign-message-modal] + :wallet-sign-message-modal [:modal wallet.sign-message/sign-message-modal] :wallet (main-tabs/get-main-tab :wallet) :collectibles-list collectibles/collectibles-list :wallet-onboarding-setup wallet.onboarding/screen :wallet-send-transaction-chat send/send-transaction - :contact-code wallet.components/contact-code :wallet-send-transaction send/send-transaction :recent-recipients request/wallet-request-contacts-list :wallet-transaction-sent transaction-sent/transaction-sent :recipient-qr-code wallet.components/recipient-qr-code :wallet-choose-amount send.views/choose-amount-token - :wallet-choose-asset send.views/choose-asset + :wallet-txn-overview send.views/transaction-overview + :wallet-choose-asset wallet.send.amount/choose-asset :wallet-send-assets wallet.components/send-assets :wallet-request-transaction request/request-transaction :wallet-send-transaction-request request/send-transaction-request diff --git a/src/status_im/ui/screens/routing/wallet_stack.cljs b/src/status_im/ui/screens/routing/wallet_stack.cljs index 0669108c8ad..a408617ff8e 100644 --- a/src/status_im/ui/screens/routing/wallet_stack.cljs +++ b/src/status_im/ui/screens/routing/wallet_stack.cljs @@ -11,6 +11,9 @@ {:name :send-transaction-stack :screens [:wallet-send-transaction :recent-recipients + :wallet-choose-amount + :wallet-txn-overview + :wallet-choose-asset :wallet-transaction-sent :recipient-qr-code :wallet-send-assets]} diff --git a/src/status_im/ui/screens/wallet/components/styles.cljs b/src/status_im/ui/screens/wallet/components/styles.cljs index f9a8742f481..1a8c78c8e4d 100644 --- a/src/status_im/ui/screens/wallet/components/styles.cljs +++ b/src/status_im/ui/screens/wallet/components/styles.cljs @@ -31,29 +31,15 @@ :justify-content :space-between :align-items :center}) -(def cartouche-text-wrapper - {:flex-direction :row - :justify-content :space-between - :padding-horizontal 15 - :padding-vertical 15}) - -(def cartouche-primary-text - {:color colors/white}) - -(def cartouche-secondary-text - {:color colors/white-transparent}) - (def text-content {:color colors/white}) (def text-secondary-content {:color colors/white-transparent}) -(def text - {:margin-right 10}) - (def text-list-primary-content - (merge text {:color colors/black})) + {:margin-right 10 + :color colors/black}) (def text-input (merge text-content @@ -64,22 +50,6 @@ :height 52 :letter-spacing -0.2})) -(def contact-code-text-input - {:text-align-vertical :top - :padding-top 16 - :padding-left 2 - :padding-right 8 - :height 72}) - -(defstyle label - {:color :white - :ios {:line-height 16} - :android {:font-size 12}}) - -(def label-transparent - (merge label - {:color colors/white-transparent})) - (def network {:color :white :font-size 13 @@ -94,27 +64,6 @@ :align-items :center :justify-content :center}) -(def asset-container - {:margin-top 8 - :height 52 - :background-color colors/white-light-transparent - :justify-content :center - :padding-left 14 - :padding-vertical 14 - :padding-right 8 - :border-radius 8}) - -(def asset-container-read-only - {:margin-top 8 - :height 52 - :border-color colors/white-light-transparent - :border-width 1 - :justify-content :center - :padding-left 14 - :padding-vertical 14 - :padding-right 8 - :border-radius 8}) - (def asset-content-container {:flex-direction :row :align-items :center @@ -139,30 +88,6 @@ (def asset-label {:margin-right 10}) -(def asset-text - {:color colors/white}) - -(defstyle container-disabled - {:border-width 1 - :border-color colors/white-light-transparent - :background-color nil - :border-radius 8}) - -(def wallet-container - {:flex-direction :row - :margin-top 8 - :height 52 - :border-width 1 - :border-color colors/white-light-transparent - :align-items :center - :padding 14 - :border-radius 8}) - -(def wallet-name - {:color :white - :font-size 15 - :letter-spacing -0.2}) - (defn participant [address?] {:color (if address? :white colors/white-transparent) :flex-shrink 1 @@ -187,27 +112,3 @@ (def recipient-no-address {:color colors/white-transparent}) - -(def wallet-value-container - {:flex 1 - :flex-direction :row}) - -(def wallet-value - {:padding-left 6 - :color colors/white-transparent - :font-size 15 - :letter-spacing -0.2}) - -(def wallet-value-amount - {:flex -1}) - -(def separator - {:height 1 - :margin-horizontal 15 - :background-color colors/white-light-transparent}) - -(def button-text - {:color :white - :font-size 15 - :letter-spacing -0.2}) - diff --git a/src/status_im/ui/screens/wallet/components/views.cljs b/src/status_im/ui/screens/wallet/components/views.cljs index 302d09d8e4f..f1e2d4772d2 100644 --- a/src/status_im/ui/screens/wallet/components/views.cljs +++ b/src/status_im/ui/screens/wallet/components/views.cljs @@ -90,30 +90,9 @@ [vector-icons/icon icon (merge {:color :white} icon-opts)]] content)]]])]) -(defn- cartouche-primary-text [s] - [react/text {:style styles/cartouche-primary-text} - s]) - -(defn cartouche-secondary-text [s] - [react/text {:style styles/cartouche-secondary-text} - s]) - -(defn cartouche-text-content [primary secondary] - [react/view styles/cartouche-text-wrapper - [cartouche-primary-text primary] - [cartouche-secondary-text secondary]]) - -(defn view-asset [symbol] - [react/view - [react/i18n-text {:style styles/label :key :wallet-asset}] - [react/view styles/asset-container-read-only - [react/text {:style styles/asset-text} - (name symbol)]]]) - (defn- type->handler [k] - (case k - :send :wallet.send/set-symbol - :request :wallet.request/set-symbol + (if (= k :request) + :wallet.request/set-symbol (throw (str "Unknown type: " k)))) (defn- render-token [{:keys [symbol name icon decimals amount] :as token} type] @@ -262,12 +241,3 @@ [amount-input m token]] (when error [tooltip/tooltip error])]) - -(defn separator [] - [react/view styles/separator]) - -(defn button-text [label] - [react/text {:style styles/button-text - :font (if platform/android? :medium :default) - :uppercase? true} - label]) diff --git a/src/status_im/ui/screens/wallet/main/styles.cljs b/src/status_im/ui/screens/wallet/main/styles.cljs index 6b4eb1aecbc..061f85476eb 100644 --- a/src/status_im/ui/screens/wallet/main/styles.cljs +++ b/src/status_im/ui/screens/wallet/main/styles.cljs @@ -55,7 +55,7 @@ (def total-balance-value {:font-size 32 - :font-weight :bold + :font-weight "500" :color colors/white}) (def total-balance-tilde @@ -63,7 +63,7 @@ (defstyle total-balance-currency {:font-size 32 - :font-weight :bold + :font-weight "500" :margin-left 6 :color colors/white-transparent}) diff --git a/src/status_im/ui/screens/wallet/navigation.cljs b/src/status_im/ui/screens/wallet/navigation.cljs index cb1f639b136..980929c2ab1 100644 --- a/src/status_im/ui/screens/wallet/navigation.cljs +++ b/src/status_im/ui/screens/wallet/navigation.cljs @@ -31,12 +31,6 @@ (re-frame/dispatch [:update-transactions]) db) -(def transaction-send-default - (let [symbol :ETH] - {:gas (ethereum/estimate-gas symbol) - :method constants/web3-send-transaction - :symbol symbol})) - (def transaction-request-default {:symbol :ETH}) @@ -45,13 +39,4 @@ (if (= event :navigate-back) db (-> db - (assoc-in [:wallet :request-transaction] transaction-request-default) - (assoc-in [:wallet :send-transaction] transaction-send-default)))) - -(defmethod navigation/preload-data! :wallet-send-transaction - [db [event]] - (if (= event :navigate-back) - db - (do - (re-frame/dispatch [:wallet/update-gas-price]) - (assoc-in db [:wallet :send-transaction] transaction-send-default)))) + (assoc-in [:wallet :request-transaction] transaction-request-default)))) diff --git a/src/status_im/ui/screens/wallet/onboarding/views.cljs b/src/status_im/ui/screens/wallet/onboarding/views.cljs index 81f739a8ff9..29891ddb326 100644 --- a/src/status_im/ui/screens/wallet/onboarding/views.cljs +++ b/src/status_im/ui/screens/wallet/onboarding/views.cljs @@ -100,23 +100,25 @@ nil]]])) (views/defview screen [] - (views/letsubs [{:keys [signing-phrase]} [:account/account]] + (views/letsubs [{:keys [signing-phrase]} [:account/account] + screen-params [:get-screen-params :wallet-onboarding-setup]] [wallet.components/simple-screen {:avoid-keyboard? true} (toolbar) (main-panel signing-phrase - (partial display-confirmation #(re-frame/dispatch [:accounts.ui/wallet-set-up-confirmed false])))])) + (partial display-confirmation #(re-frame/dispatch [:accounts.ui/wallet-set-up-confirmed false screen-params])))])) (views/defview modal [] - (views/letsubs [{:keys [signing-phrase]} [:account/account]] + (views/letsubs [{:keys [signing-phrase]} [:account/account] + screen-params [:get-screen-params :wallet-onboarding-setup-modal]] [react/view styles/modal [status-bar/status-bar {:type :modal-wallet}] [react/view components.styles/flex (toolbar) (main-panel signing-phrase - (partial display-confirmation #(re-frame/dispatch [:accounts.ui/wallet-set-up-confirmed true])))]])) + (partial display-confirmation #(re-frame/dispatch [:accounts.ui/wallet-set-up-confirmed true screen-params])))]])) (defn onboarding [] [react/view styles/root diff --git a/src/status_im/ui/screens/wallet/request/views.cljs b/src/status_im/ui/screens/wallet/request/views.cljs index 0223a9471fe..592c50c258e 100644 --- a/src/status_im/ui/screens/wallet/request/views.cljs +++ b/src/status_im/ui/screens/wallet/request/views.cljs @@ -39,7 +39,7 @@ (ethereum/normalized-address (:address contact))]]]]) (views/defview wallet-request-contacts-list [] - (views/letsubs [contacts [:contacts/all-added-people-contacts]] + (views/letsubs [contacts [:contacts/active]] [wallet.components/simple-screen [wallet.components/toolbar (i18n/label :t/recipient)] [react/view {:flex 1 diff --git a/src/status_im/ui/screens/wallet/send/events.cljs b/src/status_im/ui/screens/wallet/send/events.cljs index e4b9bd57a07..9ac8a558714 100644 --- a/src/status_im/ui/screens/wallet/send/events.cljs +++ b/src/status_im/ui/screens/wallet/send/events.cljs @@ -14,6 +14,7 @@ [status-im.ui.screens.wallet.db :as wallet.db] [status-im.utils.ethereum.ens :as ens] [status-im.utils.ethereum.eip681 :as eip681] + [status-im.utils.ethereum.eip55 :as eip55] [status-im.utils.ethereum.core :as ethereum] [status-im.utils.ethereum.erc20 :as erc20] [status-im.utils.ethereum.tokens :as tokens] @@ -23,25 +24,59 @@ [status-im.utils.security :as security] [status-im.utils.types :as types] [status-im.utils.utils :as utils] - [status-im.utils.config :as config])) + [status-im.utils.config :as config] + [taoensso.timbre :as log])) + +(def wrong-password-error-code 5) ;;;; FX -(defn- send-ethers [params on-completed masked-password] - (status/send-transaction (types/clj->json params) - (security/safe-unmask-data masked-password) - on-completed)) +(defn get-tx-params [{:keys [from to value gas gasPrice] :as params} symbol coin] + (if (= :ETH symbol) + params + (erc20/transfer-tx (:address coin) from to value gas gasPrice))) -(defn- send-tokens [all-tokens symbol chain {:keys [from to value gas gasPrice]} on-completed masked-password] - (let [contract (:address (tokens/symbol->token all-tokens (keyword chain) symbol))] - (erc20/transfer contract from to value gas gasPrice masked-password on-completed))) +(defn send-transaction! [params symbol coin on-completed password] + (let [tx-params (get-tx-params params symbol coin)] + (status/send-transaction (types/clj->json tx-params) + password + on-completed))) -(re-frame/reg-fx - ::send-transaction - (fn [[params all-tokens symbol chain on-completed masked-password]] - (case symbol - :ETH (send-ethers params on-completed masked-password) - (send-tokens all-tokens symbol chain params on-completed masked-password)))) +(handlers/register-handler-fx + :wallet/add-unconfirmed-transaction + (fn [{:keys [db now]} [_ transaction result]] + {:db (assoc-in db [:wallet :transactions result] + (models.wallet/prepare-unconfirmed-transaction db now transaction result))})) + +;;TODO(goranjovic) - fully refactor +(defn on-transaction-completed [transaction flow {:keys [public-key]} {:keys [decimals] :as coin} {:keys [result error]} in-progress?] + (let [{:keys [id method to symbol amount on-result]} transaction + amount-text (str (money/internal->formatted amount symbol decimals))] + (if error + ;; ERROR + (do (utils/show-popup (i18n/label :t/error) + (if (= (:code error) wrong-password-error-code) + (i18n/label :t/wrong-password) + (:message error))) + (reset! in-progress? false)) + (do + (re-frame/dispatch [:wallet/add-unconfirmed-transaction transaction result]) + (if on-result + (re-frame/dispatch (conj on-result id result method)) + (re-frame/dispatch [:send-transaction-message public-key flow {:address to + :asset (name symbol) + :amount amount-text + :tx-hash result}])))))) + +(defn send-transaction-wrapper [{:keys [transaction password flow all-tokens in-progress? chain contact account]}] + (let [symbol (:symbol transaction) + coin (tokens/asset-for all-tokens (keyword chain) symbol)] + (reset! in-progress? true) + (send-transaction! (models.wallet/prepare-send-transaction (:address account) transaction) + symbol + coin + #(on-transaction-completed transaction flow contact coin (types/json->clj %) in-progress?) + password))) (re-frame/reg-fx ::sign-message @@ -67,41 +102,41 @@ from-chat? (assoc :from-chat? from-chat?) gasPrice (assoc :gas-price (money/bignumber gasPrice)))) -(defn extract-qr-code-details [chain qr-uri] +(defn extract-qr-code-details [chain all-tokens qr-uri] {:pre [(keyword? chain) (string? qr-uri)]} ;; i don't like fetching all tokens here - (let [{:keys [:wallet/all-tokens]} @re-frame.db/app-db - qr-uri (string/trim qr-uri) + (let [qr-uri (string/trim qr-uri) chain-id (ethereum/chain-keyword->chain-id chain)] (or (let [m (eip681/parse-uri qr-uri)] (merge m (eip681/extract-request-details m all-tokens))) (when (ethereum/address? qr-uri) {:address qr-uri :chain-id chain-id})))) -(defn qr-data->transaction-data [qr-data] +(defn qr-data->transaction-data [qr-data contacts] {:pre [(map? qr-data)]} - ;; i don't like fetching all tokens here - (let [{:keys [:contacts/contacts]} @re-frame.db/app-db - {:keys [to name] :as tx-details} (qr-data->send-tx-data qr-data) + (let [{:keys [to name] :as tx-details} (qr-data->send-tx-data qr-data) contact-name (:name (contact.db/find-contact-by-address contacts to))] (cond-> tx-details contact-name (assoc :to-name name)))) ;; CHOOSEN RECIPIENT -(defn eth-name->address [chain recipient callback] +(defn eth-name->address [web3 chain recipient callback] (if (ens/is-valid-eth-name? recipient) - (ens/get-addr (get @re-frame.db/app-db :web3) + (ens/get-addr web3 (get ens/ens-registries chain) recipient - callback) + #(callback %)) (callback recipient))) -(defn chosen-recipient [chain recipient success-callback error-callback] - {:pre [(keyword? chain) (string? recipient)]} - (eth-name->address chain recipient - #(if (ethereum/address? %) - (success-callback %) - (error-callback %)))) +(defn chosen-recipient [web3 chain {:keys [to to-ens]} success-callback error-callback] + {:pre [(keyword? chain) (string? to)]} + (eth-name->address web3 chain to + (fn [to] + (if (ethereum/address? to) + (if (and (not to-ens) (not (eip55/valid-address-checksum? to))) + (error-callback :t/wallet-invalid-address-checksum) + (success-callback to)) + (error-callback :t/invalid-address))))) (handlers/register-handler-fx :wallet/transaction-to-success @@ -142,7 +177,8 @@ ::transaction-completed (fn [{:keys [db now] :as cofx} [_ {:keys [result error]}]] (let [{:keys [id method public-key to symbol amount-text on-result]} (get-in db [:wallet :send-transaction]) - db' (assoc-in db [:wallet :send-transaction :in-progress?] false)] + db' (assoc-in db [:wallet :send-transaction :in-progress?] false) + transaction (get-in db [:wallet :send-transaction])] (if error ;; ERROR (models.wallet/handle-transaction-error (assoc cofx :db db') error) @@ -152,20 +188,14 @@ (not= method constants/web3-personal-sign) (assoc-in [:wallet :transactions result] - (models.wallet/prepare-unconfirmed-transaction db now result)))} + (models.wallet/prepare-unconfirmed-transaction db now transaction result)))} (if on-result {:dispatch (conj on-result id result method)} - {:dispatch [:send-transaction-message public-key {:address to - :asset (name symbol) - :amount amount-text - :tx-hash result}]})))))) - -;; DISCARD TRANSACTION -(handlers/register-handler-fx - :wallet/discard-transaction - (fn [cofx _] - (models.wallet/discard-transaction cofx))) + {:dispatch [:send-transaction-message public-key :dapp {:address to + :asset (name symbol) + :amount amount-text + :tx-hash result}]})))))) (handlers/register-handler-fx :wallet.dapp/transaction-on-result @@ -197,7 +227,7 @@ ;;SEND TRANSACTION (= method constants/web3-send-transaction) (let [transaction (models.wallet/prepare-dapp-transaction queued-transaction (:contacts/contacts db))] - (models.wallet/open-modal-wallet-for-transaction db' transaction (first params))) + (models.wallet/open-modal-wallet-for-transaction db' transaction)) ;;SIGN MESSAGE (= method constants/web3-personal-sign) @@ -217,31 +247,15 @@ :send-transaction-message (concat [(re-frame/inject-cofx :random-id-generator)] navigation/navigation-interceptors) - (fn [{:keys [db] :as cofx} [_ chat-id params]] + (fn [{:keys [db] :as cofx} [_ chat-id flow params]] ;;NOTE(goranjovic): we want to send the payment message only when we have a whisper id ;; for the recipient, we always redirect to `:wallet-transaction-sent` even when we don't (let [send-command? (and chat-id (get-in db [:id->command ["send" #{:personal-chats}]]))] (fx/merge cofx - #(when send-command? + #(when (and chat-id send-command?) (commands-sending/send % chat-id send-command? params)) - (navigation/navigate-to-clean - (if (= (:view-id db) :wallet-send-transaction) - :wallet-transaction-sent - :wallet-transaction-sent-modal) - {}))))) - -(defn set-and-validate-amount-db [db amount symbol decimals] - (let [{:keys [value error]} (wallet.db/parse-amount amount decimals)] - (-> db - (assoc-in [:wallet :send-transaction :amount] (money/formatted->internal value symbol decimals)) - (assoc-in [:wallet :send-transaction :amount-text] amount) - (assoc-in [:wallet :send-transaction :amount-error] error)))) - -(handlers/register-handler-fx - :wallet.send/set-and-validate-amount - (fn [{:keys [db]} [_ amount symbol decimals]] - {:db (set-and-validate-amount-db db amount symbol decimals)})) - + (navigation/navigate-to-clean :wallet-transaction-sent {:flow flow + :chat-id chat-id}))))) (handlers/register-handler-fx :wallet/discard-transaction-navigate-back (fn [cofx _] @@ -249,40 +263,6 @@ (navigation/navigate-back) (models.wallet/discard-transaction)))) -(defn update-gas-price - ([db edit? success-event] - {:update-gas-price {:web3 (:web3 db) - :success-event (or success-event :wallet/update-gas-price-success) - :edit? edit?}}) - ([db edit?] (update-gas-price db edit? :wallet/update-gas-price-success)) - ([db] (update-gas-price db false :wallet/update-gas-price-success))) - -(defn recalculate-gas [{:keys [db] :as fx} symbol] - (-> fx - (assoc-in [:db :wallet :send-transaction :gas] (ethereum/estimate-gas symbol)) - (merge (update-gas-price db)))) - -(handlers/register-handler-fx - :wallet/update-gas-price - (fn [{:keys [db]} [_ edit?]] - (update-gas-price db edit?))) - -(handlers/register-handler-fx - :wallet.send/set-symbol - (fn [{:keys [db]} [_ symbol]] - (let [old-symbol (get-in db [:wallet :send-transaction :symbol])] - (cond-> {:db (-> db - (assoc-in [:wallet :send-transaction :symbol] symbol) - (assoc-in [:wallet :send-transaction :amount] nil) - (assoc-in [:wallet :send-transaction :amount-text] nil) - (assoc-in [:wallet :send-transaction :asset-error] nil))} - (not= old-symbol symbol) (recalculate-gas symbol))))) - -(handlers/register-handler-fx - :wallet.send/toggle-advanced - (fn [{:keys [db]} [_ advanced?]] - {:db (assoc-in db [:wallet :send-transaction :advanced?] advanced?)})) - (handlers/register-handler-fx :wallet/cancel-entering-password (fn [{:keys [db]} _] @@ -296,41 +276,13 @@ (fn [{:keys [db]} [_ masked-password]] {:db (assoc-in db [:wallet :send-transaction :password] masked-password)})) -(handlers/register-handler-fx - :wallet.send/edit-value - (fn [cofx [_ key value]] - (models.wallet/edit-value key value cofx))) - -(handlers/register-handler-fx - :wallet.send/set-gas-details - (fn [{:keys [db]} [_ gas gas-price]] - {:db (-> db - (assoc-in [:wallet :send-transaction :gas] gas) - (assoc-in [:wallet :send-transaction :gas-price] gas-price))})) - -(handlers/register-handler-fx - :wallet.send/clear-gas - (fn [{:keys [db]}] - {:db (update db :wallet dissoc :edit)})) - -(handlers/register-handler-fx - :wallet.send/reset-gas-default - (fn [{:keys [db] :as cofx}] - (let [gas-default (if-some [original-gas (-> db :wallet :send-transaction :original-gas)] - (money/to-fixed original-gas) - (money/to-fixed - (ethereum/estimate-gas - (-> db :wallet :send-transaction :symbol))))] - (assoc (models.wallet/edit-value - :gas - gas-default - cofx) - :dispatch [:wallet/update-gas-price true])))) - (handlers/register-handler-fx :close-transaction-sent-screen - (fn [cofx [_ chat-id]] + (fn [cofx [_ chat-id flow]] (fx/merge cofx {:dispatch-later [{:ms 400 :dispatch [:check-dapps-transactions-queue]}]} - (navigation/navigate-back)))) + #(case flow + :chat (re-frame/dispatch [:chat.ui/navigate-to-chat chat-id {}]) + :dapp (re-frame/dispatch [:navigate-back]) + (re-frame/dispatch [:navigate-to :wallet {}]))))) diff --git a/src/status_im/ui/screens/wallet/send/styles.cljs b/src/status_im/ui/screens/wallet/send/styles.cljs index fb83e6d9d60..ed9a440f56d 100644 --- a/src/status_im/ui/screens/wallet/send/styles.cljs +++ b/src/status_im/ui/screens/wallet/send/styles.cljs @@ -1,7 +1,6 @@ (ns status-im.ui.screens.wallet.send.styles (:require-macros [status-im.utils.styles :refer [defstyle]]) (:require [status-im.ui.components.colors :as colors] - [status-im.ui.components.styles :as styles] [status-im.ui.screens.wallet.components.styles :as wallet.components.styles])) (def send-transaction-form @@ -76,61 +75,10 @@ :padding 8 :align-items :center}) -(def advanced-button-wrapper - {:align-items :center}) - -(def advanced-wrapper - {:margin-top 24 - :margin-bottom 16}) - -(def gas-container-wrapper - {:flex 1 - :flex-direction :row}) - -(def gas-input-wrapper - {:align-items :center - :justify-content :space-between - :flex-direction :row}) - -(def advanced-options-text-wrapper - {:flex 1 - :flex-direction :row - :justify-content :space-between - :margin-vertical 15}) - -(def advanced-label - {:text-align-vertical :center - :margin-left 4}) - -(def advanced-fees-text - {:color colors/white}) - -(def advanced-fees-details-text - {:color colors/white-transparent}) - -(def transaction-fee-block-wrapper - {:flex-direction :row}) - (def transaction-fee-info {:flex-direction :row :margin 15}) -(def transaction-fee-info-text-wrapper - {:flex-shrink 1}) - -(def transaction-fee-info-icon - {:border-radius 25 - :width 25 - :height 25 - :margin-right 15 - :align-items :center - :justify-content :center - :background-color colors/black-transparent}) - -(def transaction-fee-info-icon-text - {:color colors/white - :font-size 14}) - (def transaction-fee-input {:keyboard-type :numeric :auto-capitalize "none" @@ -143,27 +91,22 @@ {:background-color colors/blue :padding-vertical 8}) -(def fee-buttons - {:background-color colors/blue}) - (def password-error-tooltip {:bottom-value 15 :color colors/red-light}) -(defstyle gas-input-error-tooltip - {:android {:bottom-value -38}}) - ;; ---------------------------------------------------------------------- ;; Choose Address View ;; ---------------------------------------------------------------------- -(def centered {:justify-content :center - :align-items :center}) +(def centered + {:justify-content :center + :align-items :center}) (defstyle choose-recipient-text-input {:color colors/white :font-size 30 - :font-weight :bold - :android {:width "100%"} - :ios {:min-width 236} + :font-weight "500" + :android {:width "75%"} + :ios {:min-width 236} :margin-horizontal 24}) diff --git a/src/status_im/ui/screens/wallet/send/subs.cljs b/src/status_im/ui/screens/wallet/send/subs.cljs index 63bc84c4216..ddb935bde4b 100644 --- a/src/status_im/ui/screens/wallet/send/subs.cljs +++ b/src/status_im/ui/screens/wallet/send/subs.cljs @@ -1,7 +1,9 @@ (ns status-im.ui.screens.wallet.send.subs (:require [re-frame.core :as re-frame] [status-im.utils.money :as money] - [status-im.models.wallet :as models.wallet])) + [status-im.models.wallet :as models.wallet] + [status-im.utils.ethereum.core :as ethereum] + [status-im.constants :as constants])) (re-frame/reg-sub ::send-transaction @@ -9,24 +11,6 @@ (fn [wallet] (:send-transaction wallet))) -(re-frame/reg-sub - :wallet.send/symbol - :<- [::send-transaction] - (fn [send-transaction] - (:symbol send-transaction))) - -(re-frame/reg-sub - :wallet.send/advanced? - :<- [::send-transaction] - (fn [send-transaction] - (:advanced? send-transaction))) - -(re-frame/reg-sub - :wallet.send/camera-flashlight - :<- [::send-transaction] - (fn [send-transaction] - (:camera-flashlight send-transaction))) - (re-frame/reg-sub :wallet.send/wrong-password? :<- [::send-transaction] @@ -39,38 +23,15 @@ (fn [{:keys [password]}] (and (not (nil? password)) (not= password "")))) -(defn edit-or-transaction-data - "Set up edit data structure, defaulting to transaction when not available" - [transaction edit] - (cond-> edit - (not (get-in edit [:gas-price :value])) - (models.wallet/build-edit - :gas-price - (money/to-fixed (money/wei-> :gwei (:gas-price transaction)))) - - (not (get-in edit [:gas :value])) - (models.wallet/build-edit - :gas - (money/to-fixed (:gas transaction))))) - -(re-frame/reg-sub - :wallet/edit - :<- [::send-transaction] - :<- [:wallet] - (fn [[send-transaction {:keys [edit]}]] - (edit-or-transaction-data - send-transaction - edit))) - -(defn check-sufficient-funds [transaction balance symbol amount] +(defn- check-sufficient-funds [{:keys [amount symbol] :as transaction} balance] (assoc transaction :sufficient-funds? (or (nil? amount) (money/sufficient-funds? amount (get balance symbol))))) -(defn check-sufficient-gas [transaction balance symbol amount] +(defn- check-sufficient-gas [{:keys [amount symbol] :as transaction} balance] (assoc transaction :sufficient-gas? (or (nil? amount) - (let [available-ether (get balance :ETH (money/bignumber 0)) + (let [available-ether (get balance :ETH (money/bignumber 0)) available-for-gas (if (= :ETH symbol) (.minus available-ether (money/bignumber amount)) available-ether)] @@ -80,13 +41,16 @@ (money/formatted->internal :ETH 18)) (money/bignumber available-for-gas)))))) +(def transaction-send-default + {:method constants/web3-send-transaction + :symbol :ETH}) + (re-frame/reg-sub :wallet.send/transaction - :<- [::send-transaction] :<- [:balance] - (fn [[{:keys [amount symbol] :as transaction} balance]] - (-> transaction + (fn [balance] + (-> transaction-send-default (models.wallet/transform-data-for-message) (models.wallet/add-max-fee) - (check-sufficient-funds balance symbol amount) - (check-sufficient-gas balance symbol amount)))) + (check-sufficient-funds balance) + (check-sufficient-gas balance)))) diff --git a/src/status_im/ui/screens/wallet/send/views.cljs b/src/status_im/ui/screens/wallet/send/views.cljs index 717087e06d5..d379137982c 100644 --- a/src/status_im/ui/screens/wallet/send/views.cljs +++ b/src/status_im/ui/screens/wallet/send/views.cljs @@ -1,1294 +1,72 @@ (ns status-im.ui.screens.wallet.send.views (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [clojure.string :as string] - [re-frame.core :as re-frame] - [status-im.contact.db :as contact.db] - [status-im.i18n :as i18n] - [status-im.ui.components.animation :as animation] - [status-im.ui.components.bottom-buttons.view :as bottom-buttons] - [status-im.ui.components.button.view :as button] - [status-im.ui.components.common.common :as common] - [status-im.ui.components.icons.vector-icons :as vector-icons] - [status-im.ui.components.react :as react] - [status-im.ui.components.status-bar.view :as status-bar] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.toolbar.actions :as actions] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.tooltip.views :as tooltip] - [status-im.ui.components.list.views :as list] - [status-im.ui.components.list.styles :as list.styles] - [status-im.ui.screens.chat.photos :as photos] - [status-im.ui.screens.wallet.components.styles :as wallet.components.styles] - [status-im.ui.screens.wallet.components.views :as components] - [status-im.ui.screens.wallet.components.views :as wallet.components] - [status-im.ui.screens.wallet.db :as wallet.db] - [status-im.ui.screens.wallet.send.animations :as send.animations] - [status-im.ui.screens.wallet.send.styles :as styles] - [status-im.ui.screens.wallet.send.events :as events] - [status-im.ui.screens.wallet.styles :as wallet.styles] - [status-im.ui.screens.wallet.main.views :as wallet.main.views] - [status-im.utils.money :as money] - [status-im.utils.security :as security] - [status-im.utils.utils :as utils] + (:require [status-im.ui.screens.wallet.send.views.recipient :as recipient] + [status-im.ui.screens.wallet.send.views.amount :as amount] + [status-im.ui.screens.wallet.send.views.overview :as overview] [status-im.utils.ethereum.tokens :as tokens] [status-im.utils.ethereum.core :as ethereum] - [status-im.transport.utils :as transport.utils] - [status-im.utils.ethereum.ens :as ens] - [taoensso.timbre :as log] - [reagent.core :as reagent] - [status-im.ui.components.colors :as colors] - [status-im.ui.screens.wallet.utils :as wallet.utils])) - -(defn- toolbar [modal? title] - (let [action (if modal? actions/close-white actions/back-white)] - [toolbar/toolbar {:style wallet.styles/toolbar} - [toolbar/nav-button (action (if modal? - #(re-frame/dispatch [:wallet/discard-transaction-navigate-back]) - #(actions/default-handler)))] - [toolbar/content-title {:color :white :font-weight :bold :font-size 17} title]])) - -(defn- advanced-cartouche [native-currency {:keys [max-fee gas gas-price]}] - [react/view - [wallet.components/cartouche {:on-press #(do (re-frame/dispatch [:wallet.send/clear-gas]) - (re-frame/dispatch [:navigate-to :wallet-transaction-fee]))} - (i18n/label :t/wallet-transaction-fee) - [react/view {:style styles/advanced-options-text-wrapper - :accessibility-label :transaction-fee-button} - [react/text {:style styles/advanced-fees-text} - (str max-fee " " (wallet.utils/display-symbol native-currency))] - [react/text {:style styles/advanced-fees-details-text} - (str (money/to-fixed gas) " * " (money/to-fixed (money/wei-> :gwei gas-price)) (i18n/label :t/gwei))]]]]) - -(defn- advanced-options [advanced? native-currency transaction scroll] - [react/view {:style styles/advanced-wrapper} - [react/touchable-highlight {:on-press (fn [] - (re-frame/dispatch [:wallet.send/toggle-advanced (not advanced?)]) - (when (and scroll @scroll) (utils/set-timeout #(.scrollToEnd @scroll) 350)))} - [react/view {:style styles/advanced-button-wrapper} - [react/view {:style styles/advanced-button - :accessibility-label :advanced-button} - [react/i18n-text {:style (merge wallet.components.styles/label - styles/advanced-label) - :key :wallet-advanced}] - [vector-icons/icon (if advanced? :main-icons/dropdown-up :main-icons/dropdown) {:color :white}]]]] - (when advanced? - [advanced-cartouche native-currency transaction])]) - -;; ---------------------------------------------------------------------- -;; Step 1 choosing an address or contact to send the transaction to -;; ---------------------------------------------------------------------- - -(defn simple-tab-navigator - "A simple tab navigator that that takes a map of tabs and the key of - the starting tab - - Example: - (simple-tab-navigator - {:main {:name \"Main\" :component (fn [] [react/text \"Hello\"])} - :other {:name \"Other\" :component (fn [] [react/text \"Goodbye\"])}} - :main)" - [tab-map default-key] - {:pre [(keyword? default-key)]} - (let [tab-key (reagent/atom default-key)] - (fn [tab-map _] - (let [tab-name @tab-key] - [react/view {:flex 1} - ;; tabs row - [react/view {:flex-direction :row} - (map (fn [[key {:keys [name component]}]] - (let [current? (= key tab-name)] - ^{:key (str key)} - [react/view {:flex 1 - :background-color colors/black-transparent} - [react/touchable-highlight {:on-press #(reset! tab-key key) - :disabled current?} - [react/view {:height 44 - :align-items :center - :justify-content :center - :border-bottom-width 2 - :border-bottom-color (if current? colors/white "rgba(0,0,0,0)")} - [react/text {:style {:color (if current? :white "rgba(255,255,255,0.4)") - :font-size 15}} name]]]])) - tab-map)] - (when-let [component-thunk (some-> tab-map tab-name :component)] - [component-thunk])])))) - -;; just a helper for the buttons in choose address view -(defn- address-button [{:keys [disabled? on-press underlay-color background-color]} content] - [react/touchable-highlight {:underlay-color underlay-color - :disabled disabled? - :on-press on-press - :style {:height 44 - :background-color background-color - :border-radius 8 - :flex 1 - :align-items :center - :justify-content :center - :margin 3}} - content]) - -;; event code - -(defn open-qr-scanner [chain text-input transaction] - (.blur @text-input) - (re-frame/dispatch [:navigate-to :recipient-qr-code - {:on-recipient - (fn [qr-data] - (if-let [parsed-qr-data (events/extract-qr-code-details chain qr-data)] - (let [{:keys [chain-id]} parsed-qr-data - tx-data (events/qr-data->transaction-data parsed-qr-data)] - (if (= chain-id (ethereum/chain-keyword->chain-id chain)) - (swap! transaction merge tx-data) - (utils/show-popup (i18n/label :t/error) - (i18n/label :t/wallet-invalid-chain-id {:data qr-data - :chain chain-id})))) - (utils/show-confirmation - {:title (i18n/label :t/error) - :content (i18n/label :t/wallet-invalid-address {:data qr-data}) - :cancel-button-text (i18n/label :t/see-it-again) - :confirm-button-text (i18n/label :t/got-it) - :on-cancel (partial open-qr-scanner chain text-input transaction)})))}])) - -(defn update-recipient [chain transaction error-message value] - (if (ens/is-valid-eth-name? value) - (do (ens/get-addr (get @re-frame.db/app-db :web3) - (get ens/ens-registries chain) - value - #(if (ethereum/address? %) - (swap! transaction assoc :to-ens value :to %) - (reset! error-message "Unknown ENS name")))) - (do (swap! transaction assoc :to value) - (reset! error-message nil)))) - -(defn choose-address-view - "A view that allows you to choose an address" - [{:keys [chain transaction on-address]}] - {:pre [(keyword? chain) (fn? on-address)]} - (fn [] - (let [error-message (reagent/atom nil) - text-input (atom nil)] - (fn [] - [react/view {:flex 1} - [react/view {:flex 1}] - [react/view styles/centered - (when @error-message - [tooltip/tooltip @error-message {:color colors/white - :font-size 12 - :bottom-value 15}]) - [react/text-input - {:on-change-text (partial update-recipient chain transaction error-message) - :auto-focus true - :auto-capitalize :none - :auto-correct false - :placeholder "0x... or name.eth" - :placeholder-text-color "rgb(143,162,234)" - :multiline true - :max-length 84 - :ref #(reset! text-input %) - ;;NOTE(goranjovic)- :default-value instead of :value to prevent the flickering due to two way binding - :default-value (or (:to-ens @transaction) (:to @transaction)) - :selection-color colors/green - :accessibility-label :recipient-address-input - :style styles/choose-recipient-text-input}]] - [react/view {:flex 1}] - [react/view {:flex-direction :row :padding 3} - [address-button {:underlay-color colors/white-transparent - :background-color colors/black-transparent - :on-press #(react/get-from-clipboard - (fn [addr] - (when (and addr (not (string/blank? addr))) - (swap! transaction assoc :to (string/trim addr)))))} - [react/view {:flex-direction :row - :padding-horizontal 18} - [vector-icons/icon :icons/paste {:color colors/white-transparent}] - [react/view {:flex 1 :flex-direction :row :justify-content :center} - [react/text {:style {:color colors/white - :font-size 15 - :line-height 22}} - (i18n/label :t/paste)]]]] - [address-button {:underlay-color colors/white-transparent - :background-color colors/black-transparent - :on-press - (fn [] - (re-frame/dispatch - [:request-permissions {:permissions [:camera] - :on-allowed (partial open-qr-scanner chain text-input transaction) - :on-denied - #(utils/set-timeout - (fn [] - (utils/show-popup (i18n/label :t/error) - (i18n/label :t/camera-access-error))) - 50)}]))} - [react/view {:flex-direction :row - :padding-horizontal 18} - [vector-icons/icon :icons/qr {:color colors/white-transparent}] - [react/view {:flex 1 :flex-direction :row :justify-content :center} - [react/text {:style {:color colors/white - :font-size 15 - :line-height 22}} - (i18n/label :t/scan)]]]] - (let [disabled? (string/blank? (:to @transaction))] - [address-button {:disabled? disabled? - :underlay-color colors/black-transparent - :background-color (if disabled? colors/blue colors/white) - :on-press - #(events/chosen-recipient chain (:to @transaction) on-address - (fn on-error [_] - (reset! error-message (i18n/label :t/invalid-address))))} - [react/text {:style {:color (if disabled? colors/white colors/blue) - :font-size 15 - :line-height 22}} - (i18n/label :t/next)]])]])))) - -;; #(re-frame/dispatch [:wallet/fill-request-from-contact contact]) - -(defn info-page [message] - [react/view {:style {:flex 1 - :align-items :center - :justify-content :center - :background-color colors/blue}} - [vector-icons/icon :icons/info {:color colors/white}] - [react/text {:style {:max-width 144 - :margin-top 15 - :color colors/white - :font-size 15 - :text-align :center - :line-height 22}} - message]]) - -(defn render-contact [on-contact contact] - {:pre [(fn? on-contact) (map? contact) (:address contact)]} - [react/touchable-highlight {:underlay-color colors/white-transparent - :on-press #(on-contact contact)} - [react/view {:flex 1 - :flex-direction :row - :padding-right 23 - :padding-left 16 - :padding-top 12} - [react/view {:margin-top 3} - [photos/photo (:photo-path contact) {:size list.styles/image-size}]] - [react/view {:margin-left 16 - :flex 1} - [react/view {:accessibility-label :contact-name-text - :margin-bottom 2} - [react/text {:style {:font-size 15 - :font-weight "500" - :line-height 22 - :color colors/white}} - (:name contact)]] - [react/text {:style {:font-size 15 - :line-height 22 - :color "rgba(255,255,255,0.4)"} - :accessibility-label :contact-address-text} - (ethereum/normalized-address (:address contact))]]]]) - -(defn choose-contact-view [{:keys [contacts on-contact]}] - {:pre [(every? map? contacts) (fn? on-contact)]} - (if (empty? contacts) - (info-page (i18n/label :t/wallet-no-contacts)) - [react/view {:flex 1} - [list/flat-list {:data contacts - :key-fn :address - :render-fn (partial - render-contact - on-contact)}]])) - -;; TODO clean up all dependencies here, leaving these in place until all behavior is verified on -;; all platforms -(defn- choose-address-contact [{:keys [modal? contacts scroll advanced? transaction network all-tokens amount-input network-status] :as opts}] - - (let [transaction (reagent/atom transaction) - chain (ethereum/network->chain-keyword network) - native-currency (tokens/native-currency chain) - {:keys [decimals] :as token} (tokens/asset-for all-tokens chain symbol) - online? (= :online network-status)] - [wallet.components/simple-screen {:avoid-keyboard? (not modal?) - :status-bar-type (if modal? :modal-wallet :wallet)} - [toolbar modal? (i18n/label :t/send-to)] - [simple-tab-navigator - {:address - {:name "Address" - :component (choose-address-view - {:chain chain - :transaction transaction - :on-address - #(re-frame/dispatch [:navigate-to :wallet-choose-amount - {:transaction (swap! transaction assoc :to %) - :native-currency native-currency - :modal? modal?}])})} - :contacts - {:name "Contacts" - :component - (partial choose-contact-view - {:contacts contacts - :on-contact - (fn [{:keys [address] :as contact}] - (re-frame/dispatch - [:navigate-to :wallet-choose-amount - {:modal? modal? - :native-currency native-currency - :contact contact - :transaction (swap! transaction assoc :to address)}]))})}} - :address]])) - -;; ---------------------------------------------------------------------- -;; Step 2 choosing an amount and token to send -;; ---------------------------------------------------------------------- - -(declare network-fees) - -;; worthy abstraction -(defn- anim-ref-send - "Call one of the methods in an animation ref. - -Takes an animation ref (a map of keys to animation tiggering methods) - -A keyword that should equal one of the keys in the map - -and optional args to be sent to the animation. - -Example: -(anim-ref-send slider-ref :open!) -(anim-ref-send slider-ref :move-top-left! 25 25)" - [anim-ref signal & args] - (when anim-ref - (assert (get anim-ref signal) - (str "Key " signal - " was not found in animation ref. Should be in " - (pr-str (keys anim-ref))))) - (some-> anim-ref (get signal) (apply args))) - -(defn- slide-up-modal - "Creates a modal that slides up from the bottom of the screen and - responds to a swipe down gesture to dismiss - - The modal initially renders in the closed position. - - It takes an options map and the react child to be displayed in the - modal. - - Options: - :anim-ref - takes a function that will be called with a map of - animation methods. - :swipe-dismiss? - a boolean that determines wether the modal screen - should be dismissed on swipe down gesture - - This slide-up-modal will callback the `anim-ref` fn and provides a - map with 2 animation methods: - - :open! - opens and displays the modal - :close! - closes the modal" - [{:keys [anim-ref swipe-dismiss?]} children] - {:pre [(fn? anim-ref)]} - ;; Add swipe down to dissmiss - (let [window-height (:height (react/get-dimensions "window") 1000) - - bottom-position (animation/create-value (- window-height)) - - modal-screen-bg-color - (animation/interpolate bottom-position - {:inputRange [(- window-height) 0] - :outputRange ["rgba(0,0,0,0)" - "rgba(0,0,0,0.7)"]}) - modal-screen-top - (animation/interpolate bottom-position - {:inputRange [(- window-height) - (+ (- window-height) 1) - 0] - :outputRange [window-height -200 -200]}) - - vertical-slide-to (fn [view-bottom] - (animation/start - (animation/timing bottom-position {:toValue view-bottom - :duration 500}))) - open-panel! #(vertical-slide-to 0) - close-panel! #(vertical-slide-to (- window-height)) - ;; swipe-down-panhandler - swipe-down-handlers - (when swipe-dismiss? - (js->clj - (.-panHandlers - (.create react/pan-responder - #js {:onMoveShouldSetPanResponder - (fn [e g] - (when-let [distance (.-dy g)] - (< 50 distance))) - :onMoveShouldSetPanResponderCapture - (fn [e g] - (when-let [distance (.-dy g)] - (< 50 distance))) - :onPanResponderRelease (fn [e g] - (when-let [distance (.-dy g)] - (when (< 200 distance) - (close-panel!))))}))))] - (anim-ref {:open! open-panel! - :close! close-panel!}) - (fn [{:keys [anim-ref] :as opts} children] - [react/animated-view (merge - {:style - {:position :absolute - :top modal-screen-top - :bottom 0 - :left 0 - :right 0 - :z-index 1 - :background-color modal-screen-bg-color}} - swipe-down-handlers) - [react/touchable-highlight {:on-press (fn [] (close-panel!)) - :style {:flex 1}} - [react/view]] - [react/animated-view {:style - {:position :absolute - :left 0 - :right 0 - :z-index 2 - :bottom bottom-position}} - children]]))) - -(defn- custom-gas-panel-action [{:keys [label active on-press icon background-color]} child] - {:pre [label (boolean? active) on-press icon background-color]} - [react/view {:style {:flex-direction :row - :padding-horizontal 22 - :padding-vertical 11 - :align-items :center}} - [react/touchable-highlight - {:disabled active - :on-press on-press} - [react/animated-view {:style {:border-radius 21 - :width 40 - :height 40 - :justify-content :center - :align-items :center - :background-color background-color}} - [vector-icons/icon icon {:color (if active colors/white colors/gray)}]]] - [react/touchable-highlight - {:disabled active - :on-press on-press - :style {:flex 1}} - [react/text {:style {:color colors/black - :font-size 17 - :padding-left 17 - :line-height 40}} - label]] - child]) - -(defn- custom-gas-edit - [{:keys [on-gas-input-change - on-gas-price-input-change - gas-input - gas-price-input]}] - (let [gas-error (reagent/atom nil) - gas-price-error (reagent/atom nil)] - (fn [{:keys [on-gas-input-change - on-gas-price-input-change - gas-input - gas-price-input]}] - [react/view {:style {:padding-horizontal 22 - :padding-vertical 11}} - [react/text "Gas price"] - (when @gas-price-error - [react/view {:style {:z-index 100}} - [tooltip/tooltip @gas-price-error - {:color colors/blue-light - :font-size 12 - :bottom-value -3}]]) - [react/view {:style {:border-radius 8 - :background-color colors/gray-light - :padding-vertical 16 - :padding-horizontal 16 - :flex-direction :row - :align-items :flex-end - :margin-vertical 7}} - [react/text-input {:keyboard-type :numeric - :placeholder "0" - :on-change-text (fn [x] - (if-not (money/bignumber x) - (reset! gas-price-error "Invalid number format") - (reset! gas-price-error nil)) - (on-gas-price-input-change x)) - :value gas-price-input - :style {:font-size 15 - :flex 1}}] - [react/text "Gwei"]] - [react/text {:style {:color colors/gray - :font-size 12}} - "Gas price is the amount you are willing to pay per unit of gas. Increasing this price may help your transaction get processed faster."] - [react/text {:style {:margin-top 22}} "Gas limit"] - (when @gas-error - [react/view {:style {:z-index 100}} - [tooltip/tooltip @gas-error - {:color colors/blue-light - :font-size 12 - :bottom-value -3}]]) - [react/view {:style {:border-radius 8 - :background-color colors/gray-light - :padding-vertical 16 - :padding-horizontal 16 - :flex-direction :row - :align-items :flex-end - :margin-vertical 7}} - [react/text-input {:keyboard-type :numeric - :placeholder "0" - :on-change-text - (fn [x] - (if-not (money/bignumber x) - (reset! gas-error "Invalid number format") - (reset! gas-error nil)) - (on-gas-input-change x)) - :value gas-input - :style {:font-size 15 - :flex 1}}]] - [react/text {:style {:color colors/gray - :font-size 12}} - "Gas limit is the maximum units of gas you're willing to spend on this transaction."]]))) - -(defn custom-gas-derived-state [{:keys [gas-input gas-price-input custom-open?]} - {:keys [custom-gas custom-gas-price - optimal-gas optimal-gas-price - gas-gas-price->fiat - fiat-currency] :as opts}] - (let [custom-input-gas - (or (when (not (string/blank? gas-input)) - (money/bignumber gas-input)) - custom-gas - optimal-gas) - custom-input-gas-price - (or (when (not (string/blank? gas-price-input)) - (money/->wei :gwei gas-price-input)) - custom-gas-price - optimal-gas-price)] - {:optimal-fiat-price - (str "~ " (:symbol fiat-currency) - (gas-gas-price->fiat {:gas optimal-gas - :gas-price optimal-gas-price})) - :custom-fiat-price - (if custom-open? - (str "~ " (:symbol fiat-currency) - (gas-gas-price->fiat {:gas custom-input-gas - :gas-price custom-input-gas-price})) - (str "...")) - :gas-price-input-value - (str (or gas-price-input - (some->> custom-gas-price (money/wei-> :gwei)) - (some->> optimal-gas-price (money/wei-> :gwei)))) - :gas-input-value - (str (or gas-input custom-gas optimal-gas)) - :gas-map-for-submit - (when custom-open? - {:gas custom-input-gas :gas-price custom-input-gas-price})})) - -;; Choosing the gas amount -(defn custom-gas-input-panel [{:keys [custom-gas custom-gas-price - optimal-gas optimal-gas-price - gas-gas-price->fiat on-submit] :as opts}] - {:pre [optimal-gas optimal-gas-price gas-gas-price->fiat on-submit]} - (let [custom-open? (and custom-gas custom-gas-price) - state-atom (reagent.core/atom {:custom-open? (boolean custom-open?) - :gas-input nil - :gas-price-input nil}) - - ;; slider animations - slider-height (animation/create-value (if custom-open? 290 0)) - slider-height-to #(animation/start - (animation/timing slider-height {:toValue % - :duration 500})) - - optimal-button-bg-color - (animation/interpolate slider-height - {:inputRange [0 200 290] - :outputRange [colors/blue colors/gray-light colors/gray-light]}) - - custom-button-bg-color - (animation/interpolate slider-height - {:inputRange [0 200 290] - :outputRange [colors/gray-light colors/blue colors/blue]}) - - open-slider! #(do - (slider-height-to 290) - (swap! state-atom assoc :custom-open? true)) - close-slider! #(do - (slider-height-to 0) - (swap! state-atom assoc :custom-open? false))] - (fn [opts] - (let [{:keys [optimal-fiat-price - custom-fiat-price - gas-price-input-value - gas-input-value - gas-map-for-submit]} - (custom-gas-derived-state @state-atom opts)] - [react/view {:style {:background-color colors/white - :border-top-left-radius 8 - :border-top-right-radius 8}} - [react/view {:style {:justify-content :center - :padding-top 22 - :padding-bottom 7}} - [react/text - {:style {:color colors/black - :font-size 22 - :line-height 28 - :font-weight :bold - :text-align :center}} - "Network fee settings"] - [react/text - {:style {:color colors/gray - :font-size 15 - :line-height 22 - :text-align :center - :padding-horizontal 45 - :padding-vertical 8}} - "This fee, known as gas is paid directly to the Ethereum network. Status does not collect any of these funds"]] - [react/view {:style {:border-top-width 1 - :border-top-color colors/black-transparent - :padding-top 11 - :padding-bottom 7}} - (custom-gas-panel-action - {:icon :icons/time - :label "Optimal" - :on-press close-slider! - :background-color optimal-button-bg-color - :active (not (:custom-open? @state-atom))} - [react/text {:style {:color colors/gray - :font-size 17 - :padding-left 17 - :line-height 20}} - optimal-fiat-price]) - (custom-gas-panel-action - {:icon :icons/sliders - :label "Custom" - :on-press open-slider! - :background-color custom-button-bg-color - :active (:custom-open? @state-atom)} - [react/text {:style {:color colors/gray - :font-size 17 - :padding-left 17 - :line-height 20 - :text-align :center - :min-width 60}} - custom-fiat-price]) - [react/animated-view {:style {:background-color colors/white - :height slider-height - :overflow :hidden}} - [custom-gas-edit - {:on-gas-price-input-change - #(when (money/bignumber %) - (swap! state-atom assoc :gas-price-input %)) - :on-gas-input-change - #(when (money/bignumber %) - (swap! state-atom assoc :gas-input %)) - :gas-price-input gas-price-input-value - :gas-input gas-input-value}]] - [react/view {:style {:flex-direction :row - :justify-content :center - :padding-vertical 16}} - [react/touchable-highlight - {:on-press #(on-submit gas-map-for-submit) - :style {:padding-horizontal 39 - :padding-vertical 12 - :border-radius 8 - :background-color colors/blue-light}} - [react/text {:style {:font-size 15 - :line-height 22 - :color colors/blue}} - "Update"]]]]])))) - -;; Choosing the asset - -(defn white-toolbar [modal? title] - (let [action (if modal? actions/close actions/back)] - [toolbar/toolbar {:style {:background-color colors/white - :border-bottom-width 1 - :border-bottom-color colors/black-transparent}} - [toolbar/nav-button (action (if modal? - #(re-frame/dispatch [:wallet/discard-transaction-navigate-back]) - #(actions/default-handler)))] - [toolbar/content-title {:color colors/black :font-size 17 :font-weight :bold} title]])) - -(defn- render-token-item [{:keys [symbol name icon decimals amount] :as token}] - [list/item - [list/item-image icon] - [list/item-content - [react/text {:style {:margin-right 10, :color colors/black}} name] - [list/item-secondary (str (wallet.utils/format-amount amount decimals) - " " - (wallet.utils/display-symbol token))]]]) - -;; TODO parameterize this with on-asset handler -(defview choose-asset [] - (letsubs [assets [:wallet/transferrable-assets-with-amount] - {:keys [on-asset]} [:get-screen-params :wallet-choose-asset]] - [react/keyboard-avoiding-view {:flex 1 :background-color colors/white} - [status-bar/status-bar {:type :modal-white}] - [white-toolbar false "Choose asset" #_(i18n/label :t/wallet-assets)] - [react/view {:style (assoc components.styles/flex :background-color :white)} - [list/flat-list {:default-separator? false ;true - :data assets - :key-fn (comp str :symbol) - :render-fn #(do - [react/touchable-highlight {:on-press - (fn [] - (on-asset %)) - :underlay-color colors/black-transparent} - (render-token-item %)])}]]])) - -(defn show-current-asset [{:keys [name icon decimals amount] :as token}] - [react/view {:style {:flex-direction :row, - :justify-content :center - :padding-horizontal 21 - :padding-vertical 12}} - [list/item-image icon] - [react/view {:margin-horizontal 9 - :flex 1} - [list/item-content - [react/text {:style {:margin-right 10, - :font-weight "500" - :font-size 15 - :color colors/white}} name] - [react/text {:style {:font-size 14 - :padding-top 4 - :color "rgba(255,255,255,0.4)"} - :ellipsize-mode :middle - :number-of-lines 1} - (str (wallet.utils/format-amount amount decimals) - " " - (wallet.utils/display-symbol token))]]] - list/item-icon-forward]) - -;; TODOs -;; consistent input validation throughout looking at wallet.db/parse-amount -;; handle incoming error text :amount-error ?? -;; consider :amount-text -;; use incoming gas-price -;; look at how callers are invoking send-transaction status-im.chat.commands.impl.transactions -;; look at what happens to gas-price on token change? Nothing I suspect -;; look at initial network fees - -(defn fetch-token [all-tokens network token-symbol] - {:pre [(map? all-tokens) (map? network)]} - (when (keyword? token-symbol) - (tokens/asset-for all-tokens - (ethereum/network->chain-keyword network) - token-symbol))) - -(defn create-initial-state [{:keys [symbol decimals] :as token} amount] - {:input-amount (when amount - (when-let [amount' (money/internal->formatted amount symbol decimals)] - (str amount'))) - :inverted false - :edit-gas false - :error-message nil}) - -(defn toggle-edit-gas [state] - (swap! state update :edit-gas not)) - -(defn input-currency-symbol [{:keys [inverted] :as state} {:keys [symbol] :as token} {:keys [code] :as fiat-currency}] - {:pre [(boolean? inverted) (keyword? symbol) (string? code)]} - (if-not (:inverted state) (name (:symbol token)) code)) - -(defn converted-currency-symbol [{:keys [inverted] :as state} {:keys [symbol] :as token} {:keys [code] :as fiat-currency}] - {:pre [(boolean? inverted) (keyword? symbol) (string? code)]} - (if (:inverted state) (name (:symbol token)) code)) - -(defn token->fiat-conversion [prices token fiat-currency value] - {:pre [(map? prices) (map? token) (map? fiat-currency) value]} - (when-let [price (get-in prices [(:symbol token) - (-> fiat-currency :code keyword) - :price])] - (some-> value - money/bignumber - (money/crypto->fiat price)))) - -(defn fiat->token-conversion [prices token fiat-currency value] - {:pre [(map? prices) (map? token) (map? fiat-currency) value]} - (when-let [price (get-in prices [(:symbol token) - (-> fiat-currency :code keyword) - :price])] - (some-> value - money/bignumber - (.div (money/bignumber price))))) - -(defn valid-input-amount? [input-amount] - (and (not (string/blank? input-amount)) - ;; we are ignoring precision for this case - (not (:error (wallet.db/parse-amount input-amount 100))))) - -(defn converted-currency-amount [{:keys [input-amount inverted]} token fiat-currency prices] - (when (valid-input-amount? input-amount) - (if-not inverted - (some-> (token->fiat-conversion prices token fiat-currency input-amount) - (money/with-precision 2)) - (some-> (fiat->token-conversion prices token fiat-currency input-amount) - (money/with-precision 8))))) - -(defn converted-currency-phrase [state token fiat-currency prices] - (str (if-let [amount-bn (converted-currency-amount state token fiat-currency prices)] - (str amount-bn) - "0") - " " (converted-currency-symbol state token fiat-currency))) - -(defn current-token-input-amount [{:keys [input-amount inverted] :as state} token fiat-currency prices] - {:pre [(map? state) (map? token) (map? fiat-currency) (map? prices)]} - (when input-amount - (when-let [amount-bn (if inverted - (fiat->token-conversion prices token fiat-currency input-amount) - (money/bignumber input-amount))] - amount-bn - (money/formatted->internal amount-bn (:symbol token) (:decimals token))))) - -(defn update-input-errors [{:keys [input-amount inverted] :as state} token fiat-currency prices] - {:pre [(map? state) (map? token) (map? fiat-currency) (map? prices)]} - (let [{:keys [value error]} - (wallet.db/parse-amount input-amount - (if inverted 2 (:decimals token)))] - (if-let [error-msg - (cond - error error - (not (money/sufficient-funds? (current-token-input-amount state token fiat-currency prices) - (:amount token))) - "Insufficient funds" - :else nil)] - (assoc state :error-message error-msg) - state))) - -(defn update-input-amount [state input-str token fiat-currency prices] - {:pre [(map? state) (map? token) (map? fiat-currency) (map? prices)]} - (cond-> (-> state - (assoc :input-amount input-str) - (dissoc :error-message)) - (not (string/blank? input-str)) - (update-input-errors token fiat-currency prices))) - -(defn max-fee [{:keys [gas gas-price]}] - {:pre [gas gas-price]} - (money/wei->ether (.times gas gas-price))) - -(defn network-fees [prices token fiat-currency gas-ether-price] - (some-> (token->fiat-conversion prices token fiat-currency gas-ether-price) - (money/with-precision 2))) - -(defn fetch-optimal-gas [symbol cb] - (ethereum/gas-price - (:web3 @re-frame.db/app-db) - (fn [_ gas-price] - (when gas-price - (cb {:optimal-gas (ethereum/estimate-gas symbol) - :optimal-gas-price gas-price}))))) - -(defn optimal-gas-present? [{:keys [optimal-gas optimal-gas-price] :as transaction}] - (and optimal-gas optimal-gas-price)) - -(defn current-gas [{:keys [gas gas-price optimal-gas optimal-gas-price] :as transaction}] - {:gas (or gas optimal-gas) :gas-price (or gas-price optimal-gas-price)}) - -;; TODO derived state - -;; !!! only send gas and gas-price in a transaction if they are custom gas prices!!! -(defn choose-amount-token-helper [{:keys [balance network prices fiat-currency - native-currency - all-tokens - modal? - contact - transaction]}] - {:pre [(map? native-currency)]} - (let [tx-atom (reagent/atom transaction) - token (or (fetch-token all-tokens network (:symbol transaction)) - native-currency) - state-atom (reagent/atom (create-initial-state token (:amount transaction))) - network-fees-modal-ref (atom nil) - open-network-fees! #(anim-ref-send @network-fees-modal-ref :open!) - close-network-fees! #(anim-ref-send @network-fees-modal-ref :close!)] - ;; initialize the starting gas price - (when-not (optimal-gas-present? transaction) - (fetch-optimal-gas - (some :symbol [transaction native-currency]) - #(swap! tx-atom merge %))) - (fn [{:keys [balance network prices fiat-currency - native-currency all-tokens modal?]}] - (let [symbol (some :symbol [@tx-atom native-currency]) - token (-> (tokens/asset-for all-tokens (ethereum/network->chain-keyword network) symbol) - (assoc :amount (get balance symbol (money/bignumber 0)))) - gas-gas-price->fiat - (fn [gas-map] - (network-fees prices token fiat-currency (max-fee gas-map)))] - [wallet.components/simple-screen {:avoid-keyboard? (not modal?) - :status-bar-type (if modal? :modal-wallet :wallet)} - [toolbar modal? "Send amount"] - (if (empty? balance) - (info-page "You don't have any assets yet") - (let [{:keys [error-message input-amount] :as state} @state-atom - input-symbol (input-currency-symbol state token fiat-currency) - converted-phrase (converted-currency-phrase state token fiat-currency prices)] - [react/view {:flex 1} - ;; network fees modal - (when (optimal-gas-present? @tx-atom) - [slide-up-modal {:anim-ref #(reset! network-fees-modal-ref %) - :swipe-dismiss? true} - [custom-gas-input-panel - (-> (select-keys @tx-atom [:gas :gas-price :optimal-gas :optimal-gas-price]) - (assoc - :fiat-currency fiat-currency - :gas-gas-price->fiat gas-gas-price->fiat - :on-submit (fn [{:keys [gas gas-price]}] - (when (and gas gas-price) - (swap! tx-atom assoc :gas gas :gas-price gas-price)) - (close-network-fees!))))]]) - [react/touchable-highlight {:style {:background-color colors/black-transparent} - :on-press #(re-frame/dispatch - [:navigate-to - :wallet-choose-asset - {:on-asset (fn [{:keys [symbol]}] - (when symbol - (swap! tx-atom assoc :symbol symbol)) - (re-frame/dispatch [:navigate-back]))}]) - :underlay-color colors/white-transparent} - [show-current-asset token]] - [react/view {:flex 1} - [react/view {:flex 1}] - [react/view {:justify-content :center - :align-items :flex-end - :flex-direction :row} - (when error-message - [tooltip/tooltip error-message {:color colors/white - :font-size 12 - :bottom-value 15}]) - [react/text-input - {:on-change-text #(swap! state-atom update-input-amount % token fiat-currency prices) - :keyboard-type :numeric - :accessibility-label :amount-input - :auto-focus true - :auto-capitalize :none - :auto-correct false - :placeholder "0" - :placeholder-text-color "rgb(143,162,234)" - :multiline true - :max-length 42 - :value input-amount - :selection-color colors/green - :style {:color colors/white - :font-size 30 - :font-weight :bold - :padding-horizontal 10 - :max-width 290}}] - [react/text {:style {:color (if (not (string/blank? input-amount)) - colors/white - "rgb(143,162,234)") - :font-size 30 - :font-weight :bold}} - input-symbol]] - [react/view {} - [react/text {:style {:text-align :center - :margin-top 16 - :font-size 15 - :line-height 22 - :color "rgb(143,162,234)"}} - converted-phrase]] - [react/view {:justify-content :center :flex-direction :row} - [react/touchable-highlight {:on-press open-network-fees! - :style {:background-color colors/black-transparent - :padding-horizontal 13 - :padding-vertical 7 - :margin-top 1 - :border-radius 8 - :opacity (if (valid-input-amount? input-amount) 1 0)}} - [react/text {:style {:color colors/white - :font-size 15 - :line-height 22}} - - (str "network fee ~ " - (when (optimal-gas-present? @tx-atom) - (gas-gas-price->fiat (current-gas @tx-atom))) - " " - (:code fiat-currency))]]] - [react/view {:flex 1}] - - [react/view {:flex-direction :row :padding 3} - [address-button {:underlay-color colors/white-transparent - :background-color colors/black-transparent - :on-press #(swap! state-atom update :inverted not)} - [react/view {:flex-direction :row} - [react/text {:style {:color colors/white - :font-size 15 - :line-height 22 - :padding-right 10}} - (:code fiat-currency)] - [vector-icons/icon :icons/change {:color colors/white-transparent}] - [react/text {:style {:color colors/white - :font-size 15 - :line-height 22 - :padding-left 11}} - (name symbol)]]] - (let [disabled? (string/blank? input-amount)] - [address-button {:disabled? disabled? - :underlay-color colors/black-transparent - :background-color (if disabled? colors/blue colors/white) - :token token - :on-press - #(re-frame/dispatch [:navigate-to - :wallet-txn-overview - {:modal? modal? - :contact contact - :transaction - (assoc - @tx-atom - :amount - (money/formatted->internal - (money/bignumber input-amount) - (:symbol token) - (:decimals token)))}])} - [react/text {:style {:color (if disabled? colors/white colors/blue) - :font-size 15 - :line-height 22}} - (i18n/label :t/next)]])]]]))])))) + [reagent.core :as reagent])) (defview choose-amount-token [] (letsubs [{:keys [transaction modal? contact native-currency]} [:get-screen-params :wallet-choose-amount] - balance [:balance] - prices [:prices] - network [:account/network] - all-tokens [:wallet/all-tokens] - fiat-currency [:wallet/currency]] - [choose-amount-token-helper {:balance balance - :network network - :all-tokens all-tokens - :modal? modal? - :prices prices - :native-currency native-currency - :fiat-currency fiat-currency - :contact contact - :transaction transaction}])) - -;; ---------------------------------------------------------------------- -;; Step 3 Final Overview -;; ---------------------------------------------------------------------- - -;; TODOS -;; wire in final send transation -;; look at duplicate logic and create a model so that we can simply execute methods against that model -;; instead of peicing together various information for each individual computation - -(defn transaction-overview [{:keys [modal? transaction contact token native-currency - fiat-currency prices] :as data}] - (let [tx-atom (atom transaction) - network-fees-modal-ref (atom nil) - open-network-fees! #(anim-ref-send @network-fees-modal-ref :open!) - close-network-fees! #(anim-ref-send @network-fees-modal-ref :close!)] - (when-not (optimal-gas-present? transaction) - (fetch-optimal-gas - (some :symbol [transaction native-currency]) - #(swap! tx-atom merge %))) - (fn [] - (let [transaction @tx-atom - gas-gas-price->fiat - (fn [gas-map] - (network-fees prices token fiat-currency (max-fee gas-map))) - - network-fee-eth (max-fee (current-gas transaction)) - network-fee-fiat - (when (optimal-gas-present? transaction) - (network-fees prices token fiat-currency network-fee-eth)) - - formatted-amount - (money/internal->formatted (:amount transaction) - (:symbol token) - (:decimals token)) - amount-str (str formatted-amount - " " (name (:symbol token))) - - fiat-amount (some-> (token->fiat-conversion prices token fiat-currency formatted-amount) - (money/with-precision 2)) - - total-eth (some-> (.add (money/bignumber formatted-amount) network-fee-eth) - (money/with-precision 11)) - - total-fiat (some-> (token->fiat-conversion prices token fiat-currency total-eth) - (money/with-precision 2)) - - #_fee-fiat-amount - #_(some-> (token->fiat-conversion prices token fiat-currency formated-amount) - (money/with-precision 2))] - [wallet.components/simple-screen {:avoid-keyboard? (not modal?) - :status-bar-type (if modal? :modal-wallet :wallet)} - [toolbar modal? "Send amount"] - [react/view {:style {:flex 1 - :border-top-width 1 - :border-top-color colors/white-light-transparent}} - (when (optimal-gas-present? @tx-atom) - [slide-up-modal {:anim-ref #(reset! network-fees-modal-ref %) - :swipe-dismiss? true} - [custom-gas-input-panel - (-> (select-keys @tx-atom [:gas :gas-price :optimal-gas :optimal-gas-price]) - (assoc - :fiat-currency fiat-currency - :gas-gas-price->fiat gas-gas-price->fiat - :on-submit (fn [{:keys [gas gas-price]}] - (when (and gas gas-price) - (swap! tx-atom assoc :gas gas :gas-price gas-price)) - (close-network-fees!))))]]) - [react/text {:style {:margin-top 18 - :text-align :center - :font-size 15 - :color colors/white-transparent}} - "Recipient"] - [react/view - (when contact - [react/view {:style {:margin-top 10 - :flex-direction :row - :justify-content :center}} - [photos/photo (:photo-path contact) {:size list.styles/image-size}]]) - [react/text {:style {:color colors/white - :margin-horizontal 24 - :margin-top 10 - :line-height 22 - :font-size 15 - :text-align :center}} - (ethereum/normalized-address (:to transaction))]] - [react/text {:style {:margin-top 18 - :font-size 15 - :text-align :center - :color colors/white-transparent}} - "Amount"] - [react/view {:style {:flex-direction :row - :align-items :center - :margin-top 10 - :margin-horizontal 24}} - [react/text {:style {:color colors/white - :font-size 15}} "Sending"] - [react/view {:style {:flex 1}} - [react/text {:style {:color colors/white - :line-height 21 - :font-size 15 - :font-weight "500" - :text-align :right}} - amount-str] - [react/text {:style {:color colors/white-transparent - :line-height 21 - :font-size 15 - :text-align :right}} - (str "~ " (:symbol fiat-currency "$") fiat-amount " " (:code fiat-currency))]]] - [react/view {:style {:margin-horizontal 24 - :margin-top 10 - :padding-top 10 - :border-top-width 1 - :border-top-color colors/white-light-transparent}} - [react/view {:style {:flex-direction :row - :align-items :center}} - [react/touchable-highlight {:on-press #(open-network-fees!) - :style {:background-color colors/black-transparent - :padding-horizontal 16 - :padding-vertical 9 - :border-radius 8}} - [react/view {:style {:flex-direction :row - :align-items :center}} - [react/text {:style {:color colors/white - :padding-right 10 - :font-size 15}} "Network fee"] - [vector-icons/icon :icons/settings {:color colors/white}]]] - [react/view {:style {:flex 1}} - [react/text {:style {:color colors/white - :line-height 21 - :font-size 15 - :font-weight "500" - :text-align :right}} - (str network-fee-eth " " (name (:symbol native-currency)))] - [react/text {:style {:color colors/white-transparent - :line-height 21 - :font-size 15 - :text-align :right}} - (str "~ " network-fee-fiat " " (:code fiat-currency))]]]] - [react/view {:style {:background-color colors/white - :border-top-left-radius 8 - :border-top-right-radius 8 - :position :absolute - :left 0 - :right 0 - :bottom 0}} - [react/text {:style {:color colors/black - :font-size 15 - :line-height 22 - :margin-top 23 - :text-align :center}} - "Total"] - [react/text {:style {:color colors/black - :margin-top 4 - :font-weight :bold - :font-size 22 - :line-height 28 - :text-align :center}} - (str total-eth " " (name (:symbol native-currency)))] - [react/text {:style {:color colors/gray - :text-align :center - :margin-top 3 - :line-height 21 - :font-size 15}} - (str "~ " (:symbol fiat-currency "$") total-fiat)] - [react/view {:style {:flex-direction :row - :justify-content :center - :padding-top 16 - :padding-bottom 24}} - [react/touchable-highlight - {:on-press (fn [] #_TODO) - :style {:padding-horizontal 39 - :padding-vertical 12 - :border-radius 8 - :background-color colors/blue-light}} - [react/text {:style {:font-size 15 - :line-height 22 - :color colors/blue}} - "Confirm"]]]] - - #_[react/text "Here we are"]]])))) - -(defview txn-overview [] - (letsubs [{:keys [transaction modal? contact]} - [:get-screen-params :wallet-txn-overview] - balance [:balance] - prices [:prices] - network [:account/network] - all-tokens [:wallet/all-tokens] + balance [:balance] + prices [:prices] + network [:account/network] + all-tokens [:wallet/all-tokens] fiat-currency [:wallet/currency] - chain (ethereum/network->chain-keyword network) - native-currency (tokens/native-currency chain)] - ;; TODO look up contact don't pass it forward - (let [token (tokens/asset-for - all-tokens - (ethereum/network->chain-keyword network) (:symbol symbol))] - [transaction-overview {:transaction transaction - :modal? modal? - :contact contact - :prices prices - :network network - :token token - :native-currency native-currency - :fiat-currency fiat-currency}]))) + web3 [:get :web3]] + (let [chain (ethereum/network->chain-keyword network)] + [amount/render-choose-amount {:web3 web3 + :balance balance + :network network + :chain chain + :all-tokens all-tokens + :modal? modal? + :prices prices + :native-currency native-currency + :fiat-currency fiat-currency + :contact contact + :transaction transaction}]))) + +(defview transaction-overview [] + (letsubs [{:keys [transaction flow contact]} [:get-screen-params :wallet-txn-overview] + prices [:prices] + network [:account/network] + all-tokens [:wallet/all-tokens] + fiat-currency [:wallet/currency] + web3 [:get :web3]] + (let [chain (ethereum/network->chain-keyword network) + native-currency (tokens/native-currency chain) + token (tokens/asset-for all-tokens + (ethereum/network->chain-keyword network) (:symbol transaction))] + [overview/render-transaction-overview {:transaction transaction + :flow flow + :contact contact + :prices prices + :network network + :token token + :native-currency native-currency + :fiat-currency fiat-currency + :all-tokens all-tokens + :chain chain + :web3 web3}]))) ;; MAIN SEND TRANSACTION VIEW -(defn- send-transaction-view [{:keys [scroll] :as opts}] - (let [amount-input (atom nil) - handler (fn [_] - (when (and scroll @scroll @amount-input - (.isFocused @amount-input)) - (log/debug "Amount field focused, scrolling down") - (.scrollToEnd @scroll)))] - (reagent/create-class - {:component-will-mount (fn [_] - ;;NOTE(goranjovic): keyboardDidShow is for android and keyboardWillShow for ios - (.addListener react/keyboard "keyboardDidShow" handler) - (.addListener react/keyboard "keyboardWillShow" handler)) - :reagent-render (fn [opts] [choose-address-contact (assoc opts :amount-input amount-input)])}))) +(defn- send-transaction-view [_opts] + (reagent/create-class + {:reagent-render (fn [opts] [recipient/render-choose-recipient opts])})) ;; SEND TRANSACTION FROM WALLET (CHAT) (defview send-transaction [] (letsubs [transaction [:wallet.send/transaction] - advanced? [:wallet.send/advanced?] network [:account/network] - scroll (atom nil) network-status [:network-status] all-tokens [:wallet/all-tokens] - contacts [:contacts/all-added-people-contacts]] + contacts [:contacts/active] + web3 [:get :web3]] [send-transaction-view {:modal? false - ;; TODO only send gas and gas-price when they are custom :transaction (dissoc transaction :gas :gas-price) - :scroll scroll - :advanced? advanced? + :web3 web3 :network network :all-tokens all-tokens :contacts contacts @@ -1296,141 +74,24 @@ Example: ;; SEND TRANSACTION FROM DAPP (defview send-transaction-modal [] - (letsubs [transaction [:wallet.send/transaction] - advanced? [:wallet.send/advanced?] - network [:account/network] - scroll (atom nil) - network-status [:network-status] - all-tokens [:wallet/all-tokens]] - (if transaction - [send-transaction-view {:modal? true - :transaction transaction - :scroll scroll - :advanced? advanced? - :network network - :all-tokens all-tokens - :network-status network-status}] - [react/view wallet.styles/wallet-modal-container - [react/view components.styles/flex - [status-bar/status-bar {:type :modal-wallet}] - [toolbar true (i18n/label :t/send-transaction)] - [react/i18n-text {:style styles/empty-text - :key :unsigned-transaction-expired}]]]))) - -(defview password-input-panel [message-label spinning?] - (letsubs [account [:account/account] - wrong-password? [:wallet.send/wrong-password?] - signing-phrase (:signing-phrase @account) - bottom-value (animation/create-value -250) - opacity-value (animation/create-value 0)] - {:component-did-mount #(send.animations/animate-sign-panel opacity-value bottom-value)} - [react/animated-view {:style (styles/animated-sign-panel bottom-value)} - (when wrong-password? - [tooltip/tooltip (i18n/label :t/wrong-password) styles/password-error-tooltip]) - [react/animated-view {:style (styles/sign-panel opacity-value)} - [react/view styles/spinner-container - (when spinning? - [react/activity-indicator {:animating true - :size :large}])] - [react/view styles/signing-phrase-container - [react/text {:style styles/signing-phrase - :accessibility-label :signing-phrase-text} - signing-phrase]] - [react/i18n-text {:style styles/signing-phrase-description :key message-label}] - [react/view {:style styles/password-container - :important-for-accessibility :no-hide-descendants} - [react/text-input - {:auto-focus true - :secure-text-entry true - :placeholder (i18n/label :t/enter-password) - :placeholder-text-color colors/gray - :on-change-text #(re-frame/dispatch [:wallet.send/set-password (security/mask-data %)]) - :style styles/password - :accessibility-label :enter-password-input - :auto-capitalize :none}]]]])) - -;; "Cancel" and "Sign Transaction >" or "Sign >" buttons, signing with password -(defview enter-password-buttons [spinning? cancel-handler sign-handler sign-label] - (letsubs [sign-enabled? [:wallet.send/sign-password-enabled?] - network-status [:network-status]] - [bottom-buttons/bottom-buttons - styles/sign-buttons - [button/button {:style components.styles/flex - :on-press cancel-handler - :accessibility-label :cancel-button} - (i18n/label :t/cancel)] - [button/button {:style (wallet.styles/button-container sign-enabled?) - :on-press sign-handler - :disabled? (or spinning? - (not sign-enabled?) - (= :offline network-status)) - :accessibility-label :sign-transaction-button} - (i18n/label sign-label) - [vector-icons/icon :icons/forward {:color colors/white}]]])) - -;; "Sign Transaction >" button -(defn- sign-transaction-button [amount-error to amount sufficient-funds? sufficient-gas? modal? online?] - (let [sign-enabled? (and (nil? amount-error) - (or modal? (not (empty? to))) ;;NOTE(goranjovic) - contract creation will have empty `to` - (not (nil? amount)) - sufficient-funds? - sufficient-gas? - online?)] - [bottom-buttons/bottom-buttons - styles/sign-buttons - [react/view] - [button/button {:style components.styles/flex - :disabled? (not sign-enabled?) - :on-press #(re-frame/dispatch [:set-in - [:wallet :send-transaction :show-password-input?] - true]) - :text-style {:color :white} - :accessibility-label :sign-transaction-button} - (i18n/label :t/transactions-sign-transaction) - [vector-icons/icon :icons/forward {:color (if sign-enabled? colors/white colors/white-light-transparent)}]]])) - -(defn- render-send-transaction-view [{:keys [modal? transaction scroll advanced? network all-tokens amount-input network-status]}] - (let [{:keys [amount amount-text amount-error asset-error show-password-input? to to-name sufficient-funds? - sufficient-gas? in-progress? from-chat? symbol]} transaction - chain (ethereum/network->chain-keyword network) - native-currency (tokens/native-currency chain) - {:keys [decimals] :as token} (tokens/asset-for all-tokens chain symbol) - online? (= :online network-status)] - [wallet.components/simple-screen {:avoid-keyboard? (not modal?) - :status-bar-type (if modal? :modal-wallet :wallet)} - [toolbar modal? (i18n/label :t/send-transaction)] - [react/view components.styles/flex - [common/network-info {:text-color :white}] - [react/scroll-view {:keyboard-should-persist-taps :always - :ref #(reset! scroll %) - :on-content-size-change #(when (and (not modal?) scroll @scroll) - (.scrollToEnd @scroll))} - (when-not online? - [wallet.main.views/snackbar :t/error-cant-send-transaction-offline]) - [react/view styles/send-transaction-form - [components/recipient-selector {:disabled? (or from-chat? modal?) - :address to - :name to-name - :modal? modal?}] - [components/asset-selector {:disabled? (or from-chat? modal?) - :error asset-error - :type :send - :symbol symbol}] - [components/amount-selector {:disabled? (or from-chat? modal?) - :error (or amount-error - (when-not sufficient-funds? (i18n/label :t/wallet-insufficient-funds)) - (when-not sufficient-gas? (i18n/label :t/wallet-insufficient-gas))) - :amount amount - :amount-text amount-text - :input-options {:on-change-text #(re-frame/dispatch [:wallet.send/set-and-validate-amount % symbol decimals]) - :ref (partial reset! amount-input)}} token] - [advanced-options advanced? native-currency transaction scroll]]] - (if show-password-input? - [enter-password-buttons in-progress? - #(re-frame/dispatch [:wallet/cancel-entering-password]) - #(re-frame/dispatch [:wallet/send-transaction]) - :t/transactions-sign-transaction] - [sign-transaction-button amount-error to amount sufficient-funds? sufficient-gas? modal? online?]) - (when show-password-input? - [password-input-panel :t/signing-phrase-description in-progress?]) - (when in-progress? [react/view styles/processing-view])]])) + (letsubs [{:keys [transaction flow contact]} [:get-screen-params :wallet-send-transaction-modal] + prices [:prices] + network [:account/network] + all-tokens [:wallet/all-tokens] + web3 [:get :web3] + fiat-currency [:wallet/currency]] + (let [chain (ethereum/network->chain-keyword network) + native-currency (tokens/native-currency chain) + token (tokens/asset-for all-tokens + (ethereum/network->chain-keyword network) (:symbol transaction))] + [overview/render-transaction-overview {:transaction transaction + :flow flow + :contact contact + :prices prices + :network network + :token token + :web3 web3 + :native-currency native-currency + :fiat-currency fiat-currency + :all-tokens all-tokens + :chain chain}]))) diff --git a/src/status_im/ui/screens/wallet/send/views/amount.cljs b/src/status_im/ui/screens/wallet/send/views/amount.cljs new file mode 100644 index 00000000000..3706143226f --- /dev/null +++ b/src/status_im/ui/screens/wallet/send/views/amount.cljs @@ -0,0 +1,316 @@ +(ns status-im.ui.screens.wallet.send.views.amount + (:require-macros [status-im.utils.views :refer [defview letsubs]]) + (:require [status-im.i18n :as i18n] + [status-im.ui.components.colors :as colors] + [status-im.ui.components.react :as react] + [status-im.ui.screens.wallet.send.views.common :as common] + [re-frame.core :as re-frame] + [status-im.utils.money :as money] + [status-im.ui.screens.wallet.utils :as wallet.utils] + [clojure.string :as string] + [status-im.ui.components.icons.vector-icons :as vector-icons] + [status-im.ui.screens.wallet.components.views :as wallet.components] + [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.ethereum.tokens :as tokens] + [reagent.core :as reagent] + [status-im.ui.screens.wallet.db :as wallet.db] + [status-im.ui.components.list.views :as list] + [status-im.ui.components.tooltip.views :as tooltip] + [status-im.ui.components.toolbar.actions :as actions] + [status-im.ui.components.toolbar.view :as toolbar] + [status-im.ui.components.status-bar.view :as status-bar] + [status-im.ui.components.styles :as components.styles])) + +(defn white-toolbar [modal? title] + (let [action (if modal? actions/close actions/back)] + [toolbar/toolbar {:style {:background-color colors/white + :border-bottom-width 1 + :border-bottom-color colors/black-transparent}} + [toolbar/nav-button (action (if modal? + #(re-frame/dispatch [:wallet/discard-transaction-navigate-back]) + #(actions/default-handler)))] + [toolbar/content-title {:color colors/black + :font-size 17 + :font-weight :bold} title]])) + +(defn- render-token-item [{:keys [name icon decimals amount] :as coin}] + [list/item + [list/item-image icon] + [list/item-content + [react/text {:style {:margin-right 10, :color colors/black}} name] + [list/item-secondary (str (wallet.utils/format-amount amount decimals) + " " + (wallet.utils/display-symbol coin))]]]) + +(defview choose-asset [] + (letsubs [assets [:wallet/transferrable-assets-with-amount] + {:keys [on-asset]} [:get-screen-params :wallet-choose-asset]] + [wallet.components/simple-screen {:avoid-keyboard? true + :status-bar-type :main} + [white-toolbar false (i18n/label :t/choose-asset)] + [react/view {:style (assoc components.styles/flex :background-color :white)} + [list/flat-list {:default-separator? false ;true + :data assets + :key-fn (comp str :symbol) + :render-fn #(do + [react/touchable-highlight {:on-press (fn [] (on-asset %)) + :underlay-color colors/black-transparent} + (render-token-item %)])}]]])) + +(defn show-current-asset [{:keys [name icon decimals amount] :as token}] + [react/view {:style {:flex-direction :row, + :justify-content :center + :padding-horizontal 21 + :padding-vertical 12}} + [list/item-image icon] + [react/view {:margin-horizontal 9 + :flex 1} + [list/item-content + [react/text {:style {:margin-right 10, + :font-weight "500" + :font-size 15 + :color colors/white}} name] + [react/text {:style {:font-size 14 + :padding-top 4 + :color colors/white-transparent} + :ellipsize-mode :middle + :number-of-lines 1} + (str (wallet.utils/format-amount amount decimals) + " " + (wallet.utils/display-symbol token))]]] + list/item-icon-forward]) + +(defn fetch-token [all-tokens network token-symbol] + {:pre [(map? all-tokens) (map? network)]} + (when (keyword? token-symbol) + (tokens/asset-for all-tokens + (ethereum/network->chain-keyword network) + token-symbol))) + +(defn create-initial-state [{:keys [symbol decimals]} amount] + {:input-amount (when amount + (when-let [amount' (money/internal->formatted amount symbol decimals)] + (str amount'))) + :inverted false + :edit-gas false + :error-message nil}) + +(defn input-currency-symbol [{:keys [inverted] :as state} {:keys [symbol] :as coin} {:keys [code]}] + {:pre [(boolean? inverted) (keyword? symbol) (string? code)]} + (if-not (:inverted state) (wallet.utils/display-symbol coin) code)) + +(defn converted-currency-symbol [{:keys [inverted] :as state} {:keys [symbol] :as coin} {:keys [code]}] + {:pre [(boolean? inverted) (keyword? symbol) (string? code)]} + (if (:inverted state) (wallet.utils/display-symbol coin) code)) + +(defn valid-input-amount? [input-amount] + (and (not (string/blank? input-amount)) + (not (:error (wallet.db/parse-amount input-amount 100))))) + +(defn converted-currency-amount [{:keys [input-amount inverted]} token fiat-currency prices] + (when (valid-input-amount? input-amount) + (if-not inverted + (some-> (common/token->fiat-conversion prices token fiat-currency input-amount) + (money/with-precision 2)) + (some-> (common/fiat->token-conversion prices token fiat-currency input-amount) + (money/with-precision 8))))) + +(defn converted-currency-phrase [state token fiat-currency prices] + (str (if-let [amount-bn (converted-currency-amount state token fiat-currency prices)] + (str amount-bn) + "0") + " " (converted-currency-symbol state token fiat-currency))) + +(defn current-token-input-amount [{:keys [input-amount inverted] :as state} token fiat-currency prices] + {:pre [(map? state) (map? token) (map? fiat-currency) (map? prices)]} + (when input-amount + (when-let [amount-bn (if inverted + (common/fiat->token-conversion prices token fiat-currency input-amount) + (money/bignumber input-amount))] + amount-bn + (money/formatted->internal amount-bn (:symbol token) (:decimals token))))) + +(defn update-input-errors [{:keys [input-amount inverted] :as state} token fiat-currency prices] + {:pre [(map? state) (map? token) (map? fiat-currency) (map? prices)]} + (let [{:keys [_value error]} + (wallet.db/parse-amount input-amount + (if inverted 2 (:decimals token)))] + (if-let [error-msg + (cond + error error + (not (money/sufficient-funds? (current-token-input-amount state token fiat-currency prices) + (:amount token))) + (i18n/label :t/wallet-insufficient-funds) + :else nil)] + (assoc state :error-message error-msg) + state))) + +(defn update-input-amount [state input-str token fiat-currency prices] + {:pre [(map? state) (map? token) (map? fiat-currency) (map? prices)]} + (cond-> (-> state + (assoc :input-amount input-str) + (dissoc :error-message)) + (not (string/blank? input-str)) + (update-input-errors token fiat-currency prices))) + +(defn render-choose-amount [{:keys [web3 + chain + network + native-currency + all-tokens + contact + transaction]}] + {:pre [(map? native-currency)]} + (let [tx-atom (reagent/atom transaction) + coin (or (fetch-token all-tokens network (:symbol transaction)) + native-currency) + state-atom (reagent/atom (create-initial-state coin (:amount transaction))) + amount-input-ref (atom nil) + network-fees-modal-ref (atom nil) + open-network-fees! #(do (.blur @amount-input-ref) + (common/anim-ref-send @network-fees-modal-ref :open!)) + close-network-fees! #(common/anim-ref-send @network-fees-modal-ref :close!)] + (when-not (common/optimal-gas-present? transaction) + (common/refresh-optimal-gas web3 tx-atom all-tokens chain)) + (fn [{:keys [balance network prices fiat-currency + native-currency all-tokens modal?]}] + (let [symbol (some :symbol [@tx-atom native-currency]) + coin (-> (tokens/asset-for all-tokens (ethereum/network->chain-keyword network) symbol) + (assoc :amount (get balance symbol (money/bignumber 0)))) + gas-gas-price->fiat + (fn [gas-map] + (common/network-fees prices coin fiat-currency (common/max-fee gas-map))) + update-amount-field #(swap! state-atom update-input-amount % coin fiat-currency prices)] + [wallet.components/simple-screen {:avoid-keyboard? (not modal?) + :status-bar-type (if modal? :modal-wallet :wallet)} + [common/toolbar :wallet (i18n/label :t/send-amount) nil] + (if (empty? balance) + (common/info-page (i18n/label :t/wallet-no-assets-enabled)) + (let [{:keys [error-message input-amount] :as state} @state-atom + input-symbol (input-currency-symbol state coin fiat-currency) + converted-phrase (converted-currency-phrase state coin fiat-currency prices)] + [react/view {:flex 1} + ;; network fees modal + (when (common/optimal-gas-present? @tx-atom) + [common/slide-up-modal {:anim-ref #(reset! network-fees-modal-ref %) + :swipe-dismiss? true} + [common/custom-gas-input-panel + (-> (select-keys @tx-atom [:gas :gas-price :optimal-gas :optimal-gas-price]) + (assoc + :fiat-currency fiat-currency + :gas-gas-price->fiat gas-gas-price->fiat + :on-submit (fn [{:keys [gas gas-price]}] + (when (and gas gas-price) + (swap! tx-atom assoc :gas gas :gas-price gas-price)) + (close-network-fees!))))]]) + [react/touchable-highlight {:style {:background-color colors/black-transparent} + :on-press #(re-frame/dispatch + [:navigate-to :wallet-choose-asset + {:on-asset (fn [{:keys [symbol]}] + (when symbol + (if-not (= symbol (:symbol @tx-atom)) + (update-amount-field nil)) + (swap! tx-atom assoc :symbol symbol) + (common/refresh-optimal-gas web3 tx-atom + all-tokens chain)) + (re-frame/dispatch [:navigate-back]))}]) + :underlay-color colors/white-transparent} + [show-current-asset coin]] + [react/view {:flex 1} + [react/view {:flex 1}] + [react/view {:justify-content :center + :align-items :center + :flex-direction :row} + (when error-message + [tooltip/tooltip error-message {:color colors/white + :font-size 12 + :bottom-value 15}]) + [react/text-input + {:ref #(reset! amount-input-ref %) + :on-change-text update-amount-field + :keyboard-type :numeric + :accessibility-label :amount-input + :auto-focus true + :auto-capitalize :none + :auto-correct false + :placeholder "0" + :placeholder-text-color colors/blue-shadow + :multiline true + :max-length 20 + :default-value input-amount + :selection-color colors/green + :keyboard-appearance :dark + :style {:color colors/white + :font-size 30 + :font-weight "500" + :padding-horizontal 10 + :padding-vertical 7 + :max-width 290 + :text-align-vertical :center}}] + [react/text {:style {:color (if (not (string/blank? input-amount)) + colors/white + colors/blue-shadow) + :font-size 30 + :font-weight "500" + :text-align-vertical :center}} + input-symbol]] + [react/view {} + [react/text {:style {:text-align :center + :margin-top 16 + :font-size 15 + :line-height 22 + :color colors/blue-shadow}} + converted-phrase]] + (when (valid-input-amount? input-amount) + [react/view {:justify-content :center + :flex-direction :row} + [react/touchable-highlight {:on-press open-network-fees! + :style {:background-color colors/black-transparent + :padding-horizontal 13 + :padding-vertical 7 + :margin-top 1 + :border-radius 8}} + [react/text {:style {:color colors/white + :font-size 15 + :line-height 22}} + (i18n/label :t/network-fee-amount {:amount (str (or (gas-gas-price->fiat (common/current-gas @tx-atom)) "0")) + :currency (:code fiat-currency)})]]]) + [react/view {:flex 1}] + + [react/view {:flex-direction :row + :padding 3} + [common/action-button {:underlay-color (colors/alpha colors/black 0.2) + :background-color colors/black-transparent + :on-press #(swap! state-atom update :inverted not)} + [react/view {:flex-direction :row} + [react/text {:style {:color colors/white + :font-size 15 + :line-height 22 + :padding-right 10}} + (:code fiat-currency)] + [vector-icons/icon :main-icons/change {:color colors/white-transparent}] + [react/text {:style {:color colors/white + :font-size 15 + :line-height 22 + :padding-left 11}} + (wallet.utils/display-symbol coin)]]] + (let [disabled? (or (string/blank? input-amount) + (not (empty? (:error-message @state-atom))))] + [common/action-button {:disabled? disabled? + :underlay-color colors/black-transparent + :background-color (if disabled? colors/blue colors/white) + :token coin + :on-press #(re-frame/dispatch [:navigate-to :wallet-txn-overview + {:modal? modal? + :contact contact + :transaction (assoc @tx-atom + :amount (money/formatted->internal + (money/bignumber input-amount) + (:symbol coin) + (:decimals coin)))}])} + [react/text {:style {:color (if disabled? + (colors/alpha colors/white 0.3) + colors/blue) + :font-size 15 + :line-height 22}} + (i18n/label :t/next)]])]]]))])))) diff --git a/src/status_im/ui/screens/wallet/send/views/common.cljs b/src/status_im/ui/screens/wallet/send/views/common.cljs new file mode 100644 index 00000000000..466737292b0 --- /dev/null +++ b/src/status_im/ui/screens/wallet/send/views/common.cljs @@ -0,0 +1,459 @@ +(ns status-im.ui.screens.wallet.send.views.common + (:require [status-im.ui.components.toolbar.actions :as actions] + [status-im.ui.components.toolbar.view :as toolbar] + [status-im.ui.screens.wallet.styles :as wallet.styles] + [re-frame.core :as re-frame] + [reagent.core :as reagent] + [status-im.ui.components.react :as react] + [status-im.ui.components.colors :as colors] + [status-im.ui.components.icons.vector-icons :as vector-icons] + [status-im.ui.components.animation :as animation] + [status-im.utils.money :as money] + [status-im.i18n :as i18n] + [clojure.string :as string] + [status-im.ui.components.tooltip.views :as tooltip] + [status-im.ui.screens.wallet.send.events :as events] + [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.dimensions :as dimensions] + [taoensso.timbre :as log] + [status-im.utils.ethereum.tokens :as tokens])) + +(defn toolbar [flow title chat-id] + (let [action (if (#{:chat :dapp} flow) actions/close-white actions/back-white)] + [toolbar/toolbar {:style wallet.styles/toolbar} + [toolbar/nav-button (action (if (= :chat flow) + #(re-frame/dispatch [:chat.ui/navigate-to-chat chat-id {}]) + #(actions/default-handler)))] + [toolbar/content-title {:color :white :font-weight :bold :font-size 17} title]])) + +(defn action-button [{:keys [disabled? on-press underlay-color background-color]} content] + [react/touchable-highlight {:underlay-color underlay-color + :disabled disabled? + :on-press on-press + :style {:height 44 + :background-color background-color + :border-radius 8 + :flex 1 + :align-items :center + :justify-content :center + :margin 3}} + content]) + +(defn info-page [message] + [react/view {:style {:flex 1 + :align-items :center + :justify-content :center + :background-color colors/blue}} + [vector-icons/icon :main-icons/info {:color colors/white}] + [react/text {:style {:max-width 144 + :margin-top 15 + :color colors/white + :font-size 15 + :text-align :center + :line-height 22}} + message]]) + +;; worthy abstraction +(defn anim-ref-send + "Call one of the methods in an animation ref. + Takes an animation ref (a map of keys to animation tiggering methods) + a keyword that should equal one of the keys in the map and optional args to be sent to the animation. + + Example: + (anim-ref-send slider-ref :open!) + (anim-ref-send slider-ref :move-top-left! 25 25)" + [anim-ref signal & args] + (when anim-ref + (assert (get anim-ref signal) + (str "Key " signal " was not found in animation ref. Should be in " + (pr-str (keys anim-ref))))) + (some-> anim-ref (get signal) (apply args))) + +(defn slide-up-modal + "Creates a modal that slides up from the bottom of the screen and + responds to a swipe down gesture to dismiss + + The modal initially renders in the closed position. + + It takes an options map and the react child to be displayed in the + modal. + + Options: + :anim-ref - takes a function that will be called with a map of + animation methods. + :swipe-dismiss? - a boolean that determines whether the modal screen + should be dismissed on swipe down gesture + + This slide-up-modal will callback the `anim-ref` fn and provides a + map with 2 animation methods: + + :open! - opens and displays the modal + :close! - closes the modal" + [{:keys [anim-ref swipe-dismiss?]} children] + {:pre [(fn? anim-ref)]} + (let [window-height (:height (react/get-dimensions "window") 1000) + + bottom-position (animation/create-value (- window-height)) + + modal-screen-bg-color + (animation/interpolate bottom-position + {:inputRange [(- window-height) 0] + :outputRange [colors/transparent + (colors/alpha colors/black 0.7)]}) + modal-screen-top + (animation/interpolate bottom-position + {:inputRange [(- window-height) + (+ (- window-height) 1) + 0] + :outputRange [window-height -200 -200]}) + + vertical-slide-to (fn [view-bottom] + (animation/start + (animation/timing bottom-position {:toValue view-bottom + :duration 500}))) + open-panel! #(vertical-slide-to 0) + close-panel! #(vertical-slide-to (- window-height)) + ;; swipe-down-panhandler + swipe-down-handlers + (when swipe-dismiss? + (js->clj + (.-panHandlers + (.create react/pan-responder + #js {:onMoveShouldSetPanResponder + (fn [e g] + (when-let [distance (.-dy g)] + (< 50 distance))) + :onMoveShouldSetPanResponderCapture + (fn [e g] + (when-let [distance (.-dy g)] + (< 50 distance))) + :onPanResponderRelease + (fn [e g] + (when-let [distance (.-dy g)] + (when (< 200 distance) + (close-panel!))))}))))] + (anim-ref {:open! open-panel! + :close! close-panel!}) + (fn [{:keys [anim-ref] :as opts} children] + [react/animated-view (merge + {:style + {:position :absolute + :top modal-screen-top + :bottom 0 + :left 0 + :right 0 + :z-index 1 + :background-color modal-screen-bg-color}} + swipe-down-handlers) + [react/touchable-highlight {:on-press (fn [] (close-panel!)) + :style {:flex 1}} + [react/view]] + [react/animated-view {:style + {:position :absolute + :left 0 + :right 0 + :z-index 2 + :bottom bottom-position}} + children]]))) + +(defn- custom-gas-panel-action [{:keys [label active on-press icon background-color]} child] + {:pre [label (boolean? active) on-press icon background-color]} + [react/view {:style {:flex-direction :row + :padding-horizontal 22 + :padding-vertical 11 + :align-items :center}} + [react/touchable-highlight + {:disabled active + :on-press on-press} + [react/animated-view {:style {:border-radius 21 + :width 40 + :height 40 + :justify-content :center + :align-items :center + :background-color background-color}} + [vector-icons/icon icon {:color (if active colors/white colors/gray)}]]] + [react/touchable-highlight + {:disabled active + :on-press on-press + :style {:flex 1}} + [react/text {:style {:color colors/black + :font-size 17 + :padding-left 17 + :line-height 40}} + label]] + child]) + +(defn- custom-gas-edit + [_opts] + (let [gas-error (reagent/atom nil) + gas-price-error (reagent/atom nil)] + (fn [{:keys [on-gas-input-change + on-gas-price-input-change + gas-input + gas-price-input]}] + [react/view {:style {:padding-horizontal 22 + :padding-vertical 11}} + [react/text (i18n/label :t/gas-price)] + (when @gas-price-error + [react/view {:style {:z-index 100}} + [tooltip/tooltip @gas-price-error + {:color colors/blue-light + :font-size 12 + :bottom-value -3}]]) + [react/view {:style {:border-radius 8 + :background-color colors/gray-lighter + :padding-vertical 16 + :padding-horizontal 16 + :flex-direction :row + :align-items :center + :margin-vertical 7}} + [react/text-input {:keyboard-type :numeric + :placeholder "0" + :on-change-text (fn [x] + (if-not (money/bignumber x) + (reset! gas-price-error (i18n/label :t/invalid-number-format)) + (reset! gas-price-error nil)) + (on-gas-price-input-change x)) + :default-value gas-price-input + :accessibility-label :gas-price-input + :keyboard-appearance :dark + :style {:font-size 15 + :flex 1 + :background-color colors/gray-lighter}}] + [react/text (i18n/label :t/gwei)]] + [react/text {:style {:color colors/gray + :font-size 12}} + (i18n/label :t/gas-cost-explanation)] + [react/text {:style {:margin-top 22}} (i18n/label :t/gas-limit)] + (when @gas-error + [react/view {:style {:z-index 100}} + [tooltip/tooltip @gas-error + {:color colors/blue-light + :font-size 12 + :bottom-value -3}]]) + [react/view {:style {:border-radius 8 + :background-color colors/gray-lighter + :padding-vertical 16 + :padding-horizontal 16 + :flex-direction :row + :align-items :flex-end + :margin-vertical 7}} + [react/text-input {:keyboard-type :numeric + :placeholder "0" + :on-change-text (fn [x] + (if-not (money/bignumber x) + (reset! gas-error (i18n/label :t/invalid-number-format)) + (reset! gas-error nil)) + (on-gas-input-change x)) + :default-value gas-input + :accessibility-label :gas-limit-input + :keyboard-appearance :dark + :style {:font-size 15 + :flex 1 + :background-color colors/gray-lighter}}]] + [react/text {:style {:color colors/gray + :font-size 12}} + (i18n/label :t/gas-limit-explanation)]]))) + +(defn- custom-gas-derived-state [{:keys [gas-input gas-price-input custom-open?]} + {:keys [custom-gas custom-gas-price + optimal-gas optimal-gas-price + gas-gas-price->fiat + fiat-currency]}] + (let [custom-input-gas + (or (when (not (string/blank? gas-input)) + (money/bignumber gas-input)) + custom-gas + optimal-gas) + custom-input-gas-price + (or (when (not (string/blank? gas-price-input)) + (money/->wei :gwei gas-price-input)) + custom-gas-price + optimal-gas-price)] + {:optimal-fiat-price + (str "~ " (:symbol fiat-currency) + (gas-gas-price->fiat {:gas optimal-gas + :gas-price optimal-gas-price})) + :custom-fiat-price + (if custom-open? + (str "~ " (:symbol fiat-currency) + (gas-gas-price->fiat {:gas custom-input-gas + :gas-price custom-input-gas-price})) + (str "...")) + :gas-price-input-value + (str (or gas-price-input + (some->> custom-gas-price (money/wei-> :gwei)) + (some->> optimal-gas-price (money/wei-> :gwei)))) + :gas-input-value + (str (or gas-input custom-gas optimal-gas)) + :gas-map-for-submit + (when custom-open? + {:gas custom-input-gas :gas-price custom-input-gas-price})})) + +;; Choosing the gas amount +(defn custom-gas-input-panel [{:keys [custom-gas custom-gas-price + optimal-gas optimal-gas-price + gas-gas-price->fiat on-submit] :as opts}] + {:pre [optimal-gas optimal-gas-price gas-gas-price->fiat on-submit]} + (let [{:keys [height]} (dimensions/window) + custom-height 290 + custom-open? (and custom-gas custom-gas-price) + state-atom (reagent.core/atom {:custom-open? (boolean custom-open?) + :gas-input nil + :gas-price-input nil}) + + ;; slider animations + slider-height (animation/create-value (if custom-open? custom-height 0)) + slider-height-to #(animation/start + (animation/timing slider-height {:toValue % + :duration 500})) + + optimal-button-bg-color + (animation/interpolate slider-height + {:inputRange [0 100 custom-height] + :outputRange [colors/blue colors/gray-light colors/gray-light]}) + + custom-button-bg-color + (animation/interpolate slider-height + {:inputRange [0 100 custom-height] + :outputRange [colors/gray-light colors/blue colors/blue]}) + + open-slider! #(do + (slider-height-to custom-height) + (swap! state-atom assoc :custom-open? true)) + close-slider! #(do + (slider-height-to 0) + (swap! state-atom assoc :custom-open? false))] + (fn [opts] + (let [{:keys [optimal-fiat-price + custom-fiat-price + gas-price-input-value + gas-input-value + gas-map-for-submit]} + (custom-gas-derived-state @state-atom opts)] + [react/scroll-view {:style {:background-color colors/white + :border-top-left-radius 8 + :border-top-right-radius 8 + :max-height (* 0.9 height)}} + [react/view {:style {:justify-content :center + :padding-top 22 + :padding-bottom 7}} + [react/text + {:style {:color colors/black + :font-size 22 + :line-height 28 + :font-weight :bold + :text-align :center}} + (i18n/label :t/network-fee-settings)] + [react/text + {:style {:color colors/gray + :font-size 15 + :line-height 22 + :text-align :center + :padding-horizontal 45 + :padding-vertical 8}} + (i18n/label :t/network-fee-explanation)]] + [react/view {:style {:border-top-width 1 + :border-top-color colors/black-transparent + :padding-top 11 + :padding-bottom 7}} + (custom-gas-panel-action {:icon :main-icons/time + :label (i18n/label :t/optimal-gas-option) + :on-press close-slider! + :background-color optimal-button-bg-color + :active (not (:custom-open? @state-atom))} + [react/text {:style {:color colors/gray + :font-size 17 + :padding-left 17 + :line-height 20}} + optimal-fiat-price]) + (custom-gas-panel-action {:icon :main-icons/sliders + :label (i18n/label :t/custom-gas-option) + :on-press open-slider! + :background-color custom-button-bg-color + :active (:custom-open? @state-atom)} + [react/text {:style {:color colors/gray + :font-size 17 + :padding-left 17 + :line-height 20 + :text-align :center + :min-width 60}} + custom-fiat-price]) + [react/animated-view {:style {:background-color colors/white + :height slider-height + :overflow :hidden}} + [custom-gas-edit + {:on-gas-price-input-change #(when (money/bignumber %) + (swap! state-atom assoc :gas-price-input %)) + :on-gas-input-change #(when (money/bignumber %) + (swap! state-atom assoc :gas-input %)) + :gas-price-input gas-price-input-value + :gas-input gas-input-value}]] + [react/view {:style {:flex-direction :row + :justify-content :center + :padding-vertical 16}} + [react/touchable-highlight + {:on-press #(on-submit gas-map-for-submit) + :style {:padding-horizontal 39 + :padding-vertical 12 + :border-radius 8 + :background-color colors/blue-light}} + [react/text {:style {:font-size 15 + :line-height 22 + :color colors/blue}} + (i18n/label :t/update)]]]]])))) + +(defn token->fiat-conversion [prices token fiat-currency value] + {:pre [(map? prices) (map? token) (map? fiat-currency) value]} + (when-let [price (get-in prices [(:symbol token) + (-> fiat-currency :code keyword) + :price])] + (some-> value + money/bignumber + (money/crypto->fiat price)))) + +(defn fiat->token-conversion [prices token fiat-currency value] + {:pre [(map? prices) (map? token) (map? fiat-currency) value]} + (when-let [price (get-in prices [(:symbol token) + (-> fiat-currency :code keyword) + :price])] + (some-> value + money/bignumber + (.div (money/bignumber price))))) + +(defn max-fee [{:keys [gas gas-price optimal-gas optimal-gas-price] :as params}] + (let [gas-param (or gas optimal-gas) + gas-price-param (or gas-price optimal-gas-price)] + (if (and gas-param gas-price-param) + (money/wei->ether (.times gas-param gas-price-param)) + 0))) + +(defn network-fees [prices token fiat-currency gas-ether-price] + (some-> (token->fiat-conversion prices token fiat-currency gas-ether-price) + (money/with-precision 3))) + +(defn fetch-optimal-gas [web3 tx-atom all-tokens chain cb] + (let [symbol (:symbol @tx-atom) + coin (tokens/symbol->token all-tokens chain symbol)] + (ethereum/gas-price + web3 + (fn [_ gas-price] + (when gas-price + (ethereum/estimate-gas-web3 + web3 + (clj->js (events/get-tx-params (assoc @tx-atom :value 0) symbol coin)) + (fn [_ gas] + (when gas + (cb {:optimal-gas (money/bignumber (* 2 gas)) + :optimal-gas-price (money/bignumber gas-price)}))))))))) + +(defn optimal-gas-present? [{:keys [optimal-gas optimal-gas-price]}] + (and optimal-gas optimal-gas-price)) + +(defn current-gas [{:keys [gas gas-price optimal-gas optimal-gas-price]}] + {:gas (or gas optimal-gas) :gas-price (or gas-price optimal-gas-price)}) + +(defn refresh-optimal-gas [web3 tx-atom all-tokens chain] + (fetch-optimal-gas web3 tx-atom all-tokens chain + (fn [res] + (swap! tx-atom merge res)))) diff --git a/src/status_im/ui/screens/wallet/send/views/overview.cljs b/src/status_im/ui/screens/wallet/send/views/overview.cljs new file mode 100644 index 00000000000..24f89529394 --- /dev/null +++ b/src/status_im/ui/screens/wallet/send/views/overview.cljs @@ -0,0 +1,327 @@ +(ns status-im.ui.screens.wallet.send.views.overview + (:require-macros [status-im.utils.views :refer [defview letsubs]]) + (:require [status-im.ui.components.react :as react] + [status-im.ui.components.icons.vector-icons :as vector-icons] + [status-im.ui.components.colors :as colors] + [status-im.ui.screens.wallet.utils :as wallet.utils] + [status-im.i18n :as i18n] + [status-im.ui.screens.wallet.send.views.common :as common] + [status-im.ui.screens.wallet.components.views :as wallet.components] + [status-im.utils.money :as money] + [reagent.core :as reagent] + [clojure.string :as string] + [status-im.ui.components.tooltip.views :as tooltip] + [status-im.ui.screens.wallet.send.events :as events] + [status-im.ui.components.list.styles :as list.styles] + [status-im.utils.ethereum.core :as ethereum] + [status-im.ui.screens.chat.photos :as photos])) + +(def signing-popup + {:background-color colors/white + :border-top-left-radius 8 + :border-top-right-radius 8 + :position :absolute + :left 0 + :right 0 + :bottom 0}) + +(defn confirm-modal [signing? {:keys [transaction total-amount gas-amount native-currency fiat-currency total-fiat]}] + [react/view {:style signing-popup} + [react/text {:style {:color colors/black + :font-size 15 + :line-height 22 + :margin-top 23 + :text-align :center}} + (i18n/label :t/total)] + [react/text {:style {:color colors/black + :margin-top 4 + :font-weight :bold + :font-size 22 + :line-height 28 + :text-align :center}} + (str total-amount " " (name (:symbol transaction)))] + (when-not (= :ETH (:symbol transaction)) + [react/text {:style {:color colors/black + :margin-top 5 + :font-weight :bold + :font-size 22 + :line-height 28 + :text-align :center}} + (str gas-amount " " (name (:symbol native-currency)))]) + [react/text {:style {:color colors/gray + :text-align :center + :margin-top 3 + :line-height 21 + :font-size 15}} + (str "~ " (:symbol fiat-currency "$") total-fiat)] + [react/view {:style {:flex-direction :row + :justify-content :center + :padding-top 16 + :padding-bottom 24}} + [react/touchable-highlight + {:on-press #(reset! signing? true) + :style {:padding-horizontal 39 + :padding-vertical 12 + :border-radius 8 + :background-color colors/blue-light}} + [react/text {:style {:font-size 15 + :line-height 22 + :color colors/blue}} + (i18n/label :t/confirm)]]]]) + +(defn- phrase-word [word] + [react/text {:style {:color colors/blue + :font-size 15 + :line-height 22 + :font-weight "500" + :width "33%" + :text-align :center}} + word]) + +(defn- phrase-separator [] + [react/view {:style {:height "100%" + :width 1 + :background-color colors/gray-light}}]) + +(defview sign-modal [account {:keys [transaction contact total-amount gas-amount native-currency fiat-currency + total-fiat all-tokens chain flow]}] + (letsubs [password (reagent/atom nil) + in-progress? (reagent/atom nil)] + (let [phrase (string/split (:signing-phrase account) #" ")] + [react/view {:style {:position :absolute + :left 0 + :right 0 + :bottom 0}} + [tooltip/tooltip (i18n/label :t/wallet-passphrase-reminder) + {:bottom-value 12 + :color colors/white + :text-color colors/blue + :font-size 12}] + [react/view {:style {:background-color colors/white + :border-top-left-radius 8 + :border-top-right-radius 8}} + [react/view {:flex 1 + :height 46 + :margin-top 18 + :flex-direction :row + :align-items :center + :margin-horizontal "15%" + :border-width 1 + :border-color colors/gray-light + :border-radius 218} + [phrase-word (first phrase)] + [phrase-separator] + [phrase-word (second phrase)] + [phrase-separator] + [phrase-word (last phrase)]] + [react/text {:style {:color colors/black + :margin-top 13 + :font-weight :bold + :font-size 22 + :line-height 28 + :text-align :center}} + (str "Send" " " total-amount " " (name (:symbol transaction)))] + (when-not (= :ETH (:symbol transaction)) + [react/text {:style {:color colors/black + :margin-top 5 + :font-weight :bold + :font-size 22 + :line-height 28 + :text-align :center}} + (str "Send" " " gas-amount " " (name (:symbol native-currency)))]) + [react/text {:style {:color colors/gray + :text-align :center + :margin-top 3 + :line-height 21 + :font-size 15}} + (str "~ " (:symbol fiat-currency "$") total-fiat)] + [react/text-input + {:auto-focus false + :secure-text-entry true + :placeholder (i18n/label :t/enter-your-login-password) + :placeholder-text-color colors/gray + :on-change-text #(reset! password %) + :style {:flex 1 + :margin-top 15 + :margin-horizontal 15 + :padding 14 + :padding-bottom 18 + :background-color colors/gray-lighter + :border-radius 8 + :font-size 15 + :letter-spacing -0.2 + :height 52} + :accessibility-label :enter-password-input + :keyboard-appearance :dark + :auto-capitalize :none}] + [react/view {:style {:flex-direction :row + :justify-content :center + :padding-top 16 + :padding-bottom 24}} + [react/touchable-highlight + {:on-press #(events/send-transaction-wrapper {:transaction transaction + :password @password + :flow flow + :all-tokens all-tokens + :in-progress? in-progress? + :chain chain + :contact contact + :account account}) + :disabled @in-progress? + :style {:padding-horizontal 39 + :padding-vertical 12 + :border-radius 8 + :background-color colors/blue-light}} + [react/text {:style {:font-size 15 + :line-height 22 + :color colors/blue}} + (i18n/label :t/send)]]]]]))) + +(defview confirm-and-sign [params] + (letsubs [signing? (reagent/atom false) + account [:account/account]] + (if-not @signing? + [confirm-modal signing? params] + [sign-modal account params]))) + +(defn render-transaction-overview [{:keys [flow transaction contact token native-currency + fiat-currency prices all-tokens chain web3]}] + (let [tx-atom (reagent/atom transaction) + network-fees-modal-ref (atom nil) + open-network-fees! #(common/anim-ref-send @network-fees-modal-ref :open!) + close-network-fees! #(common/anim-ref-send @network-fees-modal-ref :close!) + modal? (= :dapp flow)] + (when-not (common/optimal-gas-present? transaction) + (common/refresh-optimal-gas web3 tx-atom all-tokens chain)) + (fn [] + (let [transaction @tx-atom + gas-gas-price->fiat + (fn [gas-map] + (common/network-fees prices token fiat-currency (common/max-fee gas-map))) + + network-fee-eth (common/max-fee (common/current-gas transaction)) + network-fee-fiat + (when (common/optimal-gas-present? transaction) + (common/network-fees prices token fiat-currency network-fee-eth)) + + formatted-amount + (money/internal->formatted (:amount transaction) + (:symbol token) + (:decimals token)) + amount-str (str formatted-amount + " " (wallet.utils/display-symbol token)) + + fiat-amount (some-> (common/token->fiat-conversion prices token fiat-currency formatted-amount) + (money/with-precision 2)) + + total-amount (some-> (if (= :ETH (:symbol transaction)) + (.add (money/bignumber formatted-amount) network-fee-eth) + (money/bignumber formatted-amount)) + (money/with-precision (:decimals token))) + gas-amount (some-> (when-not (= :ETH (:symbol transaction)) + (money/bignumber network-fee-eth)) + (money/with-precision 18)) + + total-fiat (some-> (common/token->fiat-conversion prices token fiat-currency total-amount) + (money/with-precision 2))] + [wallet.components/simple-screen {:avoid-keyboard? (not modal?) + :status-bar-type (if modal? :modal-wallet :wallet)} + [common/toolbar flow (i18n/label :t/send-amount) (:public-key contact)] + [react/view {:style {:flex 1 + :border-top-width 1 + :border-top-color colors/white-light-transparent}} + (when (common/optimal-gas-present? @tx-atom) + [common/slide-up-modal {:anim-ref #(reset! network-fees-modal-ref %) + :swipe-dismiss? true} + [common/custom-gas-input-panel + (-> (select-keys @tx-atom [:gas :gas-price :optimal-gas :optimal-gas-price]) + (assoc + :fiat-currency fiat-currency + :gas-gas-price->fiat gas-gas-price->fiat + :on-submit (fn [{:keys [gas gas-price]}] + (when (and gas gas-price) + (swap! tx-atom assoc :gas gas :gas-price gas-price)) + (close-network-fees!))))]]) + [react/text {:style {:margin-top 18 + :text-align :center + :font-size 15 + :color colors/white-transparent}} + (i18n/label :t/recipient)] + [react/view + (when contact + [react/view {:style {:margin-top 10 + :flex-direction :row + :justify-content :center}} + [photos/photo (:photo-path contact) {:size list.styles/image-size}]]) + [react/text {:style {:color colors/white + :margin-horizontal 24 + :margin-top 10 + :line-height 22 + :font-size 15 + :text-align :center}} + (ethereum/normalized-address (:to transaction))]] + [react/text {:style {:margin-top 18 + :font-size 15 + :text-align :center + :color colors/white-transparent}} + (i18n/label :t/amount)] + [react/view {:style {:flex-direction :row + :align-items :center + :margin-top 10 + :margin-horizontal 24}} + [react/text {:style {:color colors/white + :font-size 15}} (i18n/label :t/sending)] + [react/view {:style {:flex 1}} + [react/text {:style {:color colors/white + :line-height 21 + :font-size 15 + :font-weight "500" + :text-align :right}} + amount-str] + [react/text {:style {:color colors/white-transparent + :line-height 21 + :font-size 15 + :text-align :right}} + (str "~ " (:symbol fiat-currency "$") fiat-amount " " (:code fiat-currency))]]] + [react/view {:style {:margin-horizontal 24 + :margin-top 10 + :padding-top 10 + :border-top-width 1 + :border-top-color colors/white-light-transparent}} + [react/view {:style {:flex-direction :row + :align-items :center}} + [react/touchable-highlight {:on-press #(open-network-fees!) + :accessibility-label :network-fee-button + :style {:background-color colors/black-transparent + :padding-horizontal 16 + :padding-vertical 9 + :border-radius 8}} + [react/view {:style {:flex-direction :row + :align-items :center}} + [react/text {:style {:color colors/white + :padding-right 10 + :font-size 15}} (i18n/label :t/network-fee)] + [vector-icons/icon :main-icons/settings {:color colors/white}]]] + [react/view {:style {:flex 1}} + [react/text {:style {:color colors/white + :line-height 21 + :font-size 15 + :font-weight "500" + :text-align :right} + :accessibility-label :total-fee} + (str network-fee-eth " " (wallet.utils/display-symbol native-currency))] + [react/text {:style {:color colors/white-transparent + :line-height 21 + :font-size 15 + :text-align :right}} + (str "~ " network-fee-fiat " " (:code fiat-currency))]]]] + [confirm-and-sign {:transaction transaction + :contact contact + :total-amount total-amount + :gas-amount gas-amount + :native-currency native-currency + :fiat-currency fiat-currency + :total-fiat total-fiat + :all-tokens all-tokens + :chain chain + :flow flow}]]])))) \ No newline at end of file diff --git a/src/status_im/ui/screens/wallet/send/views/recipient.cljs b/src/status_im/ui/screens/wallet/send/views/recipient.cljs new file mode 100644 index 00000000000..3ac8d210192 --- /dev/null +++ b/src/status_im/ui/screens/wallet/send/views/recipient.cljs @@ -0,0 +1,238 @@ +(ns status-im.ui.screens.wallet.send.views.recipient + (:require [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.ethereum.tokens :as tokens] + [reagent.core :as reagent] + [status-im.ui.screens.wallet.components.views :as wallet.components] + [status-im.i18n :as i18n] + [status-im.ui.screens.wallet.send.views.common :as common] + [re-frame.core :as re-frame] + [status-im.ui.components.colors :as colors] + [status-im.ui.components.list.styles :as list.styles] + [status-im.ui.components.react :as react] + [status-im.ui.screens.chat.photos :as photos] + [status-im.ui.components.icons.vector-icons :as vector-icons] + [status-im.ui.screens.wallet.send.events :as events] + [status-im.utils.utils :as utils] + [clojure.string :as string] + [status-im.ui.screens.wallet.send.styles :as styles] + [status-im.ui.components.tooltip.views :as tooltip] + [status-im.utils.ethereum.ens :as ens] + [status-im.ui.components.list.views :as list] + [taoensso.timbre :as log])) + +(defn simple-tab-navigator + "A simple tab navigator that that takes a map of tabs and the key of + the starting tab + + Example: + (simple-tab-navigator + {:main {:name \"Main\" :component (fn [] [react/text \"Hello\"])} + :other {:name \"Other\" :component (fn [] [react/text \"Goodbye\"])}} + :main)" + [tab-map default-key] + {:pre [(keyword? default-key)]} + (let [tab-key (reagent/atom default-key)] + (fn [tab-map _] + (let [tab-name @tab-key] + [react/view {:flex 1} + ;; tabs row + [react/view {:flex-direction :row} + (map (fn [[key {:keys [name component]}]] + (let [current? (= key tab-name)] + ^{:key (str key)} + [react/view {:flex 1 + :background-color colors/black-transparent} + [react/touchable-highlight {:on-press #(reset! tab-key key) + :disabled current?} + [react/view {:height 44 + :align-items :center + :justify-content :center + :border-bottom-width 2 + :border-bottom-color (if current? colors/white colors/transparent)} + [react/text {:style {:color (if current? colors/white colors/white-transparent) + :font-size 15}} name]]]])) + tab-map)] + (when-let [component-thunk (some-> tab-map tab-name :component)] + [component-thunk])])))) + +(defn open-qr-scanner [chain all-tokens contacts text-input transaction] + (.blur @text-input) + (re-frame/dispatch [:navigate-to :recipient-qr-code + {:on-recipient + (fn [qr-data] + (if-let [parsed-qr-data (events/extract-qr-code-details chain all-tokens qr-data)] + (let [{:keys [chain-id]} parsed-qr-data + tx-data (events/qr-data->transaction-data parsed-qr-data contacts)] + (if (= chain-id (ethereum/chain-keyword->chain-id chain)) + (swap! transaction merge tx-data) + (utils/show-popup (i18n/label :t/error) + (i18n/label :t/wallet-invalid-chain-id {:data qr-data + :chain chain-id})))) + (utils/show-confirmation + {:title (i18n/label :t/error) + :content (i18n/label :t/wallet-invalid-address {:data qr-data}) + :cancel-button-text (i18n/label :t/see-it-again) + :confirm-button-text (i18n/label :t/got-it) + :on-cancel (partial open-qr-scanner chain all-tokens contacts text-input transaction)})))}])) + +(defn update-recipient [web3 chain transaction error-message value] + (if (ens/is-valid-eth-name? value) + (do (ens/get-addr web3 + (get ens/ens-registries chain) + value + #(if (ethereum/address? %) + (swap! transaction assoc :to-ens value :to %) + (reset! error-message (i18n/label :t/error-unknown-ens-name))))) + (do (swap! transaction assoc :to value) + (reset! error-message nil)))) + +(defn choose-address-view + "A view that allows you to choose an address" + [{:keys [web3 chain all-tokens contacts transaction on-address]}] + {:pre [(keyword? chain) (fn? on-address)]} + (fn [] + (let [error-message (reagent/atom nil) + text-input (atom nil)] + (fn [] + [react/view {:flex 1} + [react/view {:flex 1}] + [react/view styles/centered + (when @error-message + [tooltip/tooltip @error-message {:color colors/white + :font-size 12 + :bottom-value 15}]) + [react/text-input + {:on-change-text (partial update-recipient web3 chain transaction error-message) + :auto-focus true + :auto-capitalize :none + :auto-correct false + :placeholder (i18n/label :t/address-or-ens-placeholder) + :placeholder-text-color colors/blue-shadow + :multiline true + :max-length 84 + :ref #(reset! text-input %) + :default-value (or (:to-ens @transaction) (:to @transaction)) + :selection-color colors/green + :accessibility-label :recipient-address-input + :keyboard-appearance :dark + :style styles/choose-recipient-text-input}]] + [react/view {:flex 1}] + [react/view {:flex-direction :row + :padding 3} + [common/action-button {:underlay-color colors/white-transparent + :background-color colors/black-transparent + :on-press #(react/get-from-clipboard + (fn [addr] + (when (and addr (not (string/blank? addr))) + (swap! transaction assoc :to (string/trim addr)))))} + [react/view {:flex-direction :row + :padding-horizontal 18} + [vector-icons/icon :main-icons/paste {:color colors/white-transparent}] + [react/view {:flex 1 + :flex-direction :row + :justify-content :center} + [react/text {:style {:color colors/white + :font-size 15 + :line-height 22}} + (i18n/label :t/paste)]]]] + [common/action-button {:underlay-color colors/white-transparent + :background-color colors/black-transparent + :on-press (fn [] + (re-frame/dispatch + [:request-permissions {:permissions [:camera] + :on-allowed (partial open-qr-scanner chain all-tokens contacts text-input transaction) + :on-denied #(utils/set-timeout + (fn [] + (utils/show-popup (i18n/label :t/error) + (i18n/label :t/camera-access-error))) + 50)}]))} + [react/view {:flex-direction :row + :padding-horizontal 18} + [vector-icons/icon :main-icons/qr {:color colors/white-transparent}] + [react/view {:flex 1 + :flex-direction :row + :justify-content :center} + [react/text {:style {:color colors/white + :font-size 15 + :line-height 22}} + (i18n/label :t/scan)]]]] + (let [disabled? (string/blank? (:to @transaction))] + [common/action-button {:disabled? disabled? + :underlay-color colors/black-transparent + :background-color (if disabled? colors/blue colors/white) + :on-press #(events/chosen-recipient web3 chain @transaction on-address + (fn on-error [code] + (reset! error-message (i18n/label code))))} + [react/text {:style {:color (if disabled? + (colors/alpha colors/white 0.3) + colors/blue) + :font-size 15 + :line-height 22}} + (i18n/label :t/next)]])]])))) + +(defn render-contact [on-contact contact] + {:pre [(fn? on-contact) (map? contact) (:address contact)]} + [react/touchable-highlight {:underlay-color colors/white-transparent + :on-press #(on-contact contact)} + [react/view {:flex 1 + :flex-direction :row + :padding-right 23 + :padding-left 16 + :padding-top 12} + [react/view {:margin-top 3} + [photos/photo (:photo-path contact) {:size list.styles/image-size}]] + [react/view {:margin-left 16 + :flex 1} + [react/view {:accessibility-label :contact-name-text + :margin-bottom 2} + [react/text {:style {:font-size 15 + :font-weight "500" + :line-height 22 + :color colors/white}} + (:name contact)]] + [react/text {:style {:font-size 15 + :line-height 22 + :color colors/white-transparent} + :accessibility-label :contact-address-text} + (ethereum/normalized-address (:address contact))]]]]) + +(defn choose-contact-view [{:keys [contacts on-contact]}] + {:pre [(every? map? contacts) (fn? on-contact)]} + (if (empty? contacts) + (common/info-page (i18n/label :t/wallet-no-contacts)) + [react/view {:flex 1} + [list/flat-list {:data contacts + :key-fn :address + :render-fn (partial render-contact on-contact)}]])) + +(defn render-choose-recipient [{:keys [modal? web3 contacts all-tokens transaction network network-status]}] + (let [transaction (reagent/atom transaction) + chain (ethereum/network->chain-keyword network) + native-currency (tokens/native-currency chain) + online? (= :online network-status)] + [wallet.components/simple-screen {:avoid-keyboard? (not modal?) + :status-bar-type (if modal? :modal-wallet :wallet)} + [common/toolbar :wallet (i18n/label :t/send-to) nil] + [simple-tab-navigator + {:address {:name (i18n/label :t/wallet-address-tab-title) + :component (choose-address-view + {:web3 web3 + :chain chain + :all-tokens all-tokens + :contacts contacts + :transaction transaction + :on-address #(re-frame/dispatch [:navigate-to :wallet-choose-amount + {:transaction (swap! transaction assoc :to %) + :native-currency native-currency + :modal? modal?}])})} + :contacts {:name (i18n/label :t/wallet-contacts-tab-title) + :component (partial choose-contact-view + {:contacts contacts + :on-contact (fn [{:keys [address] :as contact}] + (re-frame/dispatch + [:navigate-to :wallet-choose-amount + {:modal? modal? + :native-currency native-currency + :contact contact + :transaction (swap! transaction assoc :to address)}]))})}} + :address]])) diff --git a/src/status_im/ui/screens/wallet/sign_message/views.cljs b/src/status_im/ui/screens/wallet/sign_message/views.cljs index 6b0f3892328..6b1b8f593c7 100644 --- a/src/status_im/ui/screens/wallet/sign_message/views.cljs +++ b/src/status_im/ui/screens/wallet/sign_message/views.cljs @@ -78,7 +78,7 @@ (= :offline network-status)) :accessibility-label :sign-transaction-button} (i18n/label sign-label) - [vector-icons/icon :icons/forward {:color colors/white}]]])) + [vector-icons/icon :main-icons/next {:color colors/white}]]])) ;; SIGN MESSAGE FROM DAPP (defview sign-message-modal [] diff --git a/src/status_im/ui/screens/wallet/transaction_fee/views.cljs b/src/status_im/ui/screens/wallet/transaction_fee/views.cljs deleted file mode 100644 index 77917b429a4..00000000000 --- a/src/status_im/ui/screens/wallet/transaction_fee/views.cljs +++ /dev/null @@ -1,102 +0,0 @@ -(ns status-im.ui.screens.wallet.transaction-fee.views - (:require-macros [status-im.utils.views :refer [defview letsubs]]) - (:require [re-frame.core :as re-frame] - [status-im.i18n :as i18n] - [status-im.ui.components.bottom-buttons.view :as bottom-buttons] - [status-im.ui.components.button.view :as button] - [status-im.ui.components.react :as react] - [status-im.ui.components.styles :as components.styles] - [status-im.ui.components.toolbar.actions :as act] - [status-im.ui.components.toolbar.view :as toolbar] - [status-im.ui.components.tooltip.views :as tooltip] - [status-im.ui.screens.wallet.components.views :as components] - [status-im.ui.screens.wallet.send.styles :as styles] - [status-im.ui.screens.wallet.styles :as wallet.styles] - [status-im.utils.money :as money] - [status-im.utils.ethereum.tokens :as tokens] - [status-im.utils.ethereum.core :as ethereum] - [status-im.ui.screens.wallet.utils :as wallet.utils])) - -(defn- toolbar [title] - [toolbar/toolbar {:style wallet.styles/toolbar} - [toolbar/nav-button (act/close-white act/default-handler)] - [toolbar/content-title {:color :white} title]]) - -(defview transaction-fee [] - (letsubs [send-transaction [:wallet.send/transaction] - network [:account/network] - {gas-edit :gas - max-fee :max-fee - gas-price-edit :gas-price} [:wallet/edit] - all-tokens [:wallet/all-tokens]] - (let [{:keys [amount symbol]} send-transaction - gas (:value gas-edit) - gas-price (:value gas-price-edit) - chain (ethereum/network->chain-keyword network) - native-currency (tokens/native-currency chain) - {:keys [decimals] :as token} (tokens/asset-for all-tokens chain symbol)] - [components/simple-screen {:status-bar-type :modal-wallet} - [toolbar (i18n/label :t/wallet-transaction-fee)] - [react/view components.styles/flex - [react/view {:flex-direction :row} - - [react/view styles/gas-container-wrapper - [components/cartouche {} - (i18n/label :t/gas-limit) - [react/view styles/gas-input-wrapper - [react/text-input (merge styles/transaction-fee-input - {:on-change-text #(re-frame/dispatch [:wallet.send/edit-value :gas %]) - :default-value gas - :accessibility-label :gas-limit-input})]]] - (when (:invalid? gas-edit) - [tooltip/tooltip (i18n/label :t/invalid-number) styles/gas-input-error-tooltip])] - - [react/view styles/gas-container-wrapper - [components/cartouche {} - (i18n/label :t/gas-price) - [react/view styles/gas-input-wrapper - [react/text-input (merge styles/transaction-fee-input - {:on-change-text #(re-frame/dispatch [:wallet.send/edit-value :gas-price %]) - :default-value gas-price - :accessibility-label :gas-price-input})] - [components/cartouche-secondary-text - (i18n/label :t/gwei)]]] - (when (:invalid? gas-price-edit) - [tooltip/tooltip - (i18n/label (if (= :invalid-number (:invalid? gas-price-edit)) - :t/invalid-number - :t/wallet-send-min-wei)) - styles/gas-input-error-tooltip])]] - - [react/view styles/transaction-fee-info - [react/view styles/transaction-fee-info-icon - [react/text {:style styles/transaction-fee-info-icon-text} "?"]] - [react/view styles/transaction-fee-info-text-wrapper - [react/i18n-text {:style styles/advanced-fees-text - :key :wallet-transaction-fee-details}]]] - [components/separator] - [react/view styles/transaction-fee-block-wrapper - [components/cartouche {:disabled? true} - (i18n/label :t/amount) - [react/view {:accessibility-label :amount-input} - [components/cartouche-text-content - (str (money/to-fixed (money/internal->formatted amount symbol decimals))) - (wallet.utils/display-symbol token)]]] - [components/cartouche {:disabled? true} - (i18n/label :t/wallet-transaction-total-fee) - [react/view {:accessibility-label :total-fee-input} - [components/cartouche-text-content - (str max-fee " " (wallet.utils/display-symbol native-currency))]]]] - - [bottom-buttons/bottom-buttons styles/fee-buttons - [button/button {:on-press #(re-frame/dispatch [:wallet.send/reset-gas-default]) - :accessibility-label :reset-to-default-button} - (i18n/label :t/reset-default)] - [button/button {:on-press #(do (re-frame/dispatch [:wallet.send/set-gas-details - (:value-number gas-edit) - (:value-number gas-price-edit)]) - (act/default-handler)) - :accessibility-label :done-button - :disabled? (or (:invalid? gas-edit) - (:invalid? gas-price-edit))} - (i18n/label :t/done)]]]]))) diff --git a/src/status_im/ui/screens/wallet/transaction_sent/views.cljs b/src/status_im/ui/screens/wallet/transaction_sent/views.cljs index 09af1b5ad6d..6b4f62485c0 100644 --- a/src/status_im/ui/screens/wallet/transaction_sent/views.cljs +++ b/src/status_im/ui/screens/wallet/transaction_sent/views.cljs @@ -61,13 +61,13 @@ (bottom-action-button on-next)]) (defview transaction-sent [] - (letsubs [chat-id [:chats/current-chat-id]] + (letsubs [{:keys [chat-id flow]} [:get-screen-params :wallet-transaction-sent]] [react/view {:flex 1 :background-color colors/blue} [status-bar/status-bar {:type :transparent}] - (sent-screen {:on-next #(re-frame/dispatch [:close-transaction-sent-screen chat-id])})])) + (sent-screen {:on-next #(re-frame/dispatch [:close-transaction-sent-screen chat-id flow])})])) (defview transaction-sent-modal [] (letsubs [chat-id [:chats/current-chat-id]] [react/view {:flex 1 :background-color colors/blue} [status-bar/status-bar {:type :modal-wallet}] - (sent-screen {:on-next #(re-frame/dispatch [:close-transaction-sent-screen chat-id])})])) + (sent-screen {:on-next #(re-frame/dispatch [:close-transaction-sent-screen chat-id :dapp])})])) diff --git a/src/status_im/utils/ethereum/erc20.cljs b/src/status_im/utils/ethereum/erc20.cljs index 9822cea8547..3c8c98a22da 100644 --- a/src/status_im/utils/ethereum/erc20.cljs +++ b/src/status_im/utils/ethereum/erc20.cljs @@ -19,7 +19,8 @@ [status-im.native-module.core :as status] [status-im.utils.security :as security] [status-im.js-dependencies :as dependencies] - [status-im.utils.types :as types]) + [status-im.utils.types :as types] + [taoensso.timbre :as log]) (:refer-clojure :exclude [name symbol])) (def utils dependencies/web3-utils) @@ -112,13 +113,15 @@ (defn balance-of [web3 contract address cb] (.balanceOf (get-instance web3 contract) address cb)) -(defn transfer [contract from to value gas gas-price masked-password on-completed] - (status/send-transaction (types/clj->json - (merge (ethereum/call-params contract "transfer(address,uint256)" to value) - {:from from - :gas gas - :gasPrice gas-price})) - (security/safe-unmask-data masked-password) +(defn transfer-tx [contract from to value gas gas-price] + (merge (ethereum/call-params contract "transfer(address,uint256)" to value) + {:from from + :gas gas + :gasPrice gas-price})) + +(defn transfer [contract from to value gas gas-price password on-completed] + (status/send-transaction (types/clj->json (transfer-tx contract from to value gas gas-price)) + password on-completed)) (defn transfer-from [web3 contract from-address to-address value cb] diff --git a/test/cljs/status_im/test/chat/commands/impl/transactions.cljs b/test/cljs/status_im/test/chat/commands/impl/transactions.cljs index ff80ff24764..43705a3a60e 100644 --- a/test/cljs/status_im/test/chat/commands/impl/transactions.cljs +++ b/test/cljs/status_im/test/chat/commands/impl/transactions.cljs @@ -40,23 +40,7 @@ {:title (i18n/label :t/send-request-amount) :description (i18n/label :t/send-request-amount-max-decimals {:asset-decimals 18})})) (is (= (protocol/validate personal-send-command {:asset "ETH" :amount "0.01"} cofx) - nil))) - (testing "Yielding control prefills wallet" - (let [fx (protocol/yield-control personal-send-command {:content {:params {:asset "ETH" :amount "0.01"}}} cofx)] - (is (= (get-in fx [:db :wallet :send-transaction :amount-text]) "0.01")) - (is (= (get-in fx [:db :wallet :send-transaction :symbol]) :ETH))))) - -(deftest from-contacts - (testing "the user is in our contacts" - (let [fx (protocol/yield-control personal-send-command {:content {:params {:asset "ETH" :amount "0.01"}}} cofx)] - (is (= (get-in fx [:db :wallet :send-transaction :to]) address)) - (is (= (get-in fx [:db :wallet :send-transaction :to-name] "Recipient"))) - (is (= (get-in fx [:db :wallet :send-transaction :public-key]) public-key))) - (testing "the user is not in our contacts" - (let [fx (protocol/yield-control personal-send-command {:content {:params {:asset "ETH" :amount "0.01"}}} (update-in cofx [:db :contacts/contacts] dissoc public-key))] - (is (= (get-in fx [:db :wallet :send-transaction :to]) address)) - (is (= (get-in fx [:db :wallet :send-transaction :to-name]) "Plump Nippy Blobfish")) - (is (= (get-in fx [:db :wallet :send-transaction :public-key]) public-key)))))) + nil)))) ;; testing the `/request` command diff --git a/translations/en.json b/translations/en.json index fc1acc14fca..891738f56ef 100644 --- a/translations/en.json +++ b/translations/en.json @@ -119,7 +119,7 @@ "camera-access-error": "To grant the required camera permission, please go to your system settings and make sure that Status > Camera is selected.", "wallet-invalid-address": "Invalid address: \n {{data}}", "invalid-address": "Invalid address", - "wallet-invalid-address-checksum": "Error in address: \n {{data}}", + "wallet-invalid-address-checksum": "Error in address", "welcome-to-status": "Welcome to Status", "cryptokitty-name": "CryptoKitty #{{id}}", "address-explication": "Your public key is used to generate your address on Ethereum and is a series of numbers and letters. You can find it easily in your profile", @@ -819,7 +819,6 @@ "secret-keys-confirmation-text": "Record these now because you won't see this screen again", "secret-keys-confirmation-confirm": "GOT IT", "secret-keys-confirmation-cancel": "SEE IT AGAIN", - "see-it-again": "SEE IT AGAIN", "completing-card-setup": "Completing card setup", "generating-mnemonic": "Generating mnemonic phrase", "next-step-generating-mnemonic": "Next step is generating mnemonic phrase for your card", @@ -945,5 +944,27 @@ "mobile-network-use-mobile-details": "Status tends to use a lot of data when syncing chats. You can choose not to sync when on mobile network", "mobile-network-ask-me": "Ask me when on mobile network", "mobile-network-go-to-settings": "Go to settings", - "mobile-network-use-mobile-data": "Status tends to use a lot of data when syncing chats. You can choose not to sync when on mobile network" + "mobile-network-use-mobile-data": "Status tends to use a lot of data when syncing chats. You can choose not to sync when on mobile network", + "error-unknown-ens-name": "Unknown ENS name", + "address-or-ens-placeholder": "0x... or name.eth", + "wallet-address-tab-title": "Address", + "wallet-contacts-tab-title": "Contacts", + "invalid-number-format": "Invalid number format", + "gas-cost-explanation": "Gas price is the amount you are willing to pay per unit of gas. Increasing this price may help your transaction get processed faster.", + "gas-limit-explanation": "Gas limit is the maximum units of gas you're willing to spend on this transaction.", + "network-fee-settings": "Network fee settings", + "network-fee-explanation": "This fee, known as gas is paid directly to the Ethereum network. Status does not collect any of these funds", + "optimal-gas-option": "Optimal", + "custom-gas-option": "Custom", + "update": "Update", + "choose-asset": "Choose asset", + "wallet-no-assets-enabled": "You don't have any assets yet", + "network-fee-amount": "network fee ~ {{amount}} {{currency}}", + "total": "Total", + "wallet-passphrase-reminder": "Only send the transaction if you recognize\nyour three words", + "send-amount-currency": "Send {{amount}} {{currency}}", + "enter-your-login-password": "Enter your login password...", + "send": "Send", + "sending": "Sending", + "network-fee": "Network fee" }