diff options
author | Thomas Huang <tmashuang@users.noreply.github.com> | 2019-06-05 05:55:56 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-06-05 05:55:56 +0800 |
commit | be911711942914e7f6df535afff1a3ee87e23b6f (patch) | |
tree | bbd890e0a6a1bd72d2560e734ce66ed42a294722 /ui | |
parent | 3d7bdd6ecb5e8e17cb8ccf83f56ad389971332c1 (diff) | |
parent | 28e030a11d75df81e16f3ebf726bfc59fcee679e (diff) | |
download | tangerine-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')
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', |