diff options
author | brunobar79 <brunobar79@gmail.com> | 2018-07-07 07:21:18 +0800 |
---|---|---|
committer | brunobar79 <brunobar79@gmail.com> | 2018-07-07 07:21:18 +0800 |
commit | 7c9d942ba165ff53f44985793f28ffb48fa578e1 (patch) | |
tree | c588281c58c8bae4a4da228b2e27ea708c4eb09d /ui | |
parent | 6b2511f94f436a30c6c683f9da2c3142d9a6461c (diff) | |
parent | b4aaf30d6fe829f18dea68a5e6cc321b9fb00d4e (diff) | |
download | tangerine-wallet-browser-7c9d942ba165ff53f44985793f28ffb48fa578e1.tar tangerine-wallet-browser-7c9d942ba165ff53f44985793f28ffb48fa578e1.tar.gz tangerine-wallet-browser-7c9d942ba165ff53f44985793f28ffb48fa578e1.tar.bz2 tangerine-wallet-browser-7c9d942ba165ff53f44985793f28ffb48fa578e1.tar.lz tangerine-wallet-browser-7c9d942ba165ff53f44985793f28ffb48fa578e1.tar.xz tangerine-wallet-browser-7c9d942ba165ff53f44985793f28ffb48fa578e1.tar.zst tangerine-wallet-browser-7c9d942ba165ff53f44985793f28ffb48fa578e1.zip |
Merge branch 'develop' of github.com:MetaMask/metamask-extension into initial-trezor-support
Diffstat (limited to 'ui')
30 files changed, 542 insertions, 195 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js index c822ef3ee..f04de8fe8 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -6,7 +6,6 @@ const { calcGasTotal, calcTokenBalance, estimateGas, - estimateGasPriceFromRecentBlocks, } = require('./components/send_/send.utils') const ethUtil = require('ethereumjs-util') const { fetchLocale } = require('../i18n-helper') @@ -788,19 +787,26 @@ function updateGasData ({ }) { return (dispatch) => { dispatch(actions.gasLoadingStarted()) - const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks) - return Promise.all([ - Promise.resolve(estimatedGasPrice), - estimateGas({ - estimateGasMethod: background.estimateGas, - blockGasLimit, - selectedAddress, - selectedToken, - to, - value, - gasPrice: estimatedGasPrice, - }), - ]) + return new Promise((resolve, reject) => { + background.getGasPrice((err, data) => { + if (err) return reject(err) + return resolve(data) + }) + }) + .then(estimateGasPrice => { + return Promise.all([ + Promise.resolve(estimateGasPrice), + estimateGas({ + estimateGasMethod: background.estimateGas, + blockGasLimit, + selectedAddress, + selectedToken, + to, + value, + estimateGasPrice, + }), + ]) + }) .then(([gasPrice, gas]) => { dispatch(actions.setGasPrice(gasPrice)) dispatch(actions.setGasLimit(gas)) diff --git a/ui/app/app.js b/ui/app/app.js index d0e48a368..670b7e2d0 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -23,7 +23,7 @@ const Authenticated = require('./components/pages/authenticated') const Initialized = require('./components/pages/initialized') const Settings = require('./components/pages/settings') const UnlockPage = require('./components/pages/unlock-page') -const RestoreVaultPage = require('./components/pages/keychains/restore-vault') +const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed') const AddTokenPage = require('./components/pages/add-token') const ConfirmAddTokenPage = require('./components/pages/confirm-add-token') diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index cd8f76ed5..cefa428b9 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -31,8 +31,6 @@ const { } = require('../../conversion-util') const { - getGasPrice, - getGasLimit, getGasIsLoading, getForceGasMin, conversionRateSelector, @@ -44,6 +42,11 @@ const { getSendMaxModeState, } = require('../../selectors') +const { + getGasPrice, + getGasLimit, +} = require('../send_/send.selectors') + function mapStateToProps (state) { const selectedToken = getSelectedToken(state) const currentAccount = getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state) diff --git a/ui/app/components/pages/keychains/restore-vault.js b/ui/app/components/pages/keychains/restore-vault.js index 33575bfbb..d90a33e49 100644 --- a/ui/app/components/pages/keychains/restore-vault.js +++ b/ui/app/components/pages/keychains/restore-vault.js @@ -1,178 +1,189 @@ -const { withRouter } = require('react-router-dom') -const PropTypes = require('prop-types') -const { compose } = require('recompose') -const PersistentForm = require('../../../../lib/persistent-form') -const connect = require('../../../metamask-connect') -const h = require('react-hyperscript') -const { createNewVaultAndRestore, unMarkPasswordForgotten } = require('../../../actions') -const { DEFAULT_ROUTE } = require('../../../routes') -const log = require('loglevel') - -class RestoreVaultPage extends PersistentForm { - constructor (props) { - super(props) - - this.state = { - error: null, - } +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import {connect} from 'react-redux' +import { + createNewVaultAndRestore, + unMarkPasswordForgotten, +} from '../../../actions' +import { DEFAULT_ROUTE } from '../../../routes' +import TextField from '../../text-field' + +class RestoreVaultPage extends Component { + static contextTypes = { + t: PropTypes.func, } - createOnEnter (event) { - if (event.key === 'Enter') { - this.createNewVaultAndRestore() - } + static propTypes = { + warning: PropTypes.string, + createNewVaultAndRestore: PropTypes.func.isRequired, + leaveImportSeedScreenState: PropTypes.func, + history: PropTypes.object, + isLoading: PropTypes.bool, + }; + + state = { + seedPhrase: '', + password: '', + confirmPassword: '', + seedPhraseError: null, + passwordError: null, + confirmPasswordError: null, } - cancel () { - this.props.unMarkPasswordForgotten() - .then(this.props.history.push(DEFAULT_ROUTE)) + parseSeedPhrase = (seedPhrase) => { + return seedPhrase + .match(/\w+/g) + .join(' ') } - createNewVaultAndRestore () { - this.setState({ error: null }) + handleSeedPhraseChange (seedPhrase) { + let seedPhraseError = null + + if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) { + seedPhraseError = this.context.t('seedPhraseReq') + } + + this.setState({ seedPhrase, seedPhraseError }) + } - // check password - var passwordBox = document.getElementById('password-box') - var password = passwordBox.value - var passwordConfirmBox = document.getElementById('password-box-confirm') - var passwordConfirm = passwordConfirmBox.value + handlePasswordChange (password) { + const { confirmPassword } = this.state + let confirmPasswordError = null + let passwordError = null - if (password.length < 8) { - this.setState({ error: 'Password not long enough' }) - return + if (password && password.length < 8) { + passwordError = this.context.t('passwordNotLongEnough') } - if (password !== passwordConfirm) { - this.setState({ error: 'Passwords don\'t match' }) - return + if (confirmPassword && password !== confirmPassword) { + confirmPasswordError = this.context.t('passwordsDontMatch') } - // check seed - var seedBox = document.querySelector('textarea.twelve-word-phrase') - var seed = seedBox.value.trim() - if (seed.split(' ').length !== 12) { - this.setState({ error: 'Seed phrases are 12 words long' }) - return + this.setState({ password, passwordError, confirmPasswordError }) + } + + handleConfirmPasswordChange (confirmPassword) { + const { password } = this.state + let confirmPasswordError = null + + if (password !== confirmPassword) { + confirmPasswordError = this.context.t('passwordsDontMatch') } - // submit - this.props.createNewVaultAndRestore(password, seed) - .then(() => this.props.history.push(DEFAULT_ROUTE)) - .catch(({ message }) => { - this.setState({ error: message }) - log.error(message) - }) + this.setState({ confirmPassword, confirmPasswordError }) + } + + onClick = () => { + const { password, seedPhrase } = this.state + const { + createNewVaultAndRestore, + leaveImportSeedScreenState, + history, + } = this.props + + leaveImportSeedScreenState() + createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase)) + .then(() => history.push(DEFAULT_ROUTE)) + } + + hasError () { + const { passwordError, confirmPasswordError, seedPhraseError } = this.state + return passwordError || confirmPasswordError || seedPhraseError } render () { - const { error } = this.state - this.persistentFormParentId = 'restore-vault-form' + const { + seedPhrase, + password, + confirmPassword, + seedPhraseError, + passwordError, + confirmPasswordError, + } = this.state + const { t } = this.context + const { isLoading } = this.props + const disabled = !seedPhrase || !password || !confirmPassword || isLoading || this.hasError() return ( - h('.initialize-screen.flex-column.flex-center.flex-grow', [ - - h('h3.flex-center.text-transform-uppercase', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - padding: 6, - }, - }, [ - this.props.t('restoreVault'), - ]), - - // wallet seed entry - h('h3', 'Wallet Seed'), - h('textarea.twelve-word-phrase.letter-spacey', { - dataset: { - persistentFormId: 'wallet-seed', - }, - placeholder: this.props.t('secretPhrase'), - }), - - // password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box', - placeholder: this.props.t('newPassword8Chars'), - dataset: { - persistentFormId: 'password', - }, - style: { - width: 260, - marginTop: 12, - }, - }), - - // confirm password - h('input.large-input.letter-spacey', { - type: 'password', - id: 'password-box-confirm', - placeholder: this.props.t('confirmPassword'), - onKeyPress: this.createOnEnter.bind(this), - dataset: { - persistentFormId: 'password-confirmation', - }, - style: { - width: 260, - marginTop: 16, - }, - }), - - error && ( - h('span.error.in-progress-notification', error) - ), - - // submit - h('.flex-row.flex-space-between', { - style: { - marginTop: 30, - width: '50%', - }, - }, [ - - // cancel - h('button.primary', { - onClick: () => this.cancel(), - }, this.props.t('cancel')), - - // submit - h('button.primary', { - onClick: this.createNewVaultAndRestore.bind(this), - }, this.props.t('ok')), - - ]), - ]) + <div className="first-view-main-wrapper"> + <div className="first-view-main"> + <div className="import-account"> + <a + className="import-account__back-button" + onClick={e => { + e.preventDefault() + this.props.history.goBack() + }} + href="#" + > + {`< Back`} + </a> + <div className="import-account__title"> + { this.context.t('restoreAccountWithSeed') } + </div> + <div className="import-account__selector-label"> + { this.context.t('secretPhrase') } + </div> + <div className="import-account__input-wrapper"> + <label className="import-account__input-label">Wallet Seed</label> + <textarea + className="import-account__secret-phrase" + onChange={e => this.handleSeedPhraseChange(e.target.value)} + value={this.state.seedPhrase} + placeholder={this.context.t('separateEachWord')} + /> + </div> + <span className="error"> + { seedPhraseError } + </span> + <TextField + id="password" + label={t('newPassword')} + type="password" + className="first-time-flow__input" + value={this.state.password} + onChange={event => this.handlePasswordChange(event.target.value)} + error={passwordError} + autoComplete="new-password" + margin="normal" + largeLabel + /> + <TextField + id="confirm-password" + label={t('confirmPassword')} + type="password" + className="first-time-flow__input" + value={this.state.confirmPassword} + onChange={event => this.handleConfirmPasswordChange(event.target.value)} + error={confirmPasswordError} + autoComplete="confirm-password" + margin="normal" + largeLabel + /> + <button + className="first-time-flow__button" + onClick={() => !disabled && this.onClick()} + disabled={disabled} + > + {this.context.t('restore')} + </button> + </div> + </div> + </div> ) } } -RestoreVaultPage.propTypes = { - history: PropTypes.object, -} - -const mapStateToProps = state => { - const { appState: { warning, forgottenPassword } } = state - - return { - warning, - forgottenPassword, - } +RestoreVaultPage.contextTypes = { + t: PropTypes.func, } -const mapDispatchToProps = dispatch => { - return { - createNewVaultAndRestore: (password, seed) => { - return dispatch(createNewVaultAndRestore(password, seed)) +export default connect( + ({ appState: { warning, isLoading } }) => ({ warning, isLoading }), + dispatch => ({ + leaveImportSeedScreenState: () => { + dispatch(unMarkPasswordForgotten()) }, - unMarkPasswordForgotten: () => dispatch(unMarkPasswordForgotten()), - } -} - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) + createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)), + }) )(RestoreVaultPage) diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js index 196538c11..e13b95555 100644 --- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js @@ -21,6 +21,7 @@ export default class SendAmountRow extends Component { selectedToken: PropTypes.object, setMaxModeTo: PropTypes.func, tokenBalance: PropTypes.string, + updateGasFeeError: PropTypes.func, updateSendAmount: PropTypes.func, updateSendAmountError: PropTypes.func, updateGas: PropTypes.func, @@ -35,6 +36,7 @@ export default class SendAmountRow extends Component { primaryCurrency, selectedToken, tokenBalance, + updateGasFeeError, updateSendAmountError, } = this.props @@ -48,6 +50,19 @@ export default class SendAmountRow extends Component { selectedToken, tokenBalance, }) + + if (selectedToken) { + updateGasFeeError({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + }) + } } updateAmount (amount) { diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js index b816d948f..3504d1b73 100644 --- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js @@ -13,7 +13,7 @@ import { import { sendAmountIsInError, } from './send-amount-row.selectors' -import { getAmountErrorObject } from '../../send.utils' +import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils' import { setMaxModeTo, updateSendAmount, @@ -44,6 +44,9 @@ function mapDispatchToProps (dispatch) { return { setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)), + updateGasFeeError: (amountDataObject) => { + dispatch(updateSendErrors(getGasFeeErrorObject(amountDataObject))) + }, updateSendAmountError: (amountDataObject) => { dispatch(updateSendErrors(getAmountErrorObject(amountDataObject))) }, diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js index 579e18585..95c000a34 100644 --- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js +++ b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js @@ -13,6 +13,7 @@ const propsMethodSpies = { updateSendAmount: sinon.spy(), updateSendAmountError: sinon.spy(), updateGas: sinon.spy(), + updateGasFeeError: sinon.spy(), } sinon.spy(SendAmountRow.prototype, 'updateAmount') @@ -36,6 +37,7 @@ describe('SendAmountRow Component', function () { selectedToken={ { address: 'mockTokenAddress' } } setMaxModeTo={propsMethodSpies.setMaxModeTo} tokenBalance={'mockTokenBalance'} + updateGasFeeError={propsMethodSpies.updateGasFeeError} updateSendAmount={propsMethodSpies.updateSendAmount} updateSendAmountError={propsMethodSpies.updateSendAmountError} updateGas={propsMethodSpies.updateGas} @@ -47,6 +49,7 @@ describe('SendAmountRow Component', function () { propsMethodSpies.setMaxModeTo.resetHistory() propsMethodSpies.updateSendAmount.resetHistory() propsMethodSpies.updateSendAmountError.resetHistory() + propsMethodSpies.updateGasFeeError.resetHistory() SendAmountRow.prototype.validateAmount.resetHistory() SendAmountRow.prototype.updateAmount.resetHistory() }) @@ -72,6 +75,32 @@ describe('SendAmountRow Component', function () { ) }) + it('should call updateGasFeeError if selectedToken is truthy', () => { + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) + instance.validateAmount('someAmount') + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateGasFeeError.getCall(0).args, + [{ + amount: 'someAmount', + amountConversionRate: 'mockAmountConversionRate', + balance: 'mockBalance', + conversionRate: 7, + gasTotal: 'mockGasTotal', + primaryCurrency: 'mockPrimaryCurrency', + selectedToken: { address: 'mockTokenAddress' }, + tokenBalance: 'mockTokenBalance', + }] + ) + }) + + it('should call not updateGasFeeError if selectedToken is falsey', () => { + wrapper.setProps({ selectedToken: null }) + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) + instance.validateAmount('someAmount') + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) + }) + }) describe('updateAmount', () => { diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js index 94d9918a7..52e351aee 100644 --- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js +++ b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js @@ -33,7 +33,10 @@ proxyquire('../send-amount-row.container.js', { getTokenBalance: (s) => `mockTokenBalance:${s}`, }, './send-amount-row.selectors': { sendAmountIsInError: (s) => `mockInError:${s}` }, - '../../send.utils': { getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }) }, + '../../send.utils': { + getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }), + getGasFeeErrorObject: (mockDataObject) => ({ ...mockDataObject, mockGasFeeErrorChange: true }), + }, '../../../../actions': actionSpies, '../../../../ducks/send.duck': duckActionSpies, }) @@ -66,6 +69,7 @@ describe('send-amount-row container', () => { beforeEach(() => { dispatchSpy = sinon.spy() mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + duckActionSpies.updateSendErrors.resetHistory() }) describe('setMaxModeTo()', () => { @@ -92,6 +96,18 @@ describe('send-amount-row container', () => { }) }) + describe('updateGasFeeError()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateGasFeeError({ some: 'data' }) + assert(dispatchSpy.calledOnce) + assert(duckActionSpies.updateSendErrors.calledOnce) + assert.deepEqual( + duckActionSpies.updateSendErrors.getCall(0).args[0], + { some: 'data', mockGasFeeErrorChange: true } + ) + }) + }) + describe('updateSendAmountError()', () => { it('should dispatch an action', () => { mapDispatchToPropsObject.updateSendAmountError({ some: 'data' }) diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js index 17cea3d4e..ba5c22a47 100644 --- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js +++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js @@ -8,6 +8,7 @@ export default class SendGasRow extends Component { static propTypes = { conversionRate: PropTypes.number, convertedCurrency: PropTypes.string, + gasFeeError: PropTypes.bool, gasLoadingError: PropTypes.bool, gasTotal: PropTypes.string, showCustomizeGasModal: PropTypes.func, @@ -19,11 +20,16 @@ export default class SendGasRow extends Component { convertedCurrency, gasLoadingError, gasTotal, + gasFeeError, showCustomizeGasModal, } = this.props return ( - <SendRowWrapper label={`${this.context.t('gasFee')}:`}> + <SendRowWrapper + label={`${this.context.t('gasFee')}:`} + showError={gasFeeError} + errorType={'gasFee'} + > <GasFeeDisplay conversionRate={conversionRate} convertedCurrency={convertedCurrency} diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js index 6e6fbc8a8..8f8e3e4dd 100644 --- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js +++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js @@ -4,7 +4,7 @@ import { getCurrentCurrency, getGasTotal, } from '../../send.selectors.js' -import { sendGasIsInError } from './send-gas-row.selectors.js' +import { getGasLoadingError, gasFeeIsInError } from './send-gas-row.selectors.js' import { showModal } from '../../../../actions' import SendGasRow from './send-gas-row.component' @@ -15,7 +15,8 @@ function mapStateToProps (state) { conversionRate: getConversionRate(state), convertedCurrency: getCurrentCurrency(state), gasTotal: getGasTotal(state), - gasLoadingError: sendGasIsInError(state), + gasFeeError: gasFeeIsInError(state), + gasLoadingError: getGasLoadingError(state), } } diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js index d069ae8c6..96f6293c2 100644 --- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js +++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js @@ -1,9 +1,14 @@ const selectors = { - sendGasIsInError, + gasFeeIsInError, + getGasLoadingError, } module.exports = selectors -function sendGasIsInError (state) { +function getGasLoadingError (state) { return state.send.errors.gasLoading } + +function gasFeeIsInError (state) { + return Boolean(state.send.errors.gasFee) +} diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js index db37f18be..54a92bd2d 100644 --- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js +++ b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js @@ -18,6 +18,7 @@ describe('SendGasRow Component', function () { wrapper = shallow(<SendGasRow conversionRate={20} convertedCurrency={'mockConvertedCurrency'} + gasFeeError={'mockGasFeeError'} gasLoadingError={false} gasTotal={'mockGasTotal'} showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal} @@ -36,9 +37,13 @@ describe('SendGasRow Component', function () { it('should pass the correct props to SendRowWrapper', () => { const { label, + showError, + errorType, } = wrapper.find(SendRowWrapper).props() assert.equal(label, 'gasFee_t:') + assert.equal(showError, 'mockGasFeeError') + assert.equal(errorType, 'gasFee') }) it('should render a GasFeeDisplay as a child of the SendRowWrapper', () => { diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js index e928c8aba..2ce062505 100644 --- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js +++ b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js @@ -22,7 +22,10 @@ proxyquire('../send-gas-row.container.js', { getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, getGasTotal: (s) => `mockGasTotal:${s}`, }, - './send-gas-row.selectors.js': { sendGasIsInError: (s) => `mockGasLoadingError:${s}` }, + './send-gas-row.selectors.js': { + getGasLoadingError: (s) => `mockGasLoadingError:${s}`, + gasFeeIsInError: (s) => `mockGasFeeError:${s}`, + }, '../../../../actions': actionSpies, }) @@ -35,6 +38,7 @@ describe('send-gas-row container', () => { conversionRate: 'mockConversionRate:mockState', convertedCurrency: 'mockConvertedCurrency:mockState', gasTotal: 'mockGasTotal:mockState', + gasFeeError: 'mockGasFeeError:mockState', gasLoadingError: 'mockGasLoadingError:mockState', }) }) diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js index a5196334e..d46dd9d8b 100644 --- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js +++ b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js @@ -1,11 +1,12 @@ import assert from 'assert' import { - sendGasIsInError, + gasFeeIsInError, + getGasLoadingError, } from '../send-gas-row.selectors.js' describe('send-gas-row selectors', () => { - describe('sendGasIsInError()', () => { + describe('getGasLoadingError()', () => { it('should return send.errors.gasLoading', () => { const state = { send: { @@ -15,7 +16,33 @@ describe('send-gas-row selectors', () => { }, } - assert.equal(sendGasIsInError(state), 'abc') + assert.equal(getGasLoadingError(state), 'abc') + }) + }) + + describe('gasFeeIsInError()', () => { + it('should return true if send.errors.gasFee is truthy', () => { + const state = { + send: { + errors: { + gasFee: 'def', + }, + }, + } + + assert.equal(gasFeeIsInError(state), true) + }) + + it('should return false send.errors.gasFee is falsely', () => { + const state = { + send: { + errors: { + gasFee: null, + }, + }, + } + + assert.equal(gasFeeIsInError(state), false) }) }) diff --git a/ui/app/components/send_/send.component.js b/ui/app/components/send_/send.component.js index 219b362f2..b1ab57a2e 100644 --- a/ui/app/components/send_/send.component.js +++ b/ui/app/components/send_/send.component.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import PersistentForm from '../../../lib/persistent-form' import { getAmountErrorObject, + getGasFeeErrorObject, getToAddressForGasUpdate, doesAmountErrorRequireUpdate, } from './send.utils' @@ -112,7 +113,19 @@ export default class SendTransactionScreen extends PersistentForm { selectedToken, tokenBalance, }) - updateSendErrors(amountErrorObject) + const gasFeeErrorObject = selectedToken + ? getGasFeeErrorObject({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + }) + : { gasFee: null } + updateSendErrors(Object.assign(amountErrorObject, gasFeeErrorObject)) } if (!uninitialized) { @@ -143,6 +156,10 @@ export default class SendTransactionScreen extends PersistentForm { this.updateGas() } + componentWillUnmount () { + this.props.resetSendState() + } + render () { const { history } = this.props diff --git a/ui/app/components/send_/send.container.js b/ui/app/components/send_/send.container.js index 185653c5f..44ebd2792 100644 --- a/ui/app/components/send_/send.container.js +++ b/ui/app/components/send_/send.container.js @@ -28,6 +28,7 @@ import { setGasTotal, } from '../../actions' import { + resetSendState, updateSendErrors, } from '../../ducks/send.duck' import { @@ -87,5 +88,6 @@ function mapDispatchToProps (dispatch) { })) }, updateSendErrors: newError => dispatch(updateSendErrors(newError)), + resetSendState: () => dispatch(resetSendState()), } } diff --git a/ui/app/components/send_/send.utils.js b/ui/app/components/send_/send.utils.js index 34275248f..c4537f335 100644 --- a/ui/app/components/send_/send.utils.js +++ b/ui/app/components/send_/send.utils.js @@ -30,6 +30,7 @@ module.exports = { estimateGasPriceFromRecentBlocks, generateTokenTransferData, getAmountErrorObject, + getGasFeeErrorObject, getToAddressForGasUpdate, isBalanceSufficient, isTokenBalanceSufficient, @@ -110,9 +111,9 @@ function getAmountErrorObject ({ tokenBalance, }) { let insufficientFunds = false - if (gasTotal && conversionRate) { + if (gasTotal && conversionRate && !selectedToken) { insufficientFunds = !isBalanceSufficient({ - amount: selectedToken ? '0x0' : amount, + amount, amountConversionRate, balance, conversionRate, @@ -149,6 +150,34 @@ function getAmountErrorObject ({ return { amount: amountError } } +function getGasFeeErrorObject ({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, +}) { + let gasFeeError = null + + if (gasTotal && conversionRate) { + const insufficientFunds = !isBalanceSufficient({ + amount: '0x0', + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + }) + + if (insufficientFunds) { + gasFeeError = INSUFFICIENT_FUNDS_ERROR + } + } + + return { gasFee: gasFeeError } +} + function calcTokenBalance ({ selectedToken, usersToken }) { const { decimals } = selectedToken || {} return calcTokenAmount(usersToken.balance.toString(), decimals) + '' diff --git a/ui/app/components/send_/tests/send-component.test.js b/ui/app/components/send_/tests/send-component.test.js index 4ba9b226d..6194ec508 100644 --- a/ui/app/components/send_/tests/send-component.test.js +++ b/ui/app/components/send_/tests/send-component.test.js @@ -12,9 +12,11 @@ const propsMethodSpies = { updateAndSetGasTotal: sinon.spy(), updateSendErrors: sinon.spy(), updateSendTokenBalance: sinon.spy(), + resetSendState: sinon.spy(), } const utilsMethodStubs = { getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }), + getGasFeeErrorObject: sinon.stub().returns({ gasFee: 'mockGasFeeError' }), doesAmountErrorRequireUpdate: sinon.stub().callsFake(obj => obj.balance !== obj.prevBalance), } @@ -50,6 +52,7 @@ describe('Send Component', function () { updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal} updateSendErrors={propsMethodSpies.updateSendErrors} updateSendTokenBalance={propsMethodSpies.updateSendTokenBalance} + resetSendState={propsMethodSpies.resetSendState} />) }) @@ -58,6 +61,7 @@ describe('Send Component', function () { SendTransactionScreen.prototype.updateGas.resetHistory() utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory() utilsMethodStubs.getAmountErrorObject.resetHistory() + utilsMethodStubs.getGasFeeErrorObject.resetHistory() propsMethodSpies.updateAndSetGasTotal.resetHistory() propsMethodSpies.updateSendErrors.resetHistory() propsMethodSpies.updateSendTokenBalance.resetHistory() @@ -77,6 +81,15 @@ describe('Send Component', function () { }) }) + describe('componentWillUnmount', () => { + it('should call this.props.resetSendState', () => { + propsMethodSpies.resetSendState.resetHistory() + assert.equal(propsMethodSpies.resetSendState.callCount, 0) + wrapper.instance().componentWillUnmount() + assert.equal(propsMethodSpies.resetSendState.callCount, 1) + }) + }) + describe('componentDidUpdate', () => { it('should call doesAmountErrorRequireUpdate with the expected params', () => { utilsMethodStubs.getAmountErrorObject.resetHistory() @@ -133,8 +146,66 @@ describe('Send Component', function () { ) }) - it('should call updateSendErrors with the expected params', () => { + it('should call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true and selectedToken is truthy', () => { + utilsMethodStubs.getGasFeeErrorObject.resetHistory() + wrapper.instance().componentDidUpdate({ + from: { + balance: 'balanceChanged', + }, + }) + assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 1) + assert.deepEqual( + utilsMethodStubs.getGasFeeErrorObject.getCall(0).args[0], + { + amount: 'mockAmount', + amountConversionRate: 'mockAmountConversionRate', + balance: 'mockBalance', + conversionRate: 10, + gasTotal: 'mockGasTotal', + primaryCurrency: 'mockPrimaryCurrency', + selectedToken: 'mockSelectedToken', + tokenBalance: 'mockTokenBalance', + } + ) + }) + + it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns false', () => { + utilsMethodStubs.getGasFeeErrorObject.resetHistory() + wrapper.instance().componentDidUpdate({ + from: { address: 'mockAddress', balance: 'mockBalance' }, + }) + assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0) + }) + + it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true but selectedToken is falsy', () => { + utilsMethodStubs.getGasFeeErrorObject.resetHistory() + wrapper.setProps({ selectedToken: null }) + wrapper.instance().componentDidUpdate({ + from: { + balance: 'balanceChanged', + }, + }) + assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0) + }) + + it('should call updateSendErrors with the expected params if selectedToken is falsy', () => { + propsMethodSpies.updateSendErrors.resetHistory() + wrapper.setProps({ selectedToken: null }) + wrapper.instance().componentDidUpdate({ + from: { + balance: 'balanceChanged', + }, + }) + assert.equal(propsMethodSpies.updateSendErrors.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendErrors.getCall(0).args[0], + { amount: 'mockAmountError', gasFee: null } + ) + }) + + it('should call updateSendErrors with the expected params if selectedToken is truthy', () => { propsMethodSpies.updateSendErrors.resetHistory() + wrapper.setProps({ selectedToken: 'someToken' }) wrapper.instance().componentDidUpdate({ from: { balance: 'balanceChanged', @@ -143,7 +214,7 @@ describe('Send Component', function () { assert.equal(propsMethodSpies.updateSendErrors.callCount, 1) assert.deepEqual( propsMethodSpies.updateSendErrors.getCall(0).args[0], - { amount: 'mockAmountError'} + { amount: 'mockAmountError', gasFee: 'mockGasFeeError' } ) }) diff --git a/ui/app/components/send_/tests/send-container.test.js b/ui/app/components/send_/tests/send-container.test.js index 91484f4d8..7a9120d24 100644 --- a/ui/app/components/send_/tests/send-container.test.js +++ b/ui/app/components/send_/tests/send-container.test.js @@ -12,6 +12,7 @@ const actionSpies = { } const duckActionSpies = { updateSendErrors: sinon.spy(), + resetSendState: sinon.spy(), } proxyquire('../send.container.js', { @@ -152,6 +153,17 @@ describe('send container', () => { }) }) + describe('resetSendState()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.resetSendState() + assert(dispatchSpy.calledOnce) + assert.equal( + duckActionSpies.resetSendState.getCall(0).args.length, + 0 + ) + }) + }) + }) }) diff --git a/ui/app/components/send_/tests/send-utils.test.js b/ui/app/components/send_/tests/send-utils.test.js index a518a64e9..b8579e0e4 100644 --- a/ui/app/components/send_/tests/send-utils.test.js +++ b/ui/app/components/send_/tests/send-utils.test.js @@ -17,7 +17,11 @@ const { } = require('../send.constants') const stubs = { - addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b), + addCurrencies: sinon.stub().callsFake((a, b, obj) => { + if (String(a).match(/^0x.+/)) a = Number(String(a).slice(2)) + if (String(b).match(/^0x.+/)) b = Number(String(b).slice(2)) + return a + b + }), conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)), conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value), multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`), @@ -49,6 +53,7 @@ const { estimateGasPriceFromRecentBlocks, generateTokenTransferData, getAmountErrorObject, + getGasFeeErrorObject, getToAddressForGasUpdate, calcTokenBalance, isBalanceSufficient, @@ -143,6 +148,18 @@ describe('send utils', () => { primaryCurrency: 'ABC', expectedResult: { amount: INSUFFICIENT_FUNDS_ERROR }, }, + 'should not return insufficientFunds error if selectedToken is truthy': { + amount: '0x0', + amountConversionRate: 2, + balance: 1, + conversionRate: 3, + gasTotal: 17, + primaryCurrency: 'ABC', + selectedToken: { symbole: 'DEF', decimals: 0 }, + decimals: 0, + tokenBalance: 'sometokenbalance', + expectedResult: { amount: null }, + }, 'should return insufficientTokens error if token is selected and isTokenBalanceSufficient returns false': { amount: '0x10', amountConversionRate: 2, @@ -163,6 +180,32 @@ describe('send utils', () => { }) }) + describe('getGasFeeErrorObject()', () => { + const config = { + 'should return insufficientFunds error if isBalanceSufficient returns false': { + amountConversionRate: 2, + balance: 16, + conversionRate: 3, + gasTotal: 17, + primaryCurrency: 'ABC', + expectedResult: { gasFee: INSUFFICIENT_FUNDS_ERROR }, + }, + 'should return null error if isBalanceSufficient returns true': { + amountConversionRate: 2, + balance: 16, + conversionRate: 3, + gasTotal: 15, + primaryCurrency: 'ABC', + expectedResult: { gasFee: null }, + }, + } + Object.entries(config).map(([description, obj]) => { + it(description, () => { + assert.deepEqual(getGasFeeErrorObject(obj), obj.expectedResult) + }) + }) + }) + describe('calcTokenBalance()', () => { it('should return the calculated token blance', () => { assert.equal(calcTokenBalance({ @@ -222,6 +265,7 @@ describe('send utils', () => { describe('isTokenBalanceSufficient()', () => { it('should correctly call conversionUtil and return the result of calling conversionGTE', () => { stubs.conversionGTE.resetHistory() + stubs.conversionUtil.resetHistory() const result = isTokenBalanceSufficient({ amount: '0x10', tokenBalance: 123, diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js index df3bd59bb..99ca7335c 100644 --- a/ui/app/components/token-balance.js +++ b/ui/app/components/token-balance.js @@ -98,6 +98,10 @@ TokenBalance.prototype.componentDidUpdate = function (nextProps) { } TokenBalance.prototype.updateBalance = function (tokens = []) { + if (!this.tracker.running) { + return + } + const [{ string, symbol }] = tokens this.setState({ @@ -110,5 +114,7 @@ TokenBalance.prototype.updateBalance = function (tokens = []) { TokenBalance.prototype.componentWillUnmount = function () { if (!this.tracker) return this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) } diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 4189cf801..42351cf89 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -158,12 +158,17 @@ TokenList.prototype.componentDidUpdate = function (nextProps) { } TokenList.prototype.updateBalances = function (tokens) { + if (!this.tracker.running) { + return + } this.setState({ tokens, isLoading: false }) } TokenList.prototype.componentWillUnmount = function () { if (!this.tracker) return this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) } // function uniqueMergeTokens (tokensA, tokensB = []) { diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js index 9a2fb5311..e539514ec 100644 --- a/ui/app/components/tx-list-item.js +++ b/ui/app/components/tx-list-item.js @@ -54,6 +54,8 @@ function TxListItem () { fiatTotal: null, isTokenTx: null, } + + this.unmounted = false } TxListItem.prototype.componentDidMount = async function () { @@ -67,9 +69,16 @@ TxListItem.prototype.componentDidMount = async function () { ? await this.getSendTokenTotal() : this.getSendEtherTotal() + if (this.unmounted) { + return + } this.setState({ total, fiatTotal, isTokenTx }) } +TxListItem.prototype.componentWillUnmount = function () { + this.unmounted = true +} + TxListItem.prototype.getAddressText = function () { const { address, diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index 667e45ba2..bbfd85c90 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -332,3 +332,12 @@ $wallet-view-bg: $alabaster; align-items: center; flex: 1 0 auto; } + +.first-view-main-wrapper { + display: flex; + width: 100%; + height: 100%; + justify-content: center; + padding: 0 10px; + background: white; +} diff --git a/ui/app/css/itcss/components/request-signature.scss b/ui/app/css/itcss/components/request-signature.scss index 4707ff60e..b607aded3 100644 --- a/ui/app/css/itcss/components/request-signature.scss +++ b/ui/app/css/itcss/components/request-signature.scss @@ -181,6 +181,7 @@ overflow-wrap: break-word; border-bottom: 1px solid #d2d8dd; padding: 6px 18px 15px; + white-space: pre-line; } &__help-link { diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss index 4b706abce..49d0c290e 100644 --- a/ui/app/css/itcss/components/token-list.scss +++ b/ui/app/css/itcss/components/token-list.scss @@ -34,6 +34,7 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( &__fiat-amount { margin-top: .25%; font-size: 105%; + width: 100%; text-transform: uppercase; @media #{$wallet-balance-breakpoint-range} { diff --git a/ui/app/ducks/send.duck.js b/ui/app/ducks/send.duck.js index 055cc05c1..db01bbaa9 100644 --- a/ui/app/ducks/send.duck.js +++ b/ui/app/ducks/send.duck.js @@ -6,6 +6,7 @@ const CLOSE_FROM_DROPDOWN = 'metamask/send/CLOSE_FROM_DROPDOWN' const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN' const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN' const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS' +const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE' // TODO: determine if this approach to initState is consistent with conventional ducks pattern const initState = { @@ -42,6 +43,8 @@ export default function reducer ({ send: sendState = initState }, action = {}) { ...action.value, }, }) + case RESET_SEND_STATE: + return extend({}, initState) default: return newState } @@ -70,3 +73,7 @@ export function updateSendErrors (errorObject) { value: errorObject, } } + +export function resetSendState () { + return { type: RESET_SEND_STATE } +} diff --git a/ui/app/ducks/tests/send-duck.test.js b/ui/app/ducks/tests/send-duck.test.js index c06cf55d2..c101132d9 100644 --- a/ui/app/ducks/tests/send-duck.test.js +++ b/ui/app/ducks/tests/send-duck.test.js @@ -24,6 +24,7 @@ describe('Send Duck', () => { const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN' const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN' const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS' + const RESET_SEND_STATE = 'metamask/send/RESET_SEND_STATE' describe('SendReducer()', () => { it('should initialize state', () => { @@ -105,6 +106,15 @@ describe('Send Duck', () => { }) ) }) + + it('should return the initial state in response to a RESET_SEND_STATE action', () => { + assert.deepEqual( + SendReducer(mockState, { + type: RESET_SEND_STATE, + }), + Object.assign({}, initState) + ) + }) }) describe('openFromDropdown', () => { diff --git a/ui/app/helpers/with-token-tracker.js b/ui/app/helpers/with-token-tracker.js index e24517c18..8608b15f4 100644 --- a/ui/app/helpers/with-token-tracker.js +++ b/ui/app/helpers/with-token-tracker.js @@ -75,6 +75,9 @@ const withTokenTracker = WrappedComponent => { } updateBalance (tokens = []) { + if (!this.tracker.running) { + return + } const [{ string, symbol }] = tokens this.setState({ string, symbol, error: null }) } diff --git a/ui/app/selectors.js b/ui/app/selectors.js index cf0affe9c..3e2253550 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -17,8 +17,6 @@ const selectors = { accountsWithSendEtherInfoSelector, getCurrentAccountWithSendEtherInfo, getGasIsLoading, - getGasPrice, - getGasLimit, getForceGasMin, getAddressBook, getSendFrom, @@ -122,14 +120,6 @@ function getGasIsLoading (state) { return state.appState.gasIsLoading } -function getGasPrice (state) { - return state.metamask.send.gasPrice -} - -function getGasLimit (state) { - return state.metamask.send.gasLimit -} - function getForceGasMin (state) { return state.metamask.send.forceGasMin } |