From 14bdc5a78c8529742754d69b8e45693b06b380fe Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Sep 2017 15:07:12 -0230 Subject: Client side error handling for from, to and amount fields in send.js --- ui/app/conversion-util.js | 13 +++ ui/app/css/itcss/components/send.scss | 15 +++ ui/app/send.js | 195 ++++++++++++++++++++++++++-------- ui/app/util.js | 5 + 4 files changed, 184 insertions(+), 44 deletions(-) diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 0ede77487..7e02fe2bd 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -123,7 +123,20 @@ const addCurrencies = (a, b, { toNumericBase, numberOfDecimals }) => { }) } +const conversionGreaterThan = ( + { value, fromNumericBase }, + { value: compareToValue, fromNumericBase: compareToBase }, +) => { + const firstValue = converter({ value, fromNumericBase }) + const secondValue = converter({ + value: compareToValue, + fromNumericBase: compareToBase, + }) + return firstValue.gt(secondValue) +} + module.exports = { conversionUtil, addCurrencies, + conversionGreaterThan, } \ No newline at end of file diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 2d6374aa2..84f678130 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -104,6 +104,16 @@ color: $red; } } + + .send-screen-input-wrapper__error-message { + display: block; + position: absolute; + bottom: 4px; + font-size: 12px; + line-height: 12px; + left: 8px; + color: $red; + } } .send-screen-input { @@ -295,6 +305,11 @@ width: 163px; text-align: center; } + + &__send-button__disabled { + opacity: 0.5; + cursor: auto; + } } .send-token { diff --git a/ui/app/send.js b/ui/app/send.js index 481682bc8..16fe470be 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -18,8 +18,8 @@ const { signTx, } = require('./actions') const { stripHexPrefix, addHexPrefix } = require('ethereumjs-util') -const { isHex, numericBalance, isValidAddress } = require('./util') -const { conversionUtil } = require('./conversion-util') +const { isHex, numericBalance, isValidAddress, allNull } = require('./util') +const { conversionUtil, conversionGreaterThan } = require('./conversion-util') const BigNumber = require('bignumber.js') module.exports = connect(mapStateToProps)(SendTransactionScreen) @@ -51,7 +51,7 @@ function mapStateToProps (state) { error: warning && warning.split('.')[0], account, identity: identities[address], - balance: account ? numericBalance(account.balance) : null, + balance: account ? account.balance : null, } } @@ -65,8 +65,8 @@ function SendTransactionScreen () { newTx: { from: '', to: '', - // these values are hardcoded, so "Next" can be clicked - amount: '0x0', // see L544 + amount: 0, + amountToSend: '0x0', gasPrice: '0x5d21dba00', gas: '0x7b0d', txData: null, @@ -74,6 +74,8 @@ function SendTransactionScreen () { }, activeCurrency: 'USD', tooltipIsOpen: false, + errors: {}, + isValid: false, } this.back = this.back.bind(this) @@ -81,12 +83,26 @@ function SendTransactionScreen () { this.onSubmit = this.onSubmit.bind(this) this.setActiveCurrency = this.setActiveCurrency.bind(this) this.toggleTooltip = this.toggleTooltip.bind(this) + this.validate = this.validate.bind(this) + this.getAmountToSend = this.getAmountToSend.bind(this) + this.setErrorsFor = this.setErrorsFor.bind(this) + this.clearErrorsFor = this.clearErrorsFor.bind(this) this.renderFromInput = this.renderFromInput.bind(this) this.renderToInput = this.renderToInput.bind(this) this.renderAmountInput = this.renderAmountInput.bind(this) this.renderGasInput = this.renderGasInput.bind(this) this.renderMemoInput = this.renderMemoInput.bind(this) + this.renderErrorMessage = this.renderErrorMessage.bind(this) +} + +SendTransactionScreen.prototype.renderErrorMessage = function(errorType, warning) { + const { errors } = this.state + const errorMessage = errors[errorType]; + + return errorMessage || warning + ? h('div.send-screen-input-wrapper__error-message', [ errorMessage || warning ]) + : null } SendTransactionScreen.prototype.renderFromInput = function (from, identities) { @@ -106,6 +122,8 @@ SendTransactionScreen.prototype.renderFromInput = function (from, identities) { }, }) }, + onBlur: () => this.setErrorsFor('from'), + onFocus: () => this.clearErrorsFor('from'), }), h('datalist#accounts', [ @@ -118,6 +136,8 @@ SendTransactionScreen.prototype.renderFromInput = function (from, identities) { }), ]), + this.renderErrorMessage('from'), + ]) } @@ -139,6 +159,8 @@ SendTransactionScreen.prototype.renderToInput = function (to, identities, addres }, }) }, + onBlur: () => this.setErrorsFor('to'), + onFocus: () => this.clearErrorsFor('to'), }), h('datalist#addresses', [ @@ -160,6 +182,8 @@ SendTransactionScreen.prototype.renderToInput = function (to, identities, addres }), ]), + this.renderErrorMessage('to'), + ]) } @@ -183,12 +207,17 @@ SendTransactionScreen.prototype.renderAmountInput = function (activeCurrency) { this.state.newTx, { amount: event.target.value, + amountToSend: this.getAmountToSend(event.target.value), } ), }) }, + onBlur: () => this.setErrorsFor('amount'), + onFocus: () => this.clearErrorsFor('amount'), }), + this.renderErrorMessage('amount'), + ]) } @@ -260,14 +289,13 @@ SendTransactionScreen.prototype.render = function () { const props = this.props const { - // selectedIdentity, - // network, + warning, identities, addressBook, conversionRate, } = props - const { blockGasLimit, newTx, activeCurrency } = this.state + const { blockGasLimit, newTx, activeCurrency, isValid } = this.state const { gas, gasPrice } = newTx return ( @@ -292,12 +320,15 @@ SendTransactionScreen.prototype.render = function () { this.renderMemoInput(), + this.renderErrorMessage(null, warning), + ]), // Buttons underneath card h('section.flex-column.flex-center', [ h('button.btn-secondary.send-screen__send-button', { - onClick: (event) => this.onSubmit(event), + className: !isValid && 'send-screen__send-button__disabled', + onClick: (event) => isValid && this.onSubmit(event), }, 'Next'), h('button.btn-tertiary.send-screen__cancel-button', { onClick: this.back, @@ -325,62 +356,140 @@ SendTransactionScreen.prototype.back = function () { this.props.dispatch(backToAccountDetail(address)) } -SendTransactionScreen.prototype.onSubmit = function (event) { - event.preventDefault() - const { warning } = this.props - const state = this.state || {} +SendTransactionScreen.prototype.validate = function (balance, amountToSend, { to, from }) { + const sufficientBalance = conversionGreaterThan( + { + value: balance, + fromNumericBase: 'hex', + }, + { + value: amountToSend, + fromNumericBase: 'hex', + }, + ) - const recipient = state.newTx.to - const nickname = state.nickname || ' ' + const amountLessThanZero = conversionGreaterThan( + { + value: 0, + fromNumericBase: 'dec', + }, + { + value: amountToSend, + fromNumericBase: 'hex', + }, + ) - // TODO: convert this to hex when created and include it in send - const txData = state.newTx.memo + const errors = {} - let message + if (!sufficientBalance) { + errors.amount = 'Insufficient funds.' + } - // if (value.gt(balance)) { - // message = 'Insufficient funds.' - // return this.props.dispatch(actions.displayWarning(message)) - // } + if (amountLessThanZero) { + errors.amount = 'Can not send negative amounts of ETH.' + } - // if (input < 0) { - // message = 'Can not send negative amounts of ETH.' - // return this.props.dispatch(actions.displayWarning(message)) - // } + if (!from) { + errors.from = 'Required' + } - if (!isValidAddress(recipient) && !recipient) { - message = 'Recipient address is invalid.' - return this.props.dispatch(displayWarning(message)) + if (from && !isValidAddress(from)) { + errors.from = 'Sender address is invalid.' } - if (txData && !isHex(stripHexPrefix(txData))) { - message = 'Transaction data must be hex string.' - return this.props.dispatch(displayWarning(message)) + if (!to) { + errors.to = 'Required' } - this.props.dispatch(hideWarning()) + if (to && !isValidAddress(to)) { + errors.to = 'Recipient address is invalid.' + } - this.props.dispatch(addToAddressBook(recipient, nickname)) + // if (txData && !isHex(stripHexPrefix(txData))) { + // message = 'Transaction data must be hex string.' + // return this.props.dispatch(displayWarning(message)) + // } + + return { + isValid: allNull(errors), + errors, + } +} + +SendTransactionScreen.prototype.getAmountToSend = function (amount) { + const { activeCurrency } = this.state + const { conversionRate } = this.props // TODO: need a clean way to integrate this into conversionUtil - const sendConversionRate = state.activeCurrency === 'ETH' - ? this.props.conversionRate - : new BigNumber(1.0).div(this.props.conversionRate) + const sendConversionRate = activeCurrency === 'ETH' + ? conversionRate + : new BigNumber(1.0).div(conversionRate) - const sendAmount = conversionUtil(this.state.newTx.amount, { + return conversionUtil(amount, { fromNumericBase: 'dec', toNumericBase: 'hex', - fromCurrency: state.activeCurrency, + fromCurrency: activeCurrency, toCurrency: 'ETH', toDenomination: 'WEI', conversionRate: sendConversionRate, }) - +} + +SendTransactionScreen.prototype.setErrorsFor = function (field) { + const { balance } = this.props + const { newTx, errors: previousErrors } = this.state + const { amountToSend } = newTx + + const { + isValid, + errors: newErrors + } = this.validate(balance, amountToSend, newTx) + + const nextErrors = Object.assign({}, previousErrors, { + [field]: newErrors[field] || null + }) + + if (!isValid) { + this.setState({ + errors: nextErrors, + isValid, + }) + } +} + +SendTransactionScreen.prototype.clearErrorsFor = function (field) { + const { errors: previousErrors } = this.state + const nextErrors = Object.assign({}, previousErrors, { + [field]: null + }) + + this.setState({ + errors: nextErrors, + isValid: allNull(nextErrors), + }) +} + +SendTransactionScreen.prototype.onSubmit = function (event) { + event.preventDefault() + const { warning, balance, amountToSend } = this.props + const state = this.state || {} + + const recipient = state.newTx.to + const sender = state.newTx.from + const nickname = state.nickname || ' ' + + // TODO: convert this to hex when created and include it in send + const txData = state.newTx.memo + + this.props.dispatch(hideWarning()) + + this.props.dispatch(addToAddressBook(recipient, nickname)) + var txParams = { from: this.state.newTx.from, to: this.state.newTx.to, - value: sendAmount, + value: amountToSend, gas: this.state.newTx.gas, gasPrice: this.state.newTx.gasPrice, @@ -389,7 +498,5 @@ SendTransactionScreen.prototype.onSubmit = function (event) { if (recipient) txParams.to = addHexPrefix(recipient) if (txData) txParams.data = txData - if (!warning) { - this.props.dispatch(signTx(txParams)) - } + this.props.dispatch(signTx(txParams)) } diff --git a/ui/app/util.js b/ui/app/util.js index 7aace1b3c..82a5f9f29 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -55,6 +55,7 @@ module.exports = { getContractAtAddress, exportAsFile: exportAsFile, isInvalidChecksumAddress, + allNull, } function valuesFor (obj) { @@ -273,3 +274,7 @@ function exportAsFile (filename, data) { document.body.removeChild(elem) } } + +function allNull (obj) { + return Object.entries(obj).every(([key, value]) => value === null) +} -- cgit