diff --git a/resources/icons/change.svg b/resources/icons/change.svg
new file mode 100644
index 000000000000..d06a596dfaac
--- /dev/null
+++ b/resources/icons/change.svg
@@ -0,0 +1,4 @@
+
diff --git a/resources/icons/paste.svg b/resources/icons/paste.svg
new file mode 100644
index 000000000000..ea53e87edf95
--- /dev/null
+++ b/resources/icons/paste.svg
@@ -0,0 +1,3 @@
+
diff --git a/resources/icons/sliders.svg b/resources/icons/sliders.svg
new file mode 100644
index 000000000000..46b837f4546d
--- /dev/null
+++ b/resources/icons/sliders.svg
@@ -0,0 +1,4 @@
+
diff --git a/resources/icons/time.svg b/resources/icons/time.svg
new file mode 100644
index 000000000000..10d86cd63ce9
--- /dev/null
+++ b/resources/icons/time.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/status_im/ui/screens/routing/screens.cljs b/src/status_im/ui/screens/routing/screens.cljs
index e5b103c17df0..c7ebef0bba3f 100644
--- a/src/status_im/ui/screens/routing/screens.cljs
+++ b/src/status_im/ui/screens/routing/screens.cljs
@@ -24,6 +24,7 @@
[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.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]
[status-im.ui.screens.wallet.transaction-sent.views :as transaction-sent]
[status-im.ui.screens.contacts-list.views :as contacts-list]
@@ -104,6 +105,8 @@
:recent-recipients wallet.components/recent-recipients
: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-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/wallet/send/animations.cljs b/src/status_im/ui/screens/wallet/send/animations.cljs
index ef73a5fe28f1..b1b29fc2cc85 100644
--- a/src/status_im/ui/screens/wallet/send/animations.cljs
+++ b/src/status_im/ui/screens/wallet/send/animations.cljs
@@ -8,4 +8,4 @@
:duration 500})
(animation/timing bottom-value {:toValue 53
:easing (.bezier (animation/easing) 0.685, 0.000, 0.025, 1.185)
- :duration 500})])))
\ No newline at end of file
+ :duration 500})])))
diff --git a/src/status_im/ui/screens/wallet/send/events.cljs b/src/status_im/ui/screens/wallet/send/events.cljs
index 32e22db1ebef..9c39b1e21299 100644
--- a/src/status_im/ui/screens/wallet/send/events.cljs
+++ b/src/status_im/ui/screens/wallet/send/events.cljs
@@ -1,15 +1,19 @@
(ns status-im.ui.screens.wallet.send.events
- (:require [re-frame.core :as re-frame]
+ (:require [clojure.string :as string]
+ [re-frame.core :as re-frame]
[status-im.chat.commands.sending :as commands-sending]
[status-im.chat.models.message :as models.message]
[status-im.chat.models :as chat.models]
[status-im.constants :as constants]
+ [status-im.contact.db :as contact.db]
[status-im.i18n :as i18n]
[status-im.models.transactions :as wallet.transactions]
[status-im.models.wallet :as models.wallet]
[status-im.native-module.core :as status]
[status-im.ui.screens.navigation :as navigation]
[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.core :as ethereum]
[status-im.utils.ethereum.erc20 :as erc20]
[status-im.utils.ethereum.tokens :as tokens]
@@ -52,6 +56,58 @@
(utils/set-timeout #(utils/show-popup (i18n/label :t/transaction-failed) message) 1000)))
;;;; Handlers
+;; HANDLE QR CODE
+
+#_(defn- qr-data->send-tx-data [{:keys [address name value symbol gas gasPrice public-key from-chat?]}]
+ {:pre [(not (nil? address))]}
+ (cond-> {:to address :public-key public-key}
+ value (assoc :amount value)
+ symbol (assoc :symbol symbol)
+ gas (assoc :gas (money/bignumber gas))
+ from-chat? (assoc :from-chat? from-chat?)
+ gasPrice (assoc :gas-price (money/bignumber gasPrice))))
+
+#_(defn extract-qr-code-details [chain-id qr-uri]
+ {:pre [(integer? chain-id) (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)
+ ;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]
+ {:pre [(map? qr-data)]}
+ ;; i don't like fetching all tokens here
+ (let [{:keys [:contacts/contacts]} @re-frame.db/app-db
+ {:keys [to] :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]
+ (if (ens/is-valid-eth-name? recipient)
+ (ens/get-addr (get @re-frame.db/app-db :web3)
+ (get ens/ens-registries chain)
+ recipient
+ 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 %))))
+
+(handlers/register-handler-fx
+ :wallet/transaction-to-success
+ (fn [{:keys [db]} [_ recipient-address]]
+ {:db (assoc-in db [:wallet :send-transaction :to] recipient-address)}))
;; SEND TRANSACTION
(handlers/register-handler-fx
diff --git a/src/status_im/ui/screens/wallet/send/styles.cljs b/src/status_im/ui/screens/wallet/send/styles.cljs
index f4f0bc8439e4..434218d8e03f 100644
--- a/src/status_im/ui/screens/wallet/send/styles.cljs
+++ b/src/status_im/ui/screens/wallet/send/styles.cljs
@@ -152,3 +152,18 @@
(defstyle gas-input-error-tooltip
{:android {:bottom-value -38}})
+
+;; ----------------------------------------------------------------------
+;; Choose Address View
+;; ----------------------------------------------------------------------
+
+(def centered {:justify-content :center
+ :align-items :center})
+
+(def choose-recipient-text-input
+ {:color colors/white
+ :font-size 30
+ :font-weight :bold
+ :line-height 39
+ :min-width 236
+ :margin-horizontal 24})
diff --git a/src/status_im/ui/screens/wallet/send/views.cljs b/src/status_im/ui/screens/wallet/send/views.cljs
index f6c7a31d319f..cf6d0bf4ebe5 100644
--- a/src/status_im/ui/screens/wallet/send/views.cljs
+++ b/src/status_im/ui/screens/wallet/send/views.cljs
@@ -1,6 +1,8 @@
(ns status-im.ui.screens.wallet.send.views
(:require-macros [status-im.utils.views :refer [defview letsubs]])
- (:require [re-frame.core :as re-frame]
+ (: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]
@@ -13,11 +15,16 @@
[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]
@@ -37,7 +44,7 @@
[toolbar/nav-button (action (if modal?
#(re-frame/dispatch [:wallet/discard-transaction-navigate-back])
#(actions/default-handler)))]
- [toolbar/content-title {:color :white} title]]))
+ [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
@@ -184,6 +191,957 @@
[password-input-panel :t/signing-phrase-description in-progress?])
(when in-progress? [react/view styles/processing-view])]]))
+(defn address-entry []
+ (reagent/create-class
+ {:reagent-render
+ (fn [opts]
+ [react/view [react/text "Select address"]])}))
+
+;; ----------------------------------------------------------------------
+;; 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 choose-address-view
+ "A view that allows you to choose an address"
+ [{:keys [chain on-address]}]
+ {:pre [(keyword? chain) (fn? on-address)]}
+ (fn []
+ (let [address (reagent/atom "")
+ error-message (reagent/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 #(do (reset! address %)
+ (reset! error-message nil))
+ :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 42
+ :value @address
+ :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)))
+ (reset! address (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
+ #(re-frame/dispatch [:navigate-to
+ :recipient-qr-code
+ {:on-recipient
+ (fn [])}])
+ :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? @address)]
+ [address-button {:disabled? disabled?
+ :underlay-color colors/black-transparent
+ :background-color (if disabled? colors/blue colors/white)
+ :on-press
+ #(events/chosen-recipient
+ chain @address
+ 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 transaction scroll advanced? network all-tokens amount-input network-status] :as opts}]
+
+ (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-to)]
+ [simple-tab-navigator {:address {:name "Address"
+ :component (choose-address-view
+ {:chain chain
+ :on-address
+ #(re-frame/dispatch [:navigate-to :wallet-choose-amount
+ {:transaction (assoc transaction :to %)
+ :native-currency native-currency
+ :modal? modal?}])})}
+ :contacts {:name "Contacts"
+ :component (partial
+ choose-contact-view
+ {:contacts contacts
+ :on-contact
+ (fn [{:keys [address]}]
+ (re-frame/dispatch [:navigate-to :wallet-choose-amount
+ {:modal? modal?
+ :native-currency native-currency
+ :transaction (assoc transaction :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."]])))
+
+;; TODOs
+
+;; - add stronger validation and a tool tip for bad input
+;; - ensure that gas passed back to teh parent view is correct
+;; - wire in gas and gas-price to the parent view
+
+(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] :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 "~ $" (gas-gas-price->fiat optimal-gas optimal-gas-price))
+ :custom-fiat-price
+ (if custom-open?
+ (str "~ $" (gas-gas-price->fiat custom-input-gas 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
+ (if custom-open?
+ {:gas optimal-gas :gas-price optimal-gas-price}
+ {: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 [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)))
+
+;; 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?
+ transaction]}]
+ {:pre [(map? native-currency)]}
+ (let [tx-atom (reagent/atom transaction)
+ token (or (fetch-token all-tokens network (:symbol @tx-atom))
+ native-currency)
+ optimal-gas-price-atom (reagent/atom nil)
+ state-atom (reagent/atom (create-initial-state token (:amount @tx-atom)))
+ 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
+ (ethereum/gas-price (:web3 @re-frame.db/app-db)
+ (fn [_ gas-price]
+ (when gas-price
+ (reset! optimal-gas-price-atom gas-price))))
+ (fn [{:keys [balance network prices fiat-currency
+ native-currency all-tokens modal? transaction]}]
+ (let [{:keys [symbol to] :or {symbol (:symbol native-currency)} :as transaction} @tx-atom
+ token (-> (tokens/asset-for all-tokens (ethereum/network->chain-keyword network) symbol)
+ (assoc :amount (get balance symbol (money/bignumber 0))))
+ optimal-gas (ethereum/estimate-gas symbol)
+ gas-gas-price->fiat
+ (fn [gas' gas-price']
+ (network-fees prices token fiat-currency (max-fee gas' gas-price')))]
+ [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-price-atom
+ [slide-up-modal {:anim-ref #(reset! network-fees-modal-ref %)
+ :swipe-dismiss? true}
+ [custom-gas-input-panel
+ {:on-submit (fn [{:keys [gas gas-price]}]
+ #_(assert (and gas gas-price))
+ (swap! tx-atom assoc :gas gas :gas-price gas-price)
+ (close-network-fees!))
+ :custom-gas (:gas @tx-atom)
+ :custom-gas-price (:gas-price @tx-atom)
+ :optimal-gas optimal-gas
+ :optimal-gas-price @optimal-gas-price-atom
+ :gas-gas-price->fiat gas-gas-price->fiat}]])
+ [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-price-atom
+ (gas-gas-price->fiat
+ (:gas @tx-atom optimal-gas)
+ (:gas-price @tx-atom @optimal-gas-price-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)
+ :on-press
+ (fn []
+ ;; TODO handle moving onto overview from here
+
+ #_(prn (money/bignumber amount) (money/wei->ether (:amount token)))
+ #_(if-let [new-amount (money/bignumber amount)]
+ ;; look at sufficient funds and sufficient gas for this
+ (if (.greaterThanOrEqualTo new-amount (money/wei->ether (:amount token)))
+ (do 'good) ;; Move onto overview after adding amount to tx
+ (reset! error-message "Insufficient funds"))
+ (reset! error-message "Invalid amount")))}
+ [react/text {:style {:color (if disabled? colors/white colors/blue)
+ :font-size 15
+ :line-height 22}}
+ (i18n/label :t/next)]])]]]))]))))
+
+(defview choose-amount-token []
+ (letsubs [{:keys [transaction modal? 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
+ :transaction transaction}]))
+
+;; ----------------------------------------------------------------------
+;; Step 3 Final Overview
+;; ----------------------------------------------------------------------
+
+#_(defview final-tx-overview [])
+
;; MAIN SEND TRANSACTION VIEW
(defn- send-transaction-view [{:keys [scroll] :as opts}]
(let [amount-input (atom nil)
@@ -197,8 +1155,7 @@
;;NOTE(goranjovic): keyboardDidShow is for android and keyboardWillShow for ios
(.addListener react/keyboard "keyboardDidShow" handler)
(.addListener react/keyboard "keyboardWillShow" handler))
- :reagent-render (fn [opts] (render-send-transaction-view
- (assoc opts :amount-input amount-input)))})))
+ :reagent-render (fn [opts] [choose-address-contact (assoc opts :amount-input amount-input)])})))
;; SEND TRANSACTION FROM WALLET (CHAT)
(defview send-transaction []
@@ -207,13 +1164,16 @@
network [:account/network]
scroll (atom nil)
network-status [:network-status]
- all-tokens [:wallet/all-tokens]]
+ all-tokens [:wallet/all-tokens]
+ contacts [:contacts/all-added-people-contacts]]
[send-transaction-view {:modal? false
- :transaction transaction
+ ;; TODO only send gas and gas-price when they are custom
+ :transaction (dissoc transaction :gas :gas-price)
:scroll scroll
:advanced? advanced?
:network network
:all-tokens all-tokens
+ :contacts contacts
:network-status network-status}]))
;; SEND TRANSACTION FROM DAPP
diff --git a/translations/en.json b/translations/en.json
index 5836ba6e6de6..fc1acc14fca2 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -40,6 +40,8 @@
"amount": "Amount",
"ask-in-status": "Ask a question or report a bug",
"open": "Open",
+ "paste": "Paste",
+ "scan": "Scan",
"name-placeholder": "Display name",
"find": "Find",
"close-app-title": "Warning!",
@@ -61,6 +63,7 @@
"group-chat-member-removed": "*{{member}}* left the group",
"group-chat-admin-added": "*{{member}}* has been made admin",
"group-chat-no-contacts": "You don't have any contacts yet.\nInvite your friends to start chatting",
+ "wallet-no-contacts": "You don't have any contacts yet",
"agree-by-continuing": "By continuing you agree\n to our ",
"wallet-advanced": "Advanced",
"currency-display-name-sos": "Somalia Shilling",
@@ -115,6 +118,7 @@
"empty-chat-description": "There are no messages \nin this chat yet",
"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}}",
"welcome-to-status": "Welcome to Status",
"cryptokitty-name": "CryptoKitty #{{id}}",
@@ -233,6 +237,7 @@
"not-specified": "Not specified",
"delete-group": "Delete group",
"send-request": "Send request",
+ "send-amount": "Send amount",
"use-valid-qr-code": "This QR code doesn't contain a valid universal link, contact code or username: {{data}}",
"paste-json": "Paste JSON",
"browsing-title": "Browse",
@@ -411,6 +416,7 @@
"your-recovery-phrase": "Your recovery phrase",
"transaction-history": "Transaction history",
"send-transaction": "Send transaction",
+ "send-to": "Send to",
"currency-display-name-ltl": "Lithuanian Litas",
"step-i-of-n": "Step {{step}} of {{number}}",
"confirmations": "Confirmations",