diff options
20 files changed, 314 insertions, 55 deletions
diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index 78014feef..0cca9e959 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -71,11 +71,29 @@ async function runSendFlowTest (assert) { assert.equal(sendToAccountAddress, '0x2f8D4a878cFA04A6E60D46362f5644DeAb66572D', 'send to dropdown selects the correct address') const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(3)') - sendAmountField.find('.unit-input')[0].click() - const sendAmountFieldInput = await findAsync(sendAmountField, '.unit-input__input') + + const amountMaxButton = await queryAsync($, '.send-v2__amount-max') + amountMaxButton.click() + reactTriggerChange(sendAmountField.find('input')[1]) + assert.equal(sendAmountFieldInput.is(':disabled'), true, 'disabled the send amount input when max mode is on') + + const gasPriceButtonGroup = await queryAsync($, '.gas-price-button-group--small') + const gasPriceButton = await gasPriceButtonGroup.find('button')[0] + const valueBeforeGasPriceChange = sendAmountFieldInput.prop('value') + gasPriceButton.click() + reactTriggerChange(sendAmountField.find('input')[1]) + + await timeout(1000) + + assert.notEqual(valueBeforeGasPriceChange, sendAmountFieldInput.prop('value'), 'send amount value changes when gas price changes') + + amountMaxButton.click() + reactTriggerChange(sendAmountField.find('input')[1]) + + sendAmountField.find('.unit-input').click() sendAmountFieldInput.val('5.1') - reactTriggerChange(sendAmountField.find('input')[0]) + reactTriggerChange(sendAmountField.find('input')[1]) let errorMessage = await queryAsync($, '.send-v2__error') assert.equal(errorMessage[0].textContent, 'Insufficient funds.', 'send should render an insufficient fund error message') diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js index 0e7e30347..c3fdf51e5 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -7,6 +7,8 @@ import { setGasPrice, createSpeedUpTransaction, hideSidebar, + updateSendAmount, + setGasTotal, } from '../../../../store/actions' import { setCustomGasPrice, @@ -18,6 +20,7 @@ import { } from '../../../../ducks/gas/gas.duck' import { hideGasButtonGroup, + updateSendErrors, } from '../../../../ducks/send/send.duck' import { updateGasAndCalculate, @@ -46,6 +49,9 @@ import { isCustomPriceSafe, } from '../../../../selectors/custom-gas' import { + getTokenBalance, +} from '../../../../pages/send/send.selectors' +import { submittedPendingTransactionsSelector, } from '../../../../selectors/transactions' import { @@ -53,6 +59,7 @@ import { } from '../../../../helpers/utils/confirm-tx.util' import { addHexWEIsToDec, + subtractHexWEIsToDec, decEthToConvertedCurrency as ethTotalToConvertedCurrency, decGWEIToHexWEI, hexWEIToDecGWEI, @@ -66,6 +73,8 @@ import { } from '../../../../pages/send/send.utils' import { addHexPrefix } from 'ethereumjs-util' import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils' +import { getMaxModeOn } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors' +import { calcMaxAmount } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils' const mapStateToProps = (state, ownProps) => { const { transaction = {} } = ownProps @@ -75,8 +84,6 @@ const mapStateToProps = (state, ownProps) => { const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, transaction.id) const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit - const gasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) - const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) const gasButtonInfo = getRenderableBasicEstimateData(state, customModalGasLimitInHex) @@ -90,6 +97,8 @@ const mapStateToProps = (state, ownProps) => { const customGasPrice = calcCustomGasPrice(customModalGasPriceInHex) + const maxModeOn = getMaxModeOn(state) + const gasPrices = getEstimatedGasPrices(state) const estimatedTimes = getEstimatedGasTimes(state) const balance = getCurrentEthBalance(state) @@ -98,9 +107,13 @@ const mapStateToProps = (state, ownProps) => { const isMainnet = getIsMainnet(state) const showFiat = Boolean(isMainnet || showFiatInTestnets) - const insufficientBalance = !isBalanceSufficient({ + const newTotalEth = maxModeOn ? addHexWEIsToRenderableEth(balance, '0x0') : addHexWEIsToRenderableEth(value, customGasTotal) + + const sendAmount = maxModeOn ? subtractHexWEIsFromRenderableEth(balance, customGasTotal) : addHexWEIsToRenderableEth(value, '0x0') + + const insufficientBalance = maxModeOn ? false : !isBalanceSufficient({ amount: value, - gasTotal, + gasTotal: customGasTotal, balance, conversionRate, }) @@ -112,10 +125,12 @@ const mapStateToProps = (state, ownProps) => { customModalGasLimitInHex, customGasPrice, customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), + customGasTotal, newTotalFiat, currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes), blockTime: getBasicGasEstimateBlockTime(state), customPriceIsSafe: isCustomPriceSafe(state), + maxModeOn, gasPriceButtonGroupProps: { buttonDataLoading, defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), @@ -129,12 +144,12 @@ const mapStateToProps = (state, ownProps) => { estimatedTimesMax: estimatedTimes[0], }, infoRowProps: { - originalTotalFiat: addHexWEIsToRenderableFiat(value, gasTotal, currentCurrency, conversionRate), - originalTotalEth: addHexWEIsToRenderableEth(value, gasTotal), + originalTotalFiat: addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate), + originalTotalEth: addHexWEIsToRenderableEth(value, customGasTotal), newTotalFiat: showFiat ? newTotalFiat : '', - newTotalEth: addHexWEIsToRenderableEth(value, customGasTotal), + newTotalEth, transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal), - sendAmount: addHexWEIsToRenderableEth(value, '0x0'), + sendAmount, }, isSpeedUp: transaction.status === 'submitted', txId: transaction.id, @@ -142,6 +157,9 @@ const mapStateToProps = (state, ownProps) => { gasEstimatesLoading, isMainnet, isEthereumNetwork: isEthereumNetwork(state), + selectedToken: getSelectedToken(state), + balance, + tokenBalance: getTokenBalance(state), } } @@ -174,11 +192,16 @@ const mapDispatchToProps = dispatch => { hideSidebar: () => dispatch(hideSidebar()), fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), + setGasTotal: (total) => dispatch(setGasTotal(total)), + setAmountToMax: (maxAmountDataObject) => { + dispatch(updateSendErrors({ amount: null })) + dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject))) + }, } } const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { gasPriceButtonGroupProps, isConfirm, txId, isSpeedUp, insufficientBalance, customGasPrice } = stateProps + const { gasPriceButtonGroupProps, isConfirm, txId, isSpeedUp, insufficientBalance, maxModeOn, customGasPrice, customGasTotal, balance, selectedToken, tokenBalance} = stateProps const { updateCustomGasPrice: dispatchUpdateCustomGasPrice, hideGasButtonGroup: dispatchHideGasButtonGroup, @@ -188,6 +211,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { hideSidebar: dispatchHideSidebar, cancelAndClose: dispatchCancelAndClose, hideModal: dispatchHideModal, + setAmountToMax: dispatchSetAmountToMax, ...otherDispatchProps } = dispatchProps @@ -208,6 +232,14 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { dispatchHideGasButtonGroup() dispatchCancelAndClose() } + if (maxModeOn) { + dispatchSetAmountToMax({ + balance, + gasTotal: customGasTotal, + selectedToken, + tokenBalance, + }) + } }, gasPriceButtonGroupProps: { ...gasPriceButtonGroupProps, @@ -258,6 +290,13 @@ function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) { )(aHexWEI, bHexWEI) } +function subtractHexWEIsFromRenderableEth (aHexWEI, bHexWei) { + return pipe( + subtractHexWEIsToDec, + formatETHFee + )(aHexWEI, bHexWei) +} + function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conversionRate) { return pipe( addHexWEIsToDec, diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js index ab24b9c0e..dbe61d5cf 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -46,6 +46,10 @@ proxyquire('../gas-modal-page-container.container.js', { '../../../../ducks/send/send.duck': sendActionSpies, '../../../../selectors/selectors.js': { getCurrentEthBalance: (state) => state.metamask.balance || '0x0', + getSelectedToken: () => null, + }, + '../../../../pages/send/send.selectors': { + getTokenBalance: (state) => state.metamask.send.tokenBalance || '0x0', }, }) @@ -68,6 +72,7 @@ describe('gas-modal-page-container container', () => { gasLimit: '16', gasPrice: '32', amount: '64', + maxModeOn: false, }, currentCurrency: 'abc', conversionRate: 50, @@ -106,6 +111,7 @@ describe('gas-modal-page-container container', () => { }, } const baseExpectedResult = { + balance: '0x0', isConfirm: true, customGasPrice: 4.294967295, customGasLimit: 2863311530, @@ -114,6 +120,7 @@ describe('gas-modal-page-container container', () => { blockTime: 12, customModalGasLimitInHex: 'aaaaaaaa', customModalGasPriceInHex: 'ffffffff', + customGasTotal: 'aaaaaaa955555556', customPriceIsSafe: true, gasChartProps: { 'currentPrice': 4.294967295, @@ -142,6 +149,9 @@ describe('gas-modal-page-container container', () => { txId: 34, isEthereumNetwork: true, isMainnet: true, + maxModeOn: false, + selectedToken: null, + tokenBalance: '0x0', } const baseMockOwnProps = { transaction: { id: 34 } } const tests = [ @@ -150,7 +160,7 @@ describe('gas-modal-page-container container', () => { mockState: Object.assign({}, baseMockState, { metamask: { ...baseMockState.metamask, balance: '0xfffffffffffffffffffff' }, }), - expectedResult: Object.assign({}, baseExpectedResult, { insufficientBalance: false }), + expectedResult: Object.assign({}, baseExpectedResult, { balance: '0xfffffffffffffffffffff', insufficientBalance: false }), mockOwnProps: baseMockOwnProps, }, { diff --git a/ui/app/components/ui/currency-input/currency-input.component.js b/ui/app/components/ui/currency-input/currency-input.component.js index b5be0972b..1876c9591 100644 --- a/ui/app/components/ui/currency-input/currency-input.component.js +++ b/ui/app/components/ui/currency-input/currency-input.component.js @@ -18,6 +18,7 @@ export default class CurrencyInput extends PureComponent { static propTypes = { conversionRate: PropTypes.number, currentCurrency: PropTypes.string, + maxModeOn: PropTypes.bool, nativeCurrency: PropTypes.string, onChange: PropTypes.func, onBlur: PropTypes.func, @@ -136,7 +137,7 @@ export default class CurrencyInput extends PureComponent { } render () { - const { fiatSuffix, nativeSuffix, ...restProps } = this.props + const { fiatSuffix, nativeSuffix, maxModeOn, ...restProps } = this.props const { decimalValue } = this.state return ( @@ -146,6 +147,7 @@ export default class CurrencyInput extends PureComponent { onChange={this.handleChange} onBlur={this.handleBlur} value={decimalValue} + maxModeOn={maxModeOn} actionComponent={( <div className="currency-input__swap-component" diff --git a/ui/app/components/ui/currency-input/currency-input.container.js b/ui/app/components/ui/currency-input/currency-input.container.js index b5d7dfe6d..46e70bace 100644 --- a/ui/app/components/ui/currency-input/currency-input.container.js +++ b/ui/app/components/ui/currency-input/currency-input.container.js @@ -1,18 +1,21 @@ import { connect } from 'react-redux' import CurrencyInput from './currency-input.component' import { ETH } from '../../../helpers/constants/common' +import { getMaxModeOn } from '../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors' import {getIsMainnet, preferencesSelector} from '../../../selectors/selectors' const mapStateToProps = state => { const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state const { showFiatInTestnets } = preferencesSelector(state) const isMainnet = getIsMainnet(state) + const maxModeOn = getMaxModeOn(state) return { nativeCurrency, currentCurrency, conversionRate, hideFiat: (!isMainnet && !showFiatInTestnets), + maxModeOn, } } diff --git a/ui/app/components/ui/currency-input/tests/currency-input.container.test.js b/ui/app/components/ui/currency-input/tests/currency-input.container.test.js index 259fe594a..f10abe09a 100644 --- a/ui/app/components/ui/currency-input/tests/currency-input.container.test.js +++ b/ui/app/components/ui/currency-input/tests/currency-input.container.test.js @@ -30,6 +30,9 @@ describe('CurrencyInput container', () => { provider: { type: 'mainnet', }, + send: { + maxModeOn: false, + }, }, }, expected: { @@ -37,6 +40,7 @@ describe('CurrencyInput container', () => { currentCurrency: 'usd', nativeCurrency: 'ETH', hideFiat: false, + maxModeOn: false, }, }, // Test # 2 @@ -53,6 +57,9 @@ describe('CurrencyInput container', () => { provider: { type: 'rinkeby', }, + send: { + maxModeOn: false, + }, }, }, expected: { @@ -60,6 +67,7 @@ describe('CurrencyInput container', () => { currentCurrency: 'usd', nativeCurrency: 'ETH', hideFiat: true, + maxModeOn: false, }, }, // Test # 3 @@ -76,6 +84,9 @@ describe('CurrencyInput container', () => { provider: { type: 'rinkeby', }, + send: { + maxModeOn: false, + }, }, }, expected: { @@ -83,6 +94,7 @@ describe('CurrencyInput container', () => { currentCurrency: 'usd', nativeCurrency: 'ETH', hideFiat: false, + maxModeOn: false, }, }, // Test # 4 @@ -99,6 +111,9 @@ describe('CurrencyInput container', () => { provider: { type: 'mainnet', }, + send: { + maxModeOn: false, + }, }, }, expected: { @@ -106,6 +121,7 @@ describe('CurrencyInput container', () => { currentCurrency: 'usd', nativeCurrency: 'ETH', hideFiat: false, + maxModeOn: false, }, }, ] diff --git a/ui/app/components/ui/unit-input/index.scss b/ui/app/components/ui/unit-input/index.scss index adc4a3531..58a10c9a1 100644 --- a/ui/app/components/ui/unit-input/index.scss +++ b/ui/app/components/ui/unit-input/index.scss @@ -42,6 +42,10 @@ max-width: 22ch; height: 16px; line-height: 18px; + + &__disabled { + background-color: rgb(222, 222, 222); + } } &__input-container { @@ -59,4 +63,9 @@ &--error { border-color: $red; } + + &__disabled { + background-color: #F2F3F4; + } + } diff --git a/ui/app/components/ui/unit-input/unit-input.component.js b/ui/app/components/ui/unit-input/unit-input.component.js index 6a53f4c6f..9085a0677 100644 --- a/ui/app/components/ui/unit-input/unit-input.component.js +++ b/ui/app/components/ui/unit-input/unit-input.component.js @@ -13,6 +13,7 @@ export default class UnitInput extends PureComponent { children: PropTypes.node, actionComponent: PropTypes.node, error: PropTypes.bool, + maxModeOn: PropTypes.bool, onBlur: PropTypes.func, onChange: PropTypes.func, placeholder: PropTypes.string, @@ -71,25 +72,26 @@ export default class UnitInput extends PureComponent { } render () { - const { error, placeholder, suffix, actionComponent, children } = this.props + const { error, placeholder, suffix, actionComponent, children, maxModeOn } = this.props const { value } = this.state return ( <div - className={classnames('unit-input', { 'unit-input--error': error })} - onClick={this.handleFocus} + className={classnames('unit-input', { 'unit-input--error': error }, { 'unit-input__disabled': maxModeOn })} + onClick={maxModeOn ? null : this.handleFocus} > <div className="unit-input__inputs"> <div className="unit-input__input-container"> <input type="number" - className="unit-input__input" + className={classnames('unit-input__input', { 'unit-input__disabled': maxModeOn })} value={value} placeholder={placeholder} onChange={this.handleChange} onBlur={this.handleBlur} style={{ width: this.getInputWidth(value) }} ref={ref => { this.unitInput = ref }} + disabled={maxModeOn} /> { suffix && ( diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 2e76cc842..e2f0f9b2f 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -520,6 +520,10 @@ color: $red; } + &__error-amount { + margin-top: 5px; + } + &__warning { font-size: 12px; line-height: 12px; @@ -557,6 +561,12 @@ justify-content: space-between; } + &__form-field-container { + display: flex; + flex-direction: column; + width: 277px; + } + &__form-field { flex: 1 1 auto; min-width: 0; @@ -817,12 +827,47 @@ } &__amount-max { - color: $curious-blue; font-family: Roboto; font-size: 12px; - left: 8px; - border: none; - cursor: pointer; + position: relative; + display: inline-block; + width: 56px; + height: 20px; + margin-top: 5px; + + &__button { + width: 56px; + height: 20px; + position: absolute; + border: 2px solid #B0D7F2; + border-radius: 6px; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + color: #2f9ae0; + + &__disabled { + color: #B0D7F2; + cursor: auto; + } + } + + input:checked + &__button { + background-color: #037DD6; + border: 2px solid #037DD6; + color: #fff; + } + } + + &__amount-max input { + opacity: 0; + width: 0; + height: 0; } &__gas-fee-display { @@ -1077,7 +1122,7 @@ font-size: 14px; color: #2f9ae0; cursor: pointer; - margin-top: 16px; + margin-top: 5px; } .sliders-icon-container { diff --git a/ui/app/helpers/utils/conversions.util.js b/ui/app/helpers/utils/conversions.util.js index b4ec50626..5e1c21ff7 100644 --- a/ui/app/helpers/utils/conversions.util.js +++ b/ui/app/helpers/utils/conversions.util.js @@ -1,6 +1,6 @@ import ethUtil from 'ethereumjs-util' import { ETH, GWEI, WEI } from '../constants/common' -import { conversionUtil, addCurrencies } from './conversion-util' +import { conversionUtil, addCurrencies, subtractCurrencies } from './conversion-util' export function bnToHex (inputBn) { return ethUtil.addHexPrefix(inputBn.toString(16)) @@ -92,6 +92,15 @@ export function addHexWEIsToDec (aHexWEI, bHexWEI) { }) } +export function subtractHexWEIsToDec (aHexWEI, bHexWEI) { + return subtractCurrencies(aHexWEI, bHexWEI, { + aBase: 16, + bBase: 16, + fromDenomination: 'WEI', + numberOfDecimals: 6, + }) +} + export function decEthToConvertedCurrency (ethTotal, convertedCurrency, conversionRate) { return conversionUtil(ethTotal, { fromNumericBase: 'dec', diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js index e256d1442..249703763 100644 --- a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js @@ -1,16 +1,21 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' +import classnames from 'classnames' export default class AmountMaxButton extends Component { static propTypes = { balance: PropTypes.string, + buttonDataLoading: PropTypes.bool, + clearMaxAmount: PropTypes.func, + inError: PropTypes.bool, gasTotal: PropTypes.string, maxModeOn: PropTypes.bool, selectedToken: PropTypes.object, setAmountToMax: PropTypes.func, setMaxModeTo: PropTypes.func, tokenBalance: PropTypes.string, + } static contextTypes = { @@ -36,7 +41,7 @@ export default class AmountMaxButton extends Component { } onMaxClick = (event) => { - const { setMaxModeTo } = this.props + const { setMaxModeTo, clearMaxAmount, maxModeOn } = this.props const { metricsEvent } = this.context metricsEvent({ @@ -46,25 +51,25 @@ export default class AmountMaxButton extends Component { name: 'Clicked "Amount Max"', }, }) - - event.preventDefault() - setMaxModeTo(true) - this.setMaxAmount() + if (!maxModeOn) { + setMaxModeTo(true) + this.setMaxAmount() + } else { + setMaxModeTo(false) + clearMaxAmount() + } } render () { - return this.props.maxModeOn - ? null - : ( - <div> - <span - className="send-v2__amount-max" - onClick={this.onMaxClick} - > - {this.context.t('max')} - </span> - </div> + const { maxModeOn, buttonDataLoading, inError } = this.props + + return ( + <div className={'send-v2__amount-max'} onClick={buttonDataLoading || inError ? null : this.onMaxClick}> + <input type="checkbox" checked={maxModeOn} /> + <div className={classnames('send-v2__amount-max__button', { 'send-v2__amount-max__button__disabled': buttonDataLoading || inError })}> + {this.context.t('max')} + </div> + </div> ) } - } diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js index cd48a105f..e444589a1 100644 --- a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js @@ -5,6 +5,7 @@ import { getSendFromBalance, getTokenBalance, } from '../../../send.selectors.js' +import { getBasicGasEstimateLoadingStatus } from '../../../../../selectors/custom-gas' import { getMaxModeOn } from './amount-max-button.selectors.js' import { calcMaxAmount } from './amount-max-button.utils.js' import { @@ -22,6 +23,7 @@ function mapStateToProps (state) { return { balance: getSendFromBalance(state), + buttonDataLoading: getBasicGasEstimateLoadingStatus(state), gasTotal: getGasTotal(state), maxModeOn: getMaxModeOn(state), selectedToken: getSelectedToken(state), @@ -35,6 +37,9 @@ function mapDispatchToProps (dispatch) { dispatch(updateSendErrors({ amount: null })) dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject))) }, + clearMaxAmount: () => { + dispatch(updateSendAmount('0')) + }, setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), } } diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js index a6cb29d4c..f986b26bb 100644 --- a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js @@ -65,7 +65,7 @@ describe('AmountMaxButton Component', function () { assert(wrapper.exists('.send-v2__amount-max')) }) - it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => { + it('should call setMaxModeTo and setMaxAmount when the checkbox is checked', () => { const { onClick, } = wrapper.find('.send-v2__amount-max').props() @@ -81,11 +81,6 @@ describe('AmountMaxButton Component', function () { ) }) - it('should not render anything when maxModeOn is true', () => { - wrapper.setProps({ maxModeOn: true }) - assert.ok(!wrapper.exists('.send-v2__amount-max')) - }) - it('should render the expected text when maxModeOn is false', () => { wrapper.setProps({ maxModeOn: false }) assert.equal(wrapper.find('.send-v2__amount-max').text(), 'max_t') diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js index a75ed5e8f..dcee8fda0 100644 --- a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js @@ -29,6 +29,7 @@ proxyquire('../amount-max-button.container.js', { }, './amount-max-button.selectors.js': { getMaxModeOn: (s) => `mockMaxModeOn:${s}` }, './amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 }, + '../../../../../selectors/custom-gas': { getBasicGasEstimateLoadingStatus: (s) => `mockButtonDataLoading:${s}`}, '../../../../../store/actions': actionSpies, '../../../../../ducks/send/send.duck': duckActionSpies, }) @@ -40,6 +41,7 @@ describe('amount-max-button container', () => { it('should map the correct properties to props', () => { assert.deepEqual(mapStateToProps('mockState'), { balance: 'mockBalance:mockState', + buttonDataLoading: 'mockButtonDataLoading:mockState', gasTotal: 'mockGasTotal:mockState', maxModeOn: 'mockMaxModeOn:mockState', selectedToken: 'mockSelectedToken:mockState', diff --git a/ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js index c0241ea91..10e90c419 100644 --- a/ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js +++ b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js @@ -110,7 +110,7 @@ export default class SendAmountRow extends Component { showError={inError} errorType={'amount'} > - {!inError && gasTotal && <AmountMaxButton />} + {gasTotal && <AmountMaxButton inError={inError} />} { this.renderInput() } </SendRowWrapper> ) diff --git a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js index 1b850ac57..4c09ed564 100644 --- a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js +++ b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js @@ -8,14 +8,19 @@ import AdvancedGasInputs from '../../../../components/app/gas-customization/adva export default class SendGasRow extends Component { static propTypes = { + balance: PropTypes.string, conversionRate: PropTypes.number, convertedCurrency: PropTypes.string, gasFeeError: PropTypes.bool, gasLoadingError: PropTypes.bool, gasTotal: PropTypes.string, + maxModeOn: PropTypes.bool, showCustomizeGasModal: PropTypes.func, + selectedToken: PropTypes.object, + setAmountToMax: PropTypes.func, setGasPrice: PropTypes.func, setGasLimit: PropTypes.func, + tokenBalance: PropTypes.string, gasPriceButtonGroupProps: PropTypes.object, gasButtonGroupShown: PropTypes.bool, advancedInlineGasShown: PropTypes.bool, @@ -47,6 +52,23 @@ export default class SendGasRow extends Component { </div> } + setMaxAmount () { + const { + balance, + gasTotal, + selectedToken, + setAmountToMax, + tokenBalance, + } = this.props + + setAmountToMax({ + balance, + gasTotal, + selectedToken, + tokenBalance, + }) + } + renderContent () { const { conversionRate, @@ -57,6 +79,7 @@ export default class SendGasRow extends Component { gasPriceButtonGroupProps, gasButtonGroupShown, advancedInlineGasShown, + maxModeOn, resetGasButtons, setGasPrice, setGasLimit, @@ -71,7 +94,7 @@ export default class SendGasRow extends Component { className="gas-price-button-group--small" showCheck={false} {...gasPriceButtonGroupProps} - handleGasPriceSelection={(...args) => { + handleGasPriceSelection={async (...args) => { metricsEvent({ eventOpts: { category: 'Transactions', @@ -79,7 +102,10 @@ export default class SendGasRow extends Component { name: 'Changed Gas Button', }, }) - gasPriceButtonGroupProps.handleGasPriceSelection(...args) + await gasPriceButtonGroupProps.handleGasPriceSelection(...args) + if (maxModeOn) { + this.setMaxAmount() + } }} /> { this.renderAdvancedOptionsButton() } @@ -89,7 +115,12 @@ export default class SendGasRow extends Component { convertedCurrency={convertedCurrency} gasLoadingError={gasLoadingError} gasTotal={gasTotal} - onReset={resetGasButtons} + onReset={() => { + resetGasButtons() + if (maxModeOn) { + this.setMaxAmount() + } + }} onClick={() => showCustomizeGasModal()} /> const advancedGasInputs = <div> diff --git a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js index c4daa98af..10eaa50b8 100644 --- a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js +++ b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js @@ -6,11 +6,17 @@ import { getGasPrice, getGasLimit, getSendAmount, + getSendFromBalance, + getTokenBalance, } from '../../send.selectors.js' import { + getMaxModeOn, +} from '../send-amount-row/amount-max-button/amount-max-button.selectors' +import { isBalanceSufficient, calcGasTotal, } from '../../send.utils.js' +import { calcMaxAmount } from '../send-amount-row/amount-max-button/amount-max-button.utils' import { getBasicGasEstimateLoadingStatus, getRenderableEstimateDataForSmallButtonsFromGWEI, @@ -18,6 +24,7 @@ import { } from '../../../../selectors/custom-gas' import { showGasButtonGroup, + updateSendErrors, } from '../../../../ducks/send/send.duck' import { resetCustomData, @@ -25,10 +32,11 @@ import { setCustomGasLimit, } from '../../../../ducks/gas/gas.duck' import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js' -import { showModal, setGasPrice, setGasLimit, setGasTotal } from '../../../../store/actions' +import { showModal, setGasPrice, setGasLimit, setGasTotal, updateSendAmount } from '../../../../store/actions' import { getAdvancedInlineGasShown, getCurrentEthBalance, getSelectedToken } from '../../../../selectors/selectors' import SendGasRow from './send-gas-row.component' + export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendGasRow) function mapStateToProps (state) { @@ -49,6 +57,7 @@ function mapStateToProps (state) { }) return { + balance: getSendFromBalance(state), conversionRate, convertedCurrency: getCurrentCurrency(state), gasTotal, @@ -65,6 +74,9 @@ function mapStateToProps (state) { gasPrice, gasLimit, insufficientBalance, + maxModeOn: getMaxModeOn(state), + selectedToken: getSelectedToken(state), + tokenBalance: getTokenBalance(state), } } @@ -85,6 +97,10 @@ function mapDispatchToProps (dispatch) { dispatch(setGasTotal(calcGasTotal(newLimit, gasPrice))) } }, + setAmountToMax: maxAmountDataObject => { + dispatch(updateSendErrors({ amount: null })) + dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject))) + }, showGasButtonGroup: () => dispatch(showGasButtonGroup()), resetCustomData: () => dispatch(resetCustomData()), } diff --git a/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js index ddc6ea985..4acb310f8 100644 --- a/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js +++ b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js @@ -44,6 +44,11 @@ proxyquire('../send-gas-row.container.js', { getGasPrice: (s) => `mockGasPrice:${s}`, getGasLimit: (s) => `mockGasLimit:${s}`, getSendAmount: (s) => `mockSendAmount:${s}`, + getSendFromBalance: (s) => `mockBalance:${s}`, + getTokenBalance: (s) => `mockTokenBalance:${s}`, + }, + '../send-amount-row/amount-max-button/amount-max-button.selectors': { + getMaxModeOn: (s) => `mockMaxModeOn:${s}`, }, '../../send.utils.js': { isBalanceSufficient: ({ @@ -75,6 +80,7 @@ describe('send-gas-row container', () => { it('should map the correct properties to props', () => { assert.deepEqual(mapStateToProps('mockState'), { + balance: 'mockBalance:mockState', conversionRate: 'mockConversionRate:mockState', convertedCurrency: 'mockConvertedCurrency:mockState', gasTotal: 'mockGasTotal:mockState', @@ -91,6 +97,9 @@ describe('send-gas-row container', () => { gasLimit: 'mockGasLimit:mockState', gasPrice: 'mockGasPrice:mockState', insufficientBalance: false, + maxModeOn: 'mockMaxModeOn:mockState', + selectedToken: false, + tokenBalance: 'mockTokenBalance:mockState', }) }) diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js index 61bc7bab7..0be01996a 100644 --- a/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js @@ -1,5 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' +import classnames from 'classnames' export default class SendRowErrorMessage extends Component { @@ -19,7 +20,7 @@ export default class SendRowErrorMessage extends Component { return ( errorMessage - ? <div className="send-v2__error">{this.context.t(errorMessage)}</div> + ? <div className={classnames('send-v2__error', {'send-v2__error-amount': errorType === 'amount'})}>{this.context.t(errorMessage)}</div> : null ) } diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.component.js index 94309bd96..075b86633 100644 --- a/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.component.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.component.js @@ -18,7 +18,7 @@ export default class SendRowWrapper extends Component { t: PropTypes.func, }; - render () { + renderAmountFormRow () { const { children, errorType = '', @@ -34,7 +34,39 @@ export default class SendRowWrapper extends Component { <div className="send-v2__form-row"> <div className="send-v2__form-label"> {label} - {showError && <SendRowErrorMessage errorType={errorType}/>} + {customLabelContent} + </div> + <div className="send-v2__form-field-container"> + <div className="send-v2__form-field"> + {formField} + </div> + <div> + {showError && <SendRowErrorMessage errorType={errorType} />} + {!showError && showWarning && <SendRowWarningMessage warningType={warningType} />} + </div> + </div> + </div> + ) + } + + renderFormRow () { + const { + children, + errorType = '', + label, + showError = false, + showWarning = false, + warningType = '', + } = this.props + + const formField = Array.isArray(children) ? children[1] || children[0] : children + const customLabelContent = (Array.isArray(children) && children.length) > 1 ? children[0] : null + + return ( + <div className="send-v2__form-row"> + <div className="send-v2__form-label"> + {label} + {showError && <SendRowErrorMessage errorType={errorType} />} {!showError && showWarning && <SendRowWarningMessage warningType={warningType} />} {customLabelContent} </div> @@ -45,4 +77,14 @@ export default class SendRowWrapper extends Component { ) } + render () { + const { + errorType = '', + } = this.props + + return ( + errorType === 'amount' ? this.renderAmountFormRow() : this.renderFormRow() + ) + } + } |