From 03598d47c22039bb71170626097dae87cb10bb7f Mon Sep 17 00:00:00 2001 From: janherich Date: Mon, 16 Jul 2018 01:07:34 +0200 Subject: [PATCH] Complete implementation of the `/send` command + tests --- .../chat/commands/impl/transactions.cljs | 179 ++++++++++++++---- .../commands/impl/transactions/styles.cljs | 73 +++++++ src/status_im/chat/commands/protocol.cljs | 3 +- .../chat/commands/styles/validation.cljs | 22 +++ src/status_im/chat/commands/validation.cljs | 10 + .../test/chat/commands/impl/transactions.cljs | 38 ++++ test/cljs/status_im/test/runner.cljs | 2 + 7 files changed, 287 insertions(+), 40 deletions(-) create mode 100644 src/status_im/chat/commands/styles/validation.cljs create mode 100644 src/status_im/chat/commands/validation.cljs create mode 100644 test/cljs/status_im/test/chat/commands/impl/transactions.cljs diff --git a/src/status_im/chat/commands/impl/transactions.cljs b/src/status_im/chat/commands/impl/transactions.cljs index e70d32e99d..bb79b429c3 100644 --- a/src/status_im/chat/commands/impl/transactions.cljs +++ b/src/status_im/chat/commands/impl/transactions.cljs @@ -1,14 +1,21 @@ (ns status-im.chat.commands.impl.transactions (: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.chat.commands.protocol :as protocol] + [status-im.chat.commands.impl.transactions.styles :as transactions-styles] + [status-im.chat.events.requests :as request-events] [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.components.list.views :as list] [status-im.i18n :as i18n] - [status-im.chat.commands.impl.transactions.styles :as transactions-styles] - [status-im.chat.styles.message.message :as message-styles])) + [status-im.constants :as constants] + [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.ethereum.tokens :as tokens] + [status-im.ui.screens.wallet.send.events :as send.events] + [status-im.ui.screens.wallet.choose-recipient.events :as choose-recipient.events] + [status-im.ui.screens.navigation :as navigation])) (defn- render-asset [selected-event-creator] (fn [{:keys [name symbol amount decimals] :as asset}] @@ -38,7 +45,7 @@ :keyboardShouldPersistTaps :always :bounces false}]])) -(defn send-short-preview +(defn personal-send-request-short-preview [{:keys [content]}] (let [parameters (:params content)] [react/text {} @@ -52,12 +59,12 @@ tx-exists? [:wallet-transaction-exists? tx-hash]] [react/touchable-highlight {:on-press #(when tx-exists? (re-frame/dispatch [:show-transaction-details tx-hash]))} - [react/view message-styles/command-send-status-container + [react/view transactions-styles/command-send-status-container [vector-icons/icon (if confirmed? :icons/check :icons/dots) {:color colors/blue - :container-style (message-styles/command-send-status-icon outgoing)}] + :container-style (transactions-styles/command-send-status-icon outgoing)}] [react/view - [react/text {:style message-styles/command-send-status-text} + [react/text {:style transactions-styles/command-send-status-text} (i18n/label (cond confirmed? :status-confirmed tx-exists? :status-pending @@ -69,65 +76,159 @@ (let [{{:keys [amount fiat-amount tx-hash asset currency] send-network :network} :params} content recipient-name (get-in content [:params :bot-db :public :recipient]) network-mismatch? (and (seq send-network) (not= network send-network))] - [react/view message-styles/command-send-message-view + [react/view transactions-styles/command-send-message-view [react/view - [react/view message-styles/command-send-amount-row - [react/view message-styles/command-send-amount - [react/text {:style message-styles/command-send-amount-text + [react/view transactions-styles/command-send-amount-row + [react/view transactions-styles/command-send-amount + [react/text {:style transactions-styles/command-send-amount-text :font :medium} amount - [react/text {:style (message-styles/command-amount-currency-separator outgoing)} + [react/text {:style (transactions-styles/command-amount-currency-separator outgoing)} "."] - [react/text {:style (message-styles/command-send-currency-text outgoing) + [react/text {:style (transactions-styles/command-send-currency-text outgoing) :font :default} asset]]]] (when fiat-amount - [react/view message-styles/command-send-fiat-amount - [react/text {:style message-styles/command-send-fiat-amount-text} + [react/view transactions-styles/command-send-fiat-amount + [react/text {:style transactions-styles/command-send-fiat-amount-text} (str "~ " fiat-amount " " (or currency (i18n/label :usd-currency)))]]) (when (and group-chat recipient-name) - [react/text {:style message-styles/command-send-recipient-text} + [react/text {:style transactions-styles/command-send-recipient-text} (str (i18n/label :send-sending-to) " " recipient-name)]) [react/view - [react/text {:style (message-styles/command-send-timestamp outgoing)} + [react/text {:style (transactions-styles/command-send-timestamp outgoing)} (str (i18n/label :sent-at) " " timestamp-str)]] [send-status tx-hash outgoing] (when network-mismatch? [react/text send-network])]]))) +(def personal-send-request-params + [{:id :asset + :type :text + :placeholder "Currency" + ;; Suggestion components should be structured in such way that they will just take + ;; one argument, event-creator fn used to construct event to fire whenever something + ;; is selected. + :suggestions choose-asset} + {:id :amount + :type :number + :placeholder "Amount"}]) + +;;TODO(goranjovic): currently we only allow tokens which are enabled in Manage assets here +;; because balances are only fetched for them. Revisit this decision with regard to battery/network consequences +;; if we were to update all balances. +(defn- allowed-assets [{:account/keys [account] :keys [chain]}] + (let [chain-keyword (keyword chain) + visible-tokens (get-in account [:settings :wallet :visible-tokens chain-keyword])] + (into {"ETH" 18} + (comp (filter #(and (not (:nft? %)) + (contains? visible-tokens (:symbol %)))) + (map (juxt (comp name :symbol) :decimals))) + (tokens/tokens-for chain-keyword)))) + +(defn- personal-send-request-validation [{:keys [asset amount]} {:keys [db]}] + (let [asset-decimals (get (allowed-assets db) asset)] + (cond + + (not asset-decimals) + {:title "Invalid Asset" + :description (str "Unknown token - " asset)} + + (not amount) + {:title "Amount" + :description "Amount must be specified"} + + :else + (let [sanitised-str (string/replace amount #"," ".") + portions (string/split sanitised-str ".") + decimals (get portions 1) + amount (js/parseFloat sanitised-str)] + (cond + + (or (js/isNaN amount) + (> (count portions) 2)) + {:title "Amount" + :description "Amount is not valid number"} + + (and decimals (> decimals asset-decimals)) + {:title "Amount" + :description (str "Max number of decimals is " asset-decimals)}))))) + +;; 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 :whisper-identity]) + (assoc :symbol symbol + :gas (ethereum/estimate-gas symbol) + :from-chat? true))) + +;; `/send` command + (deftype PersonalSendCommand [] protocol/Command (id [_] - :send) + "send") (scope [_] #{:personal-chats}) (parameters [_] - [{:id :asset - :type :text - :placeholder "Currency" - ;; Suggestion components should be structured in such way that they will just take - ;; one argument, event-creator fn used to construct event to fire whenever something - ;; is selected. - :suggestions choose-asset} - {:id :amount - :type :number - :placeholder "Amount"}]) - (validate [_ _ _] - ;; There is no validation for the `/send` command, as it's fully delegated to the wallet - nil) - (yield-control [_ parameters cofx] - ;; navigate to wallet - nil) - (on-send [_ message-id parameters cofx] - (when-let [tx-hash (get-in cofx [:db :wallet :send-transaction :tx-hash])] - {:dispatch [:update-transactions]})) + personal-send-request-params) + (validate [_ parameters cofx] + ;; Only superficial/formatting validation, "real validation" will be performed + ;; by the wallet, where we yield control in the next step + (personal-send-request-validation parameters cofx)) + (yield-control [_ parameters {:keys [db]}] + ;; Prefill wallet and navigate there + (let [recipient-contact (get-in db [:contacts/contacts (:current-chat-id db)]) + sender-account (:account/account db) + chain (keyword (:chain db)) + symbol (-> parameters :asset keyword) + {:keys [decimals]} (tokens/asset-for chain symbol)] + (merge {:db (-> db + (send.events/set-and-validate-amount-db (:amount parameters) symbol decimals) + (choose-recipient.events/fill-request-details + (transaction-details recipient-contact symbol)) + (update-in [:wallet :send-transaction] dissoc :id :password :wrong-password?) + (navigation/navigate-to + (if (:wallet-set-up-passed? sender-account) + :wallet-send-transaction-chat + :wallet-onboarding-setup)))} + (send.events/update-gas-price db false)))) + (on-send [_ _ _ _] + ;; TODO(janherich) - remove this once periodic updates are implemented + {:dispatch [:update-transactions]}) (on-receive [_ _ _] - nil) + ;; TODOD(janherich) - this just copyies the current logic but still seems super weird, + ;; remove/reconsider once periodic updates are implemented + {:dispatch [:update-transactions] + :dispatch-later [{:ms constants/command-send-status-update-interval-ms + :dispatch [:update-transactions]}]}) (short-preview [_ command-message _] - (send-short-preview command-message)) + (personal-send-request-short-preview command-message)) (preview [_ command-message _] (send-preview command-message))) + +;; `/request` command + +(deftype PersonalRequestCommand [] + protocol/Command + (id [_] + "request") + (scope [_] + #{:personal-chats}) + (parameters [_] + personal-send-request-params) + (validate [_ parameters cofx] + (personal-send-request-validation parameters cofx)) + (yield-control [_ _ _]) + (on-send [_ _ _ _]) + (on-receive [_ command-message cofx] + (let [{:keys [chat-id message-id]} command-message] + (request-events/add-request chat-id message-id cofx))) + (short-preview [_ command-message _] + (personal-send-request-short-preview command-message)) + (preview [_ command-message _] + nil)) diff --git a/src/status_im/chat/commands/impl/transactions/styles.cljs b/src/status_im/chat/commands/impl/transactions/styles.cljs index 3200215236..a681c63166 100644 --- a/src/status_im/chat/commands/impl/transactions/styles.cljs +++ b/src/status_im/chat/commands/impl/transactions/styles.cljs @@ -1,4 +1,5 @@ (ns status-im.chat.commands.impl.transactions.styles + (:require-macros [status-im.utils.styles :refer [defstyle]]) (:require [status-im.ui.components.colors :as colors])) (def asset-container @@ -33,3 +34,75 @@ {:height 1 :background-color colors/gray-light :margin-left 56}) + +(def command-send-status-container + {:margin-top 6 + :flex-direction :row}) + +(defn command-send-status-icon [outgoing] + {:background-color (if outgoing + colors/blue-darker + colors/blue-transparent) + :width 24 + :height 24 + :border-radius 16 + :padding-top 4 + :padding-left 4}) + +(defstyle command-send-status-text + {:color colors/blue + :android {:margin-top 3} + :ios {:margin-top 4} + :margin-left 6 + :font-size 12}) + +(def command-send-message-view + {:flex-direction :column + :align-items :flex-start}) + +(def command-send-amount-row + {:flex-direction :row + :justify-content :space-between}) + +(def command-send-amount + {:flex-direction :column + :align-items :flex-end + :max-width 250}) + +(defstyle command-send-amount-text + {:font-size 22 + :color colors/blue + :ios {:letter-spacing -0.5}}) + +(def command-send-currency + {:flex-direction :column + :align-items :flex-end}) + +(defn command-amount-currency-separator [outgoing] + {:opacity 0 + :color (if outgoing colors/hawkes-blue colors/white)}) + +(defn command-send-currency-text [outgoing] + {:font-size 22 + :margin-left 4 + :letter-spacing 1 + :color (if outgoing colors/wild-blue-yonder colors/blue-transparent-40)}) + +(def command-send-fiat-amount + {:flex-direction :column + :justify-content :flex-end + :margin-top 6}) + +(def command-send-fiat-amount-text + {:font-size 12 + :color colors/black}) + +(def command-send-recipient-text + {:color colors/blue + :font-size 14 + :line-height 18}) + +(defn command-send-timestamp [outgoing] + {:color (if outgoing colors/wild-blue-yonder colors/gray) + :margin-top 6 + :font-size 12}) diff --git a/src/status_im/chat/commands/protocol.cljs b/src/status_im/chat/commands/protocol.cljs index 4476a51ed9..e659d6db88 100644 --- a/src/status_im/chat/commands/protocol.cljs +++ b/src/status_im/chat/commands/protocol.cljs @@ -27,7 +27,8 @@ (validate [this parameters cofx] "Function validating the parameters once command is send. Takes parameters map and `cofx` map as argument, returns either `nil` meaning that no errors were - found and command send workflow can proceed, or sequence of errors to display") + found and command send workflow can proceed, or one/more errors to display. + Each error is represented by the map containing `:title` and `:description` keys.") (yield-control [this parameters cofx] "Optional function, which if implemented, can step out of the normal command workflow (`validate-and-send`) and yield control back to application before sending. diff --git a/src/status_im/chat/commands/styles/validation.cljs b/src/status_im/chat/commands/styles/validation.cljs new file mode 100644 index 0000000000..e993788de0 --- /dev/null +++ b/src/status_im/chat/commands/styles/validation.cljs @@ -0,0 +1,22 @@ +(ns status-im.chat.commands.styles.validation + (:require [status-im.ui.components.styles :as common])) + +(defn root [bottom] + {:flex-direction :column + :left 0 + :right 0 + :bottom bottom + :position :absolute}) + +(def message-container + {:background-color common/color-red + :padding 16}) + +(def message-title + {:color common/color-white + :font-size 12}) + +(def message-description + {:color common/color-white + :font-size 12 + :opacity 0.9}) diff --git a/src/status_im/chat/commands/validation.cljs b/src/status_im/chat/commands/validation.cljs new file mode 100644 index 0000000000..68bc9cd9dd --- /dev/null +++ b/src/status_im/chat/commands/validation.cljs @@ -0,0 +1,10 @@ +(ns status-im.chat.commands.validation + (:require [status-im.ui.components.react :as react] + [status-im.chat.commands.styles.validation :as styles])) + +(defn validation-message [{:keys [title description]}] + [react/view styles/message-container + [react/text {:style styles/message-title} + title] + [react/text {:style styles/message-description} + description]]) diff --git a/test/cljs/status_im/test/chat/commands/impl/transactions.cljs b/test/cljs/status_im/test/chat/commands/impl/transactions.cljs new file mode 100644 index 0000000000..5caacedb2d --- /dev/null +++ b/test/cljs/status_im/test/chat/commands/impl/transactions.cljs @@ -0,0 +1,38 @@ +(ns status-im.test.chat.commands.impl.transactions + (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.chat.commands.impl.transactions :as transactions] + [status-im.chat.commands.protocol :as protocol])) + +(def personal-send-command (transactions/PersonalSendCommand.)) + +(def cofx {:db {:account/account {:settings {:wallet {:visible-tokens {:mainnet #{:SNT}}}} + :wallet-set-up-passed? true} + :chain "mainnet" + :current-chat-id "recipient" + :contacts/contacts {"recipient" {:name "Recipient" + :address "0xAA" + :whisper-identity "0xBB"}}}}) + +(deftest personal-send-command-test + (testing "That correct parameters are defined" + (is (= (into #{} (map :id) (protocol/parameters personal-send-command)) + #{:asset :amount}))) + (testing "Parameters validation" + (is (= (protocol/validate personal-send-command {:asset "TST"} cofx) + {:title "Invalid Asset" + :description "Unknown token - TST"})) + (is (= (protocol/validate personal-send-command {:asset "SNT"} cofx) + {:title "Amount" + :description "Amount must be specified"})) + (is (= (protocol/validate personal-send-command {:asset "SNT" :amount "a"} cofx) + {:title "Amount" + :description "Amount is not valid number"})) + (is (= (protocol/validate personal-send-command {:asset "ETH" :amount "0.54354353454353453453454353453445345545"} cofx) + {:title "Amount" + :description "Max number of decimals is 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 {: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))))) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index 81c3919cc6..d42f41bf1c 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -27,6 +27,7 @@ [status-im.test.chat.views.message] [status-im.test.chat.views.photos] [status-im.test.chat.commands.core] + [status-im.test.chat.commands.impl.transactions] [status-im.test.i18n] [status-im.test.protocol.web3.inbox] [status-im.test.utils.utils] @@ -82,6 +83,7 @@ 'status-im.test.chat.views.message 'status-im.test.chat.views.photos 'status-im.test.chat.commands.core + 'status-im.test.chat.commands.impl.transactions 'status-im.test.i18n 'status-im.test.transport.core 'status-im.test.transport.inbox