From 6a342fd3a0736b7984d4971e3930ca6c49e05c81 Mon Sep 17 00:00:00 2001 From: janherich Date: Mon, 16 Jul 2018 15:08:37 +0200 Subject: [PATCH] Full implementation of the `/reauest` command + tests --- .../chat/commands/impl/transactions.cljs | 253 +++++++++++++----- .../commands/impl/transactions/styles.cljs | 86 +++++- .../test/chat/commands/impl/transactions.cljs | 41 ++- 3 files changed, 314 insertions(+), 66 deletions(-) diff --git a/src/status_im/chat/commands/impl/transactions.cljs b/src/status_im/chat/commands/impl/transactions.cljs index bb79b429c3..ede2e4ab6a 100644 --- a/src/status_im/chat/commands/impl/transactions.cljs +++ b/src/status_im/chat/commands/impl/transactions.cljs @@ -1,22 +1,27 @@ (ns status-im.chat.commands.impl.transactions (:require-macros [status-im.utils.views :refer [defview letsubs]]) (:require [clojure.string :as string] + [reagent.core :as reagent] [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.data-store.requests :as requests-store] [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.ui.components.animation :as animation] [status-im.i18n :as i18n] [status-im.constants :as constants] [status-im.utils.ethereum.core :as ethereum] [status-im.utils.ethereum.tokens :as tokens] + [status-im.utils.datetime :as datetime] [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])) +;; common `send/request` functionality + (defn- render-asset [selected-event-creator] (fn [{:keys [name symbol amount decimals] :as asset}] [react/touchable-highlight @@ -54,58 +59,6 @@ " " (:asset parameters))])) -(defview send-status [tx-hash outgoing] - (letsubs [confirmed? [:transaction-confirmed? tx-hash] - 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 transactions-styles/command-send-status-container - [vector-icons/icon (if confirmed? :icons/check :icons/dots) - {:color colors/blue - :container-style (transactions-styles/command-send-status-icon outgoing)}] - [react/view - [react/text {:style transactions-styles/command-send-status-text} - (i18n/label (cond - confirmed? :status-confirmed - tx-exists? :status-pending - :else :status-tx-not-found))]]]])) - -(defview send-preview - [{:keys [content timestamp-str outgoing group-chat]}] - (letsubs [network [:network-name]] - (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 transactions-styles/command-send-message-view - [react/view - [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 (transactions-styles/command-amount-currency-separator outgoing)} - "."] - [react/text {:style (transactions-styles/command-send-currency-text outgoing) - :font :default} - asset]]]] - (when fiat-amount - [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 transactions-styles/command-send-recipient-text} - (str - (i18n/label :send-sending-to) - " " - recipient-name)]) - [react/view - [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 @@ -158,6 +111,60 @@ {:title "Amount" :description (str "Max number of decimals is " asset-decimals)}))))) +;; `/send` command + +(defview send-status [tx-hash outgoing] + (letsubs [confirmed? [:transaction-confirmed? tx-hash] + 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 transactions-styles/command-send-status-container + [vector-icons/icon (if confirmed? :icons/check :icons/dots) + {:color colors/blue + :container-style (transactions-styles/command-send-status-icon outgoing)}] + [react/view + [react/text {:style transactions-styles/command-send-status-text} + (i18n/label (cond + confirmed? :status-confirmed + tx-exists? :status-pending + :else :status-tx-not-found))]]]])) + +(defview send-preview + [{:keys [content timestamp-str outgoing group-chat]}] + (letsubs [network [:network-name]] + (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 transactions-styles/command-send-message-view + [react/view + [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 (transactions-styles/command-amount-currency-separator outgoing)} + "."] + [react/text {:style (transactions-styles/command-send-currency-text outgoing) + :font :default} + asset]]]] + (when fiat-amount + [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 transactions-styles/command-send-recipient-text} + (str + (i18n/label :send-sending-to) + " " + recipient-name)]) + [react/view + [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])]]))) + ;; TODO(goranjovic) - update to include tokens in https://github.com/status-im/status-react/issues/3233 (defn- transaction-details [contact symbol] (-> contact @@ -166,8 +173,6 @@ :gas (ethereum/estimate-gas symbol) :from-chat? true))) -;; `/send` command - (deftype PersonalSendCommand [] protocol/Command (id [_] @@ -180,15 +185,15 @@ ;; 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]}] + (yield-control [_ {:keys [amount asset]} {: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) + symbol (keyword asset) {:keys [decimals]} (tokens/asset-for chain symbol)] (merge {:db (-> db - (send.events/set-and-validate-amount-db (:amount parameters) symbol decimals) + (send.events/set-and-validate-amount-db amount symbol decimals) (choose-recipient.events/fill-request-details (transaction-details recipient-contact symbol)) (update-in [:wallet :send-transaction] dissoc :id :password :wrong-password?) @@ -213,6 +218,124 @@ ;; `/request` command +(def request-message-icon-scale-delay 600) + +(defn set-chat-command [message-id command] + (let [metadata {:to-message-id message-id}] + (re-frame/dispatch [:select-chat-input-command command metadata]))) + +(def min-scale 1) +(def max-scale 1.3) + +(defn button-animation [val to-value loop? answered?] + (animation/anim-sequence + [(animation/anim-delay + (if (and @loop? (not @answered?)) + request-message-icon-scale-delay + 0)) + (animation/spring val {:toValue to-value + :useNativeDriver true})])) + +(defn request-button-animation-logic + [{:keys [to-value val loop? answered?] :as context}] + (animation/start + (button-animation val to-value loop? answered?) + #(if (and @loop? (not @answered?)) + (let [new-value (if (= to-value min-scale) max-scale min-scale) + context' (assoc context :to-value new-value)] + (request-button-animation-logic context')) + (animation/start + (button-animation val min-scale loop? answered?))))) + +(defn request-button-label + "The request button label will be in the form of `request-the-command-name`" + [command-name] + (keyword (str "request-" (name command-name)))) + +(defn request-button [message-id _ on-press-handler] + (let [scale-anim-val (animation/create-value min-scale) + answered? (re-frame/subscribe [:is-request-answered? message-id]) + loop? (reagent/atom true) + context {:to-value max-scale + :val scale-anim-val + :answered? answered? + :loop? loop?}] + (reagent/create-class + {:display-name "request-button" + :component-did-mount + (if (or (nil? on-press-handler) @answered?) (fn []) #(request-button-animation-logic context)) + :component-will-unmount + #(reset! loop? false) + :reagent-render + (fn [message-id {command-icon :icon :as command} on-press-handler] + (when command + [react/touchable-highlight + {:on-press on-press-handler + :style transactions-styles/command-request-image-touchable + :accessibility-label (request-button-label (:name command))} + [react/animated-view {:style (transactions-styles/command-request-image-view command scale-anim-val)} + (when command-icon + [react/icon command-icon transactions-styles/command-request-image])]]))}))) + +(defview request-preview + [{:keys [message-id content outgoing timestamp timestamp-str group-chat]} {:keys [db]}] + (letsubs [answered? [:is-request-answered? message-id] + status-initialized? [:get :status-module-initialized?] + network [:network-name] + prices [:prices]] + (let [{:keys [amount asset fiat-amount currency] request-network :network} (:params content) + recipient-name (get-in content [:params :bot-db :public :recipient]) + network-mismatch? (and request-network (not= request-network network)) + command (get-in db [:id->command ["send" #{:personal-chats}] :type]) + on-press-handler (cond + network-mismatch? nil + (and (not answered?) + status-initialized?) #(set-chat-command message-id command))] + [react/view + [react/touchable-highlight + {:on-press on-press-handler} + [react/view (transactions-styles/command-request-message-view outgoing) + [react/view + [react/view + [react/text {:style (transactions-styles/command-request-header-text outgoing)} + (i18n/label :transaction-request)]] + [react/view transactions-styles/command-request-row + [react/text {:style transactions-styles/command-request-amount-text + :font :medium} + amount + [react/text {:style (transactions-styles/command-amount-currency-separator outgoing)} + "."] + [react/text {:style (transactions-styles/command-request-currency-text outgoing) + :font :default} + asset]]] + [react/view transactions-styles/command-request-fiat-amount-row + [react/text {:style transactions-styles/command-request-fiat-amount-text} + (str "~ " fiat-amount " " (or currency (i18n/label :usd-currency)))]] + (when (and group-chat recipient-name) + [react/text {:style transactions-styles/command-request-recipient-text} + (str + (i18n/label :request-requesting-from) + " " + recipient-name)]) + (when network-mismatch? + [react/text {:style transactions-styles/command-request-network-text} + (str (i18n/label :on) " " request-network)]) + [react/view transactions-styles/command-request-timestamp-row + [react/text {:style (transactions-styles/command-request-timestamp-text outgoing)} + (str + (datetime/timestamp->mini-date timestamp) + " " + (i18n/label :at) + " " + timestamp-str)]] + (when-not outgoing + [react/view + [react/view transactions-styles/command-request-separator-line] + [react/view transactions-styles/command-request-button + [react/text {:style (transactions-styles/command-request-button-text answered?) + :on-press on-press-handler} + (i18n/label (if answered? :command-button-sent :command-button-send))]]])]]]]))) + (deftype PersonalRequestCommand [] protocol/Command (id [_] @@ -225,10 +348,14 @@ (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))) + (on-receive [_ {:keys [message-id chat-id]} {:keys [db]}] + (let [request {:chat-id chat-id + :message-id message-id + :response "send" + :status "open"}] + {:db (assoc-in db [:chats chat-id :requests message-id] request) + :data-store/tx [(requests-store/save-request-tx request)]})) (short-preview [_ command-message _] (personal-send-request-short-preview command-message)) - (preview [_ command-message _] - nil)) + (preview [_ command-message cofx] + (request-preview command-message cofx))) diff --git a/src/status_im/chat/commands/impl/transactions/styles.cljs b/src/status_im/chat/commands/impl/transactions/styles.cljs index a681c63166..375488d0df 100644 --- a/src/status_im/chat/commands/impl/transactions/styles.cljs +++ b/src/status_im/chat/commands/impl/transactions/styles.cljs @@ -1,6 +1,7 @@ (ns status-im.chat.commands.impl.transactions.styles (:require-macros [status-im.utils.styles :refer [defstyle]]) - (:require [status-im.ui.components.colors :as colors])) + (:require [status-im.ui.components.colors :as colors] + [status-im.ui.components.styles :as styles])) (def asset-container {:flex-direction :row @@ -88,6 +89,15 @@ :letter-spacing 1 :color (if outgoing colors/wild-blue-yonder colors/blue-transparent-40)}) +(defn command-request-currency-text [outgoing] + {:font-size 22 + :letter-spacing 1 + :color (if outgoing colors/wild-blue-yonder colors/gray)}) + +(defn command-request-timestamp-text [outgoing] + {:font-size 12 + :color (if outgoing colors/wild-blue-yonder colors/gray)}) + (def command-send-fiat-amount {:flex-direction :column :justify-content :flex-end @@ -106,3 +116,77 @@ {:color (if outgoing colors/wild-blue-yonder colors/gray) :margin-top 6 :font-size 12}) + +(def command-request-image-touchable + {:position :absolute + :top 0 + :right -8 + :align-items :center + :justify-content :center + :width 48 + :height 48}) + +(defn command-request-image-view [command scale] + {:width 32 + :height 32 + :border-radius 16 + :background-color (:color command) + :transform [{:scale scale}]}) + +(def command-request-image + {:position :absolute + :top 9 + :left 10 + :width 12 + :height 13}) + +(defn command-request-message-view [outgoing] + {:border-radius 14 + :padding-vertical 4 + :background-color (if outgoing colors/hawkes-blue styles/color-white)}) + +(defn command-request-header-text [outgoing] + {:font-size 12 + :color (if outgoing colors/wild-blue-yonder colors/gray)}) + +(def command-request-row + {:flex-direction :row + :margin-top 6}) + +(defstyle command-request-amount-text + {:font-size 22 + :ios {:letter-spacing -0.5} + :color colors/black}) + +(def command-request-separator-line + {:background-color colors/gray-light + :height 1 + :border-radius 8 + :margin-top 10}) + +(def command-request-button + {:align-items :center + :padding-top 8}) + +(defn command-request-button-text [answered?] + {:font-size 15 + :color (if answered? colors/gray colors/blue)}) + +(def command-request-fiat-amount-row + {:margin-top 6}) + +(def command-request-fiat-amount-text + {:font-size 12 + :color colors/black}) + +(def command-request-recipient-text + {:color colors/blue + :font-size 14 + :line-height 18}) + +(def command-request-network-text + {:color colors/red}) + +(def command-request-timestamp-row + {:margin-top 6}) + 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 5caacedb2d..df6b899394 100644 --- a/test/cljs/status_im/test/chat/commands/impl/transactions.cljs +++ b/test/cljs/status_im/test/chat/commands/impl/transactions.cljs @@ -3,8 +3,6 @@ [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" @@ -13,6 +11,10 @@ :address "0xAA" :whisper-identity "0xBB"}}}}) +;; testing the `/send` command + +(def personal-send-command (transactions/PersonalSendCommand.)) + (deftest personal-send-command-test (testing "That correct parameters are defined" (is (= (into #{} (map :id) (protocol/parameters personal-send-command)) @@ -36,3 +38,38 @@ (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))))) + +;; testing the `/request` command + +(def personal-request-command (transactions/PersonalRequestCommand.)) + +(deftest personal-send-command-test + (testing "That correct parameters are defined" + (is (= (into #{} (map :id) (protocol/parameters personal-request-command)) + #{:asset :amount}))) + (testing "Parameters validation" + (is (= (protocol/validate personal-request-command {:asset "TST"} cofx) + {:title "Invalid Asset" + :description "Unknown token - TST"})) + (is (= (protocol/validate personal-request-command {:asset "SNT"} cofx) + {:title "Amount" + :description "Amount must be specified"})) + (is (= (protocol/validate personal-request-command {:asset "SNT" :amount "a"} cofx) + {:title "Amount" + :description "Amount is not valid number"})) + (is (= (protocol/validate personal-request-command {:asset "ETH" :amount "0.54354353454353453453454353453445345545"} cofx) + {:title "Amount" + :description "Max number of decimals is 18"})) + (is (= (protocol/validate personal-request-command {:asset "ETH" :amount "0.01"} cofx) + nil))) + (testing "On receive adds pending request when `/request` command is received" + (let [fx (protocol/on-receive personal-request-command + {:chat-id "recipient" + :message-id "0xAA"} + cofx)] + (is (= (get-in fx [:db :chats "recipient" :requests "0xAA"]) + {:chat-id "recipient" + :message-id "0xAA" + :response "send" + :status "open"}))))) +