feature #3231 and #4012 - unified chat and wallet workflow

Signed-off-by: Julien Eluard <julien.eluard@gmail.com>
This commit is contained in:
Goran Jovic 2018-05-14 16:22:03 +02:00 committed by Julien Eluard
parent d4d4406341
commit f16bd200e8
No known key found for this signature in database
GPG Key ID: 6FD7DB5437FCBEF6
13 changed files with 139 additions and 375 deletions

View File

@ -1,114 +1,8 @@
// Send command/response
function calculateFee(n, tx) {
var estimatedGas = 21000;
if (tx !== null) {
try {
estimatedGas = web3.eth.estimateGas(tx);
} catch (err) {
}
}
function amountParameterBox(params, context) {
var gasMultiplicator = Math.pow(1.4, n).toFixed(3);
var gasPrice = 211000000000;
try {
gasPrice = web3.eth.gasPrice;
} catch (err) {
}
var weiFee = gasPrice * gasMultiplicator * estimatedGas;
// force fee in eth to be of BigNumber type
var ethFee = web3.toBigNumber(web3.fromWei(weiFee, "ether"));
// always display 7 decimal places
return ethFee.toFixed(7);
}
function calculateGasPrice(n) {
var gasMultiplicator = Math.pow(1.4, n).toFixed(3);
var gasPrice = 211000000000;
try {
gasPrice = web3.eth.gasPrice;
} catch (err) {
}
return gasPrice * gasMultiplicator;
}
status.defineSubscription(
"calculatedFee",
{value: ["sliderValue"], tx: ["transaction"]},
function (params) {
return calculateFee(params.value, params.tx);
}
);
function getFeeExplanation(n) {
return I18n.t('send_explanation') + I18n.t('send_explanation_' + (n + 2));
}
status.defineSubscription(
"feeExplanation",
{value: ["sliderValue"]},
function(params) {
return getFeeExplanation(params.value);
}
);
function amountParameterBox(groupChat, params, context) {
if (!params["bot-db"]) {
params["bot-db"] = {};
}
var contactAddress;
if (groupChat) {
if (params["bot-db"]["public"] && params["bot-db"]["public"]["recipient"]) {
contactAddress = params["bot-db"]["public"]["recipient"]["address"];
} else {
contactAddress = null;
}
} else {
contactAddress = context.to;
}
var txData;
var amount;
var amountIndex = groupChat ? 1 : 0;
try {
amount = params.args[amountIndex].replace(",", ".");
txData = {
to: contactAddress,
value: web3.toWei(amount) || 0
};
} catch (err) {
amount = null;
txData = {
to: contactAddress,
value: 0
};
}
var sliderValue = params["bot-db"]["sliderValue"] || 0;
try {
status.setDefaultDb({
transaction: txData,
calculatedFee: calculateFee(sliderValue, txData),
feeExplanation: getFeeExplanation(sliderValue),
sliderValue: sliderValue
});
} catch (err) {
status.setDefaultDb({
transaction: txData,
calculatedFee: "0",
feeExplanation: "",
sliderValue: sliderValue
});
}
return {
title: I18n.t('send_title'),
@ -127,128 +21,7 @@ function amountParameterBox(groupChat, params, context) {
}
},
I18n.t('send_specify_amount')
),
status.components.touchable(
{
onPress: status.components.dispatch([status.events.FOCUS_INPUT, []])
},
status.components.view({
flexDirection: "row",
alignItems: "center",
textAlign: "center",
justifyContent: "center"
}, [
status.components.text({
font: "light",
numberOfLines: 1,
ellipsizeMode: "tail",
style: {
maxWidth: 250,
fontSize: 38,
marginLeft: 8,
color: "black"
},
accessibilityLabel: "amount-text"
},
amount || "0.00"
),
status.components.text({
font: "light",
style: {
fontSize: 38,
marginLeft: 8,
color: "rgb(147, 155, 161)"
}
},
I18n.t('eth')
),
])
),
status.components.text({
style: {
fontSize: 14,
color: "rgb(147, 155, 161)",
paddingTop: 14,
paddingLeft: 16,
paddingRight: 16,
paddingBottom: 5
}
},
I18n.t('send_fee')
),
status.components.view({
flexDirection: "row"
}, [
status.components.text({
style: {
fontSize: 17,
color: "black",
paddingLeft: 16
},
accessibilityLabel: "fee-text",
}, [status.components.subscribe(["calculatedFee"])]),
status.components.text({
style: {
fontSize: 17,
color: "rgb(147, 155, 161)",
paddingLeft: 4,
paddingRight: 4
}
},
I18n.t('eth')
)
]),
status.components.slider({
maximumValue: 2,
minimumValue: -2,
onSlidingComplete: status.components.dispatch(
[status.events.UPDATE_DB, "sliderValue"]
),
step: 1,
style: {
marginLeft: 16,
marginRight: 16
},
accessibilityLabel: "adjust-fee-slider-button"
}),
status.components.view({
flexDirection: "row"
}, [
status.components.text({
style: {
flex: 1,
fontSize: 14,
color: "rgb(147, 155, 161)",
paddingLeft: 16,
alignSelf: "flex-start"
}
},
I18n.t('send_cheaper')
),
status.components.text({
style: {
flex: 1,
fontSize: 14,
color: "rgb(147, 155, 161)",
paddingRight: 16,
alignSelf: "flex-end",
textAlign: "right"
}
},
I18n.t('send_faster')
)
]),
status.components.text({
style: {
fontSize: 14,
color: "black",
paddingTop: 16,
paddingLeft: 16,
paddingRight: 16,
paddingBottom: 16,
lineHeight: 24
}
}, [status.components.subscribe(["feeExplanation"])])
)
])
};
}
@ -264,34 +37,19 @@ var recipientSendParam = {
}
};
function amountSendParam(groupChat) {
function amountSendParam() {
return {
name: "amount",
type: status.types.NUMBER,
suggestions: amountParameterBox.bind(this, groupChat)
suggestions: amountParameterBox.bind(this)
};
}
var paramsPersonalSend = [amountSendParam(false)];
var paramsGroupSend = [recipientSendParam, amountSendParam(true)];
var paramsPersonalSend = [amountSendParam()];
var paramsGroupSend = [recipientSendParam, amountSendParam()];
function validateSend(validateRecipient, params, context) {
if (!params["bot-db"]) {
params["bot-db"] = {};
}
if (validateRecipient) {
if (!params["bot-db"]["public"]
|| !params["bot-db"]["public"]["recipient"]
|| !params["bot-db"]["public"]["recipient"]["address"]) {
return {
markup: status.components.validationMessage(
"Wrong address",
"Recipient address must be specified"
)
};
}
}
if (!params["amount"]) {
return {
@ -336,73 +94,11 @@ function validateSend(validateRecipient, params, context) {
};
}
try {
var balance = web3.eth.getBalance(context.from);
if (isNaN(balance)) {
throw new Error();
}
} catch (err) {
return {
markup: status.components.validationMessage(
I18n.t('validation_internal_title'),
I18n.t('validation_balance')
)
};
}
var fee = calculateFee(
params["bot-db"]["sliderValue"],
{
to: context.to,
value: val
}
);
if (bn(val).plus(bn(web3.toWei(fee, "ether"))).greaterThan(bn(balance))) {
return {
markup: status.components.validationMessage(
I18n.t('validation_title'),
I18n.t('validation_insufficient_amount')
+ web3.fromWei(balance, "ether")
+ " ETH)"
)
};
}
}
function handleSend(groupChat, params, context) {
var val = web3.toWei(params["amount"].replace(",", "."), "ether");
function handleSend(params, context) {
var gasPrice = calculateGasPrice(params["bot-db"]["sliderValue"]);
var data = {
from: context.from,
value: val,
gas: web3.toBigNumber(21000)
};
if (groupChat) {
data.to = params["bot-db"]["public"]["recipient"]["address"];
} else {
data.to = context.to;
}
if (gasPrice) {
data.gasPrice = gasPrice;
}
web3.eth.sendTransaction(data, function(error, hash) {
if (error) {
// Do nothing, as error handling will be done as response to transaction.failed event from go
} else {
status.sendSignal("handler-result", {
status: "success",
hash: hash,
origParams: context["orig-params"]
});
}
});
// async handler, so we don't return anything immediately
}
function previewSend(showRecipient, params, context) {
@ -513,8 +209,8 @@ var personalSend = {
description: I18n.t('send_description'),
params: paramsPersonalSend,
validator: validateSend.bind(this, false),
handler: handleSend.bind(this, false),
asyncHandler: true,
handler: handleSend.bind(this),
asyncHandler: false,
preview: previewSend.bind(this, false),
shortPreview: shortPreviewSend
};
@ -528,8 +224,8 @@ var groupSend = {
description: I18n.t('send_description'),
params: paramsGroupSend,
validator: validateSend.bind(this, true),
handler: handleSend.bind(this, true),
asyncHandler: true,
handler: handleSend.bind(this),
asyncHandler: false,
preview: previewSend.bind(this, true),
shortPreview: shortPreviewSend
};

View File

@ -4,15 +4,6 @@ I18n.translations = {
send_description: 'Send a payment',
send_choose_recipient: 'Choose recipient',
send_specify_amount: 'Specify amount',
send_fee: 'Fee',
send_cheaper: 'Cheaper',
send_faster: 'Faster',
send_explanation: 'This is the most amount of money that might be used to process this transaction. Your transaction will be mined ',
send_explanation_0: 'in a few minutes or more.',
send_explanation_1: 'likely within a few minutes.',
send_explanation_2: 'usually within a minute.',
send_explanation_3: 'probably within 30 seconds.',
send_explanation_4: 'probably within a few seconds.',
send_sending_to: 'to ',
eth: 'ETH',

View File

@ -23,7 +23,7 @@
[status-im.data-store.chats :as chats-store]
[status-im.data-store.messages :as messages-store]
[status-im.data-store.contacts :as contacts-store]
status-im.chat.events.commands
[status-im.chat.events.commands :as events.commands]
status-im.chat.events.requests
status-im.chat.events.send-message
status-im.chat.events.receive-message
@ -298,6 +298,13 @@
(fn [cofx [chat-id opts]]
(navigate-to-chat chat-id opts cofx)))
(handlers/register-handler-fx
:execute-stored-command-and-return-to-chat
(fn [cofx [_ chat-id]]
(handlers-macro/merge-fx cofx
(events.commands/execute-stored-command)
(navigate-to-chat chat-id {}))))
(defn start-chat
"Start a chat, making sure it exists"
[chat-id opts {:keys [db] :as cofx}]

View File

@ -5,7 +5,8 @@
[status-im.utils.ethereum.core :as ethereum]
[status-im.utils.handlers :as handlers]
[status-im.i18n :as i18n]
[status-im.utils.platform :as platform]))
[status-im.utils.platform :as platform]
[status-im.chat.events.shortcuts :as shortcuts]))
;;;; Helper fns
@ -55,11 +56,17 @@
(when proceed-event-creator
{:dispatch (proceed-event-creator returned)})))
(defn short-preview? [opts]
(= :short-preview (:data-type opts)))
(handlers/register-handler-fx
:request-command-message-data
[re-frame/trim-v (re-frame/inject-cofx :data-store/get-local-storage-data)]
(fn [{:keys [db]} [message opts]]
(request-command-message-data db message opts)))
(if (and (short-preview? opts)
(shortcuts/shortcut-override? message))
(shortcuts/shortcut-override-fx db message opts)
(request-command-message-data db message opts))))
(handlers/register-handler-fx
:execute-command-immediately
@ -70,3 +77,10 @@
{:dispatch [:request-permissions {:permissions [:read-external-storage]
:on-allowed #(re-frame/dispatch [:initialize-geth])}]}
(log/debug "ignoring command: " command-name))))
;; NOTE(goranjovic) - continues execution of a command that was paused by a shortcut
(defn execute-stored-command [{:keys [db]}]
(let [{:keys [message opts]} (:commands/stored-command db)]
(-> db
(request-command-message-data message opts)
(dissoc :commands/stored-command))))

View File

@ -10,10 +10,7 @@
[status-im.chat.events.commands :as commands-events]
[status-im.bots.events :as bots-events]
[status-im.ui.components.react :as react-comp]
[status-im.utils.datetime :as time]
[status-im.utils.handlers :as handlers]
[status-im.utils.random :as random]
[status-im.i18n :as i18n]))
[status-im.utils.handlers :as handlers]))
;;;; Effects
@ -328,15 +325,23 @@
proceed-events)]
{:dispatch-n events})))
(defn cleanup-chat-command [db]
(-> (model/set-chat-ui-props db {:sending-in-progress? false})
(clear-seq-arguments)
(set-chat-input-metadata nil)
(set-chat-input-text nil)))
(handlers/register-handler-fx
:cleanup-chat-command
(fn [{:keys [db]}]
{:db (cleanup-chat-command db)}))
(handlers/register-handler-fx
::send-command
message-model/send-interceptors
(fn [{:keys [db] :as cofx} [command-message]]
(let [{:keys [current-chat-id current-public-key]} db
new-db (-> (model/set-chat-ui-props db {:sending-in-progress? false})
(clear-seq-arguments)
(set-chat-input-metadata nil)
(set-chat-input-text nil))
new-db (cleanup-chat-command db)
address (get-in db [:account/account :address])]
(merge {:db new-db}
(message-model/process-command (assoc cofx :db new-db)

View File

@ -0,0 +1,40 @@
(ns status-im.chat.events.shortcuts
(:require [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]
[status-im.utils.ethereum.core :as ethereum]))
;; TODO(goranjovic) - update to include tokens in https://github.com/status-im/status-react/issues/3233
(defn- transaction-details [contact]
(-> contact
(select-keys [:name :address :whisper-identity])
(assoc :symbol :ETH
:gas (ethereum/estimate-gas :ETH)
:from-chat? true)))
(defn send-shortcut-fx [db contact params]
(merge {:db (-> db
(send.events/set-and-validate-amount-db (:amount params))
(choose-recipient.events/fill-request-details (transaction-details contact))
(navigation/navigate-to :wallet-send-transaction-chat))}
(send.events/update-gas-price db false)))
(def shortcuts
{"send" send-shortcut-fx})
(defn shortcut-override? [message]
(get shortcuts (get-in message [:content :command])))
(defn shortcut-override-fx [db {:keys [chat-id content] :as message} opts]
(let [command (:command content)
contact (get-in db [:contacts/contacts chat-id])
shortcut-specific-fx (get shortcuts command)
stored-command {:message message :opts opts}]
(-> db
(assoc :commands/stored-command stored-command)
;; NOTE(goranjovic) - stores the command if we want to continue it after
;; shortcut has been executed, see `:execute-stored-command`
(shortcut-specific-fx contact (:params content))
;; TODO(goranjovic) - replace this dispatch with a function call
;; Need to refactor chat events namespaces for this to avoid circular dependecy
(assoc :dispatch [:cleanup-chat-command]))))

View File

@ -147,6 +147,7 @@
:contacts/click-handler
:contacts/click-action
:contacts/click-params
:commands/stored-command
:group/selected-contacts
:accounts/accounts
:accounts/create

View File

@ -122,6 +122,7 @@
:dapp-description dapp-description
:wallet-onboarding-setup wallet.onboarding.setup/screen
:wallet-send-transaction send-transaction
:wallet-send-transaction-chat send-transaction
:wallet-transaction-sent transaction-sent
:wallet-request-transaction request-transaction
:wallet-send-transaction-request send-transaction-request

View File

@ -13,7 +13,7 @@
toggled-state (if (= :on flashlight-state) :off :on)]
(assoc-in db [:wallet :send-transaction :camera-flashlight] toggled-state))))
(defn- fill-request-details [db {:keys [address name value symbol gas gasPrice whisper-identity]}]
(defn- fill-request-details [db {:keys [address name value symbol gas gasPrice whisper-identity from-chat?]}]
{:pre [(not (nil? address))]}
(update-in
db [:wallet :send-transaction]
@ -22,6 +22,7 @@
symbol (assoc :symbol symbol)
gas (assoc :gas (money/bignumber gas))
gasPrice (assoc :gas-price (money/bignumber gasPrice))
from-chat? (assoc :from-chat? from-chat?)
(and symbol (not gasPrice))
(assoc :gas-price (ethereum/estimate-gas symbol)))))

View File

@ -237,13 +237,6 @@
:from-chat? from-chat?})
:dispatch [:navigate-to-modal :wallet-send-transaction-modal]}))
(handlers/register-handler-fx
:wallet/update-gas-price
(fn [{:keys [db]} [_ edit?]]
{:update-gas-price {:web3 (:web3 db)
:success-event :wallet/update-gas-price-success
:edit? edit?}}))
(handlers/register-handler-db
:wallet/update-gas-price-success
(fn [db [_ price edit?]]

View File

@ -68,14 +68,17 @@
(re-frame/dispatch [::transaction-completed {:id (name (key result)) :response (second result)} modal?]))
;;;; Handlers
(defn set-and-validate-amount-db [db amount]
(let [{:keys [value error]} (wallet.db/parse-amount amount)]
(-> db
(assoc-in [:wallet :send-transaction :amount] (money/ether->wei value))
(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]]
(let [{:keys [value error]} (wallet.db/parse-amount amount)]
{:db (-> db
(assoc-in [:wallet :send-transaction :amount] (money/ether->wei value))
(assoc-in [:wallet :send-transaction :amount-text] amount)
(assoc-in [:wallet :send-transaction :amount-error] error))})))
{:db (set-and-validate-amount-db db amount)}))
(handlers/register-handler-fx
:wallet.send/set-symbol
@ -347,10 +350,21 @@
assoc
:gas (ethereum/estimate-gas (get-in db [:wallet :send-transaction :symbol])))}))
(defn update-gas-price [db edit?]
{:update-gas-price {:web3 (:web3 db)
:success-event :wallet/update-gas-price-success
:edit? edit?}})
(handlers/register-handler-fx
:wallet/update-gas-price
(fn [{:keys [db]} [_ edit?]]
(update-gas-price db edit?)))
(handlers/register-handler-fx
:close-transaction-sent-screen
(fn [{:keys [db]} _]
{:dispatch (if (= :wallet-send-transaction (second (:navigation-stack db)))
[:navigate-to-clean :wallet]
[:navigate-back])
(fn [{:keys [db]} [_ chat-id]]
{:dispatch (condp = (second (:navigation-stack db))
:wallet-send-transaction [:navigate-to-clean :wallet]
:wallet-send-transaction-chat [:execute-stored-command-and-return-to-chat chat-id]
[:navigate-back])
:dispatch-later [{:ms 400 :dispatch [:check-transactions-queue]}]}))

View File

@ -12,26 +12,27 @@
[status-im.utils.platform :as platform]))
(defview transaction-sent [& [modal?]]
[react/view wallet.styles/wallet-modal-container
[status-bar/status-bar {:type (if modal? :modal-wallet :transparent)}]
[react/view styles/transaction-sent-container
[react/view styles/ok-icon-container
[vi/icon :icons/ok {:color components.styles/color-blue4}]]
[react/text {:style styles/transaction-sent
:font (if platform/android? :medium :default)
:accessibility-label :transaction-sent-text}
(i18n/label :t/transaction-sent)]
[react/view styles/gap]
[react/text {:style styles/transaction-sent-description} (i18n/label :t/transaction-description)]]
[react/view components.styles/flex]
[components/separator]
[react/touchable-highlight {:on-press #(re-frame/dispatch [:close-transaction-sent-screen])
:accessibility-label :got-it-button}
[react/view styles/got-it-container
[react/text {:style styles/got-it
:font (if platform/android? :medium :default)
:uppercase? true}
(i18n/label :t/got-it)]]]])
(letsubs [chat-id [:get-current-chat-id]]
[react/view wallet.styles/wallet-modal-container
[status-bar/status-bar {:type (if modal? :modal-wallet :transparent)}]
[react/view styles/transaction-sent-container
[react/view styles/ok-icon-container
[vi/icon :icons/ok {:color components.styles/color-blue4}]]
[react/text {:style styles/transaction-sent
:font (if platform/android? :medium :default)
:accessibility-label :transaction-sent-text}
(i18n/label :t/transaction-sent)]
[react/view styles/gap]
[react/text {:style styles/transaction-sent-description} (i18n/label :t/transaction-description)]]
[react/view components.styles/flex]
[components/separator]
[react/touchable-highlight {:on-press #(re-frame/dispatch [:close-transaction-sent-screen chat-id])
:accessibility-label :got-it-button}
[react/view styles/got-it-container
[react/text {:style styles/got-it
:font (if platform/android? :medium :default)
:uppercase? true}
(i18n/label :t/got-it)]]]]))
(defview transaction-sent-modal []
[transaction-sent true])

View File

@ -212,13 +212,13 @@
:on-content-size-change #(when (and (not modal?) scroll @scroll)
(.scrollToEnd @scroll))}
[react/view styles/send-transaction-form
[components/recipient-selector {:disabled? modal?
[components/recipient-selector {:disabled? (or from-chat? modal?)
:address to
:name to-name}]
[components/asset-selector {:disabled? modal?
[components/asset-selector {:disabled? (or from-chat? modal?)
:type :send
:symbol symbol}]
[components/amount-selector {:disabled? modal?
[components/amount-selector {:disabled? (or from-chat? modal?)
:error (or amount-error
(when-not sufficient-funds? (i18n/label :t/wallet-insufficient-funds)))
:input-options {:max-length 21