aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
authorThomas Huang <tmashuang@users.noreply.github.com>2019-06-05 05:55:56 +0800
committerGitHub <noreply@github.com>2019-06-05 05:55:56 +0800
commitbe911711942914e7f6df535afff1a3ee87e23b6f (patch)
treebbd890e0a6a1bd72d2560e734ce66ed42a294722 /ui
parent3d7bdd6ecb5e8e17cb8ccf83f56ad389971332c1 (diff)
parent28e030a11d75df81e16f3ebf726bfc59fcee679e (diff)
downloadtangerine-wallet-browser-be911711942914e7f6df535afff1a3ee87e23b6f.tar
tangerine-wallet-browser-be911711942914e7f6df535afff1a3ee87e23b6f.tar.gz
tangerine-wallet-browser-be911711942914e7f6df535afff1a3ee87e23b6f.tar.bz2
tangerine-wallet-browser-be911711942914e7f6df535afff1a3ee87e23b6f.tar.lz
tangerine-wallet-browser-be911711942914e7f6df535afff1a3ee87e23b6f.tar.xz
tangerine-wallet-browser-be911711942914e7f6df535afff1a3ee87e23b6f.tar.zst
tangerine-wallet-browser-be911711942914e7f6df535afff1a3ee87e23b6f.zip
Merge pull request #6683 from MetaMask/develop
Merge dev to master
Diffstat (limited to 'ui')
-rw-r--r--ui/app/components/app/ens-input.js14
-rw-r--r--ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js72
-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.scss93
-rw-r--r--ui/app/helpers/utils/conversions.util.js11
-rw-r--r--ui/app/helpers/utils/metametrics.util.js53
-rw-r--r--ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js9
-rw-r--r--ui/app/pages/create-account/connect-hardware/index.js8
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js41
-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
-rw-r--r--ui/app/pages/send/to-autocomplete/to-autocomplete.js21
24 files changed, 424 insertions, 81 deletions
diff --git a/ui/app/components/app/ens-input.js b/ui/app/components/app/ens-input.js
index 5eea0dd90..f17f6c3d6 100644
--- a/ui/app/components/app/ens-input.js
+++ b/ui/app/components/app/ens-input.js
@@ -41,12 +41,15 @@ EnsInput.prototype.onChange = function (recipient) {
ensResolution: null,
ensFailure: null,
toError: null,
+ recipient,
})
}
this.setState({
loadingEns: true,
+ recipient,
})
+
this.checkName(recipient)
}
@@ -56,6 +59,7 @@ EnsInput.prototype.render = function () {
list: 'addresses',
onChange: this.onChange.bind(this),
qrScanner: true,
+ recipient: (this.state || {}).recipient,
})
return h('div', {
style: { width: '100%', position: 'relative' },
@@ -79,19 +83,21 @@ EnsInput.prototype.componentDidMount = function () {
EnsInput.prototype.lookupEnsName = function (recipient) {
const { ensResolution } = this.state
+ recipient = recipient.trim()
log.info(`ENS attempting to resolve name: ${recipient}`)
- this.ens.lookup(recipient.trim())
+ this.ens.lookup(recipient)
.then((address) => {
if (address === ZERO_ADDRESS) throw new Error(this.context.t('noAddressForName'))
if (address !== ensResolution) {
this.setState({
loadingEns: false,
ensResolution: address,
- nickname: recipient.trim(),
+ nickname: recipient,
hoverText: address + '\n' + this.context.t('clickCopy'),
ensFailure: false,
toError: null,
+ recipient,
})
}
})
@@ -101,11 +107,11 @@ EnsInput.prototype.lookupEnsName = function (recipient) {
ensResolution: recipient,
ensFailure: true,
toError: null,
+ recipient: null,
}
if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') {
setStateObj.hoverText = this.context.t('ensNameNotFound')
setStateObj.toError = 'ensNameNotFound'
- setStateObj.ensFailure = false
} else {
log.error(reason)
setStateObj.hoverText = reason.message
@@ -128,7 +134,7 @@ EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
}
if (prevState && ensResolution && this.props.onChange &&
ensResolution !== prevState.ensResolution) {
- this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError, toWarning: state.toWarning })
+ this.props.onChange({ toAddress: ensResolution, recipient: state.recipient, nickname, toError: state.toError, toWarning: state.toWarning })
}
}
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..9da9a2ef6 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,29 @@ 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,
+ customGasLimit,
+ } = stateProps
const {
updateCustomGasPrice: dispatchUpdateCustomGasPrice,
hideGasButtonGroup: dispatchHideGasButtonGroup,
@@ -188,6 +224,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
hideSidebar: dispatchHideSidebar,
cancelAndClose: dispatchCancelAndClose,
hideModal: dispatchHideModal,
+ setAmountToMax: dispatchSetAmountToMax,
...otherDispatchProps
} = dispatchProps
@@ -208,6 +245,14 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
dispatchHideGasButtonGroup()
dispatchCancelAndClose()
}
+ if (maxModeOn) {
+ dispatchSetAmountToMax({
+ balance,
+ gasTotal: customGasTotal,
+ selectedToken,
+ tokenBalance,
+ })
+ }
},
gasPriceButtonGroupProps: {
...gasPriceButtonGroupProps,
@@ -219,7 +264,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
dispatchHideSidebar()
}
},
- disableSave: insufficientBalance || (isSpeedUp && customGasPrice === 0),
+ disableSave: insufficientBalance || (isSpeedUp && customGasPrice === 0) || customGasLimit < 21000,
}
}
@@ -258,6 +303,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 927640f0b..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;
@@ -763,7 +773,43 @@
}
}
- &__to-autocomplete, &__memo-text-area, &__hex-data {
+ &__to-autocomplete {
+ display: flex;
+ flex-direction: column;
+ z-index: 1025;
+ position: relative;
+ height: 54px;
+ width: 100%;
+ border: 1px solid $alto;
+ border-radius: 4px;
+ background-color: $white;
+ color: $tundora;
+ padding: 0 10px;
+ font-family: Roboto;
+ line-height: 21px;
+
+ &__input {
+ font-size: 16px;
+ height: 100%;
+ border: none;
+ }
+
+ &__resolved {
+ font-size: 12px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ height: 30px;
+ cursor: pointer;
+
+ + .send-v2__to-autocomplete__qr-code {
+ top: 2px;
+ right: 0;
+ }
+ }
+ }
+
+ &__memo-text-area, &__hex-data {
&__input {
z-index: 1025;
position: relative;
@@ -781,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 {
@@ -1041,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/helpers/utils/metametrics.util.js b/ui/app/helpers/utils/metametrics.util.js
index cafbd5c07..50270c6a8 100644
--- a/ui/app/helpers/utils/metametrics.util.js
+++ b/ui/app/helpers/utils/metametrics.util.js
@@ -12,6 +12,8 @@ const METAMETRICS_TRACKING_URL = inDevelopment
? 'http://www.metamask.io/metametrics'
: 'http://www.metamask.io/metametrics-prod'
+/** ***************Custom variables*************** **/
+// Custon variable declarations
const METAMETRICS_CUSTOM_GAS_LIMIT_CHANGE = 'gasLimitChange'
const METAMETRICS_CUSTOM_GAS_PRICE_CHANGE = 'gasPriceChange'
const METAMETRICS_CUSTOM_FUNCTION_TYPE = 'functionType'
@@ -24,13 +26,7 @@ const METAMETRICS_CUSTOM_ERROR_MESSAGE = 'errorMessage'
const METAMETRICS_CUSTOM_RPC_NETWORK_ID = 'networkId'
const METAMETRICS_CUSTOM_RPC_CHAIN_ID = 'chainId'
const METAMETRICS_CUSTOM_GAS_CHANGED = 'gasChanged'
-
-const METAMETRICS_CUSTOM_NETWORK = 'network'
-const METAMETRICS_CUSTOM_ENVIRONMENT_TYPE = 'environmentType'
-const METAMETRICS_CUSTOM_ACTIVE_CURRENCY = 'activeCurrency'
-const METAMETRICS_CUSTOM_ACCOUNT_TYPE = 'accountType'
-const METAMETRICS_CUSTOM_NUMBER_OF_TOKENS = 'numberOfTokens'
-const METAMETRICS_CUSTOM_NUMBER_OF_ACCOUNTS = 'numberOfAccounts'
+const METAMETRICS_CUSTOM_ASSET_SELECTED = 'assetSelected'
const customVariableNameIdMap = {
[METAMETRICS_CUSTOM_FUNCTION_TYPE]: 1,
@@ -38,14 +34,28 @@ const customVariableNameIdMap = {
[METAMETRICS_CUSTOM_CONFIRM_SCREEN_ORIGIN]: 3,
[METAMETRICS_CUSTOM_GAS_LIMIT_CHANGE]: 4,
[METAMETRICS_CUSTOM_GAS_PRICE_CHANGE]: 5,
+
[METAMETRICS_CUSTOM_FROM_NETWORK]: 1,
[METAMETRICS_CUSTOM_TO_NETWORK]: 2,
+
[METAMETRICS_CUSTOM_RPC_NETWORK_ID]: 1,
[METAMETRICS_CUSTOM_RPC_CHAIN_ID]: 2,
- [METAMETRICS_CUSTOM_ERROR_FIELD]: 1,
- [METAMETRICS_CUSTOM_ERROR_MESSAGE]: 2,
+
+ [METAMETRICS_CUSTOM_ERROR_FIELD]: 3,
+ [METAMETRICS_CUSTOM_ERROR_MESSAGE]: 4,
+
[METAMETRICS_CUSTOM_GAS_CHANGED]: 1,
+ [METAMETRICS_CUSTOM_ASSET_SELECTED]: 2,
}
+/** ********************************************************** **/
+
+const METAMETRICS_CUSTOM_NETWORK = 'network'
+const METAMETRICS_CUSTOM_ENVIRONMENT_TYPE = 'environmentType'
+const METAMETRICS_CUSTOM_ACTIVE_CURRENCY = 'activeCurrency'
+const METAMETRICS_CUSTOM_ACCOUNT_TYPE = 'accountType'
+const METAMETRICS_CUSTOM_NUMBER_OF_TOKENS = 'numberOfTokens'
+const METAMETRICS_CUSTOM_NUMBER_OF_ACCOUNTS = 'numberOfAccounts'
+
const customDimensionsNameIdMap = {
[METAMETRICS_CUSTOM_NETWORK]: 5,
@@ -61,6 +71,7 @@ function composeUrlRefParamAddition (previousPath, confirmTransactionOrigin) {
return `&urlref=${externalOrigin ? 'EXTERNAL' : encodeURIComponent(previousPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}`
}
+// composes query params of the form &dimension[0-999]=[value]
function composeCustomDimensionParamAddition (customDimensions) {
const customDimensionParamStrings = Object.keys(customDimensions).reduce((acc, name) => {
return [...acc, `dimension${customDimensionsNameIdMap[name]}=${customDimensions[name]}`]
@@ -68,6 +79,8 @@ function composeCustomDimensionParamAddition (customDimensions) {
return `&${customDimensionParamStrings.join('&')}`
}
+// composes query params in form: &cvar={[id]:[[name],[value]]}
+// Example: &cvar={"1":["OS","iphone 5.0"],"2":["Matomo Mobile Version","1.6.2"],"3":["Locale","en::en"],"4":["Num Accounts","2"]}
function composeCustomVarParamAddition (customVariables) {
const customVariableIdValuePairs = Object.keys(customVariables).reduce((acc, name) => {
return {
@@ -84,6 +97,28 @@ function composeParamAddition (paramValue, paramName) {
: `&${paramName}=${paramValue}`
}
+/**
+ * @name composeUrl
+ * @param {Object} config - configuration object for composing the metametrics url
+ * @property {object} config.eventOpts Object containing event category, action and name descriptors
+ * @property {object} config.customVariables Object containing custom properties with values relevant to a specific event
+ * @property {object} config.pageOpts Objects containing information about a page/route the event is dispatched from
+ * @property {number} config.network The selected network of the user when the event occurs
+ * @property {string} config.environmentType The "environment" the user is using the app from: 'popup', 'notification' or 'fullscreen'
+ * @property {string} config.activeCurrency The current the user has select as their primary currency at the time of the event
+ * @property {string} config.accountType The account type being used at the time of the event: 'hardware', 'imported' or 'default'
+ * @property {number} config.numberOfTokens The number of tokens that the user has added at the time of the event
+ * @property {number} config.numberOfAccounts The number of accounts the user has added at the time of the event
+ * @property {string} config.previousPath The location path the user was on prior to the path they are on at the time of the event
+ * @property {string} config.currentPath The location path the user is on at the time of the event
+ * @property {string} config.metaMetricsId A random id assigned to a user at the time of opting in to metametrics. A hexadecimal number
+ * @property {string} config.confirmTransactionOrigin The origin on a transaction
+ * @property {string} config.url The url to track an event at. Overrides `currentPath`
+ * @property {boolean} config.excludeMetaMetricsId Whether or not the tracked event data should be associated with a metametrics id
+ * @property {boolean} config.isNewVisit Whether or not the event should be tracked as a new visit/user sessions
+ * @returns {String} Returns a url to be passed to fetch to make the appropriate request to matomo.
+ * Example: https://chromeextensionmm.innocraft.cloud/piwik.php?idsite=1&rec=1&apiv=1&e_c=Navigation&e_a=Home&e_n=Clicked%20Send:%20Eth&urlref=http%3A%2F%2Fwww.metamask.io%2Fmetametrics%2Fhome.html%23send&dimension5=3&dimension6=fullscreen&dimension7=ETH&dimension8=default&dimension9=0&dimension10=3&url=http%3A%2F%2Fwww.metamask.io%2Fmetametrics%2Fhome.html%23&_id=49c10aff19795e9a&rand=7906028754863992&pv_id=53acad&uid=49c1
+ */
function composeUrl (config) {
const {
eventOpts = {},
diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
index 3c4e6dcac..c6a05cf0f 100644
--- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
+++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -9,6 +9,7 @@ import { DEFAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE } from '../../helpers/constant
import {
INSUFFICIENT_FUNDS_ERROR_KEY,
TRANSACTION_ERROR_KEY,
+ GAS_LIMIT_TOO_LOW_ERROR_KEY,
} from '../../helpers/constants/error-keys'
import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../helpers/constants/transactions'
import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display'
@@ -134,6 +135,7 @@ export default class ConfirmTransactionBase extends Component {
value: amount,
} = {},
} = {},
+ customGas,
} = this.props
const insufficientBalance = balance && !isBalanceSufficient({
@@ -150,6 +152,13 @@ export default class ConfirmTransactionBase extends Component {
}
}
+ if (customGas.gasLimit < 21000) {
+ return {
+ valid: false,
+ errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
+ }
+ }
+
if (simulationFails) {
return {
valid: true,
diff --git a/ui/app/pages/create-account/connect-hardware/index.js b/ui/app/pages/create-account/connect-hardware/index.js
index 5a91a2725..80a160205 100644
--- a/ui/app/pages/create-account/connect-hardware/index.js
+++ b/ui/app/pages/create-account/connect-hardware/index.js
@@ -8,8 +8,6 @@ const ConnectScreen = require('./connect-screen')
const AccountList = require('./account-list')
const { DEFAULT_ROUTE } = require('../../../helpers/constants/routes')
const { formatBalance } = require('../../../helpers/utils/util')
-const { getPlatform } = require('../../../../../app/scripts/lib/util')
-const { PLATFORM_FIREFOX } = require('../../../../../app/scripts/lib/enums')
class ConnectHardwareForm extends Component {
constructor (props) {
@@ -51,12 +49,6 @@ class ConnectHardwareForm extends Component {
}
connectToHardwareWallet = (device) => {
- // Ledger hardware wallets are not supported on firefox
- if (getPlatform() === PLATFORM_FIREFOX && device === 'ledger') {
- this.setState({ browserSupported: false, error: null})
- return null
- }
-
if (this.state.accounts.length) {
return null
}
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..7901ccef6 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 = {
@@ -35,8 +40,8 @@ export default class AmountMaxButton extends Component {
})
}
- onMaxClick = (event) => {
- const { setMaxModeTo } = this.props
+ onMaxClick = () => {
+ 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()
+ )
+ }
+
}
diff --git a/ui/app/pages/send/to-autocomplete/to-autocomplete.js b/ui/app/pages/send/to-autocomplete/to-autocomplete.js
index 328a5b62b..11f86acf3 100644
--- a/ui/app/pages/send/to-autocomplete/to-autocomplete.js
+++ b/ui/app/pages/send/to-autocomplete/to-autocomplete.js
@@ -1,6 +1,7 @@
const Component = require('react').Component
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
+const copyToClipboard = require('copy-to-clipboard')
const inherits = require('util').inherits
const AccountListItem = require('../account-list-item/account-list-item.component').default
const connect = require('react-redux').connect
@@ -93,24 +94,34 @@ ToAutoComplete.prototype.componentDidUpdate = function (nextProps) {
ToAutoComplete.prototype.render = function () {
const {
to,
+ recipient,
dropdownOpen,
onChange,
inError,
qrScanner,
} = this.props
- return h('div.send-v2__to-autocomplete', {}, [
+ const isRecipientToDiff = recipient && recipient !== to
+
+ return h('div.send-v2__to-autocomplete', {style: {
+ borderColor: inError ? 'red' : null,
+ }}, [
h(`input.send-v2__to-autocomplete__input${qrScanner ? '.with-qr' : ''}`, {
placeholder: this.context.t('recipientAddress'),
className: inError ? `send-v2__error-border` : '',
- value: to,
+ value: recipient,
onChange: event => onChange(event.target.value),
onFocus: event => this.handleInputEvent(event),
- style: {
- borderColor: inError ? 'red' : null,
- },
}),
+ isRecipientToDiff && h(Tooltip, {title: this.context.t('copyToClipboard')},
+ h('div.send-v2__to-autocomplete__resolved', {
+ onClick: (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ copyToClipboard(to)
+ },
+ }, to)),
qrScanner && h(Tooltip, {
title: this.context.t('scanQrCode'),
position: 'bottom',