aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEtienne Dusseault <etienne.dusseault@gmail.com>2019-05-21 00:38:08 +0800
committerWhymarrh Whitby <whymarrh.whitby@gmail.com>2019-05-21 00:38:08 +0800
commit0e9c8fb5ccb9b02a53879e8e676df2c7735a8c69 (patch)
tree9e6cf162d9aec30d3f954e6703949a2fc47cac2e
parentb27a4d2b4ef8b55987d2fa64421304a256811414 (diff)
downloadtangerine-wallet-browser-0e9c8fb5ccb9b02a53879e8e676df2c7735a8c69.tar
tangerine-wallet-browser-0e9c8fb5ccb9b02a53879e8e676df2c7735a8c69.tar.gz
tangerine-wallet-browser-0e9c8fb5ccb9b02a53879e8e676df2c7735a8c69.tar.bz2
tangerine-wallet-browser-0e9c8fb5ccb9b02a53879e8e676df2c7735a8c69.tar.lz
tangerine-wallet-browser-0e9c8fb5ccb9b02a53879e8e676df2c7735a8c69.tar.xz
tangerine-wallet-browser-0e9c8fb5ccb9b02a53879e8e676df2c7735a8c69.tar.zst
tangerine-wallet-browser-0e9c8fb5ccb9b02a53879e8e676df2c7735a8c69.zip
Improved UX for sweeping accounts (#6488)
* Changed max button to checkbox, disabled input if max mode is on, recalculate price according to gas fee if max mode is on * Disabled insufficient funds message in the modal if max mode is on, displays proper amounts in modal when max mode is on, sets the send amount according to custom gas price after gas modal save, resets the send amount after resetting custom gas price * Disabled max mode checkbox if gas buttons are loading, refactored gas-modal-page-container * Implemented new max button & max mode message. Moved insufficient funds error to underneath the send amount field * Fixed existing integration test to pass, created new tests to ensure send amount field is disabled when max button is clicked and the amount changes when the gas price is changed. Refactored some components
-rw-r--r--test/integration/lib/send-new-ui.js24
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js57
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js12
-rw-r--r--ui/app/components/ui/currency-input/currency-input.component.js4
-rw-r--r--ui/app/components/ui/currency-input/currency-input.container.js3
-rw-r--r--ui/app/components/ui/currency-input/tests/currency-input.container.test.js16
-rw-r--r--ui/app/components/ui/unit-input/index.scss9
-rw-r--r--ui/app/components/ui/unit-input/unit-input.component.js10
-rw-r--r--ui/app/css/itcss/components/send.scss55
-rw-r--r--ui/app/helpers/utils/conversions.util.js11
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js39
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js5
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js7
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js2
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js2
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js37
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js18
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js9
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js3
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.component.js46
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()
+ )
+ }
+
}