diff options
author | pldespaigne <pl.despaigne@gmail.com> | 2019-05-31 00:22:55 +0800 |
---|---|---|
committer | pldespaigne <pl.despaigne@gmail.com> | 2019-05-31 00:22:55 +0800 |
commit | 9a658ee53d1f75ce07c33581ac1189fa8c4fd173 (patch) | |
tree | ea92ef1971ffaa72c29bf16904906bc1841654c7 /ui/app | |
parent | 9b87aaae1907eb04ca0a4055b5bb2c863e56aa39 (diff) | |
parent | 681f3f67b89b64fc837df1103198b641c7e7b2d6 (diff) | |
download | tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar.gz tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar.bz2 tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar.lz tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar.xz tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.tar.zst tangerine-wallet-browser-9a658ee53d1f75ce07c33581ac1189fa8c4fd173.zip |
merge
Diffstat (limited to 'ui/app')
261 files changed, 3404 insertions, 1030 deletions
diff --git a/ui/app/components/app/account-panel.js b/ui/app/components/app/account-panel.js index 79882f34a..e61cb8ad6 100644 --- a/ui/app/components/app/account-panel.js +++ b/ui/app/components/app/account-panel.js @@ -69,18 +69,9 @@ AccountPanel.prototype.render = function () { ) } -function balanceOrFaucetingIndication (account, isFauceting) { - // Temporarily deactivating isFauceting indication - // because it shows fauceting for empty restored accounts. - if (/* isFauceting*/ false) { - return { - key: 'Account is auto-funding.', - value: 'Please wait.', - } - } else { - return { - key: 'BALANCE', - value: formatBalance(account.balance), - } +function balanceOrFaucetingIndication (account) { + return { + key: 'BALANCE', + value: formatBalance(account.balance), } } diff --git a/ui/app/components/app/add-token-button/index.scss b/ui/app/components/app/add-token-button/index.scss index 39f404716..c4350a2d3 100644 --- a/ui/app/components/app/add-token-button/index.scss +++ b/ui/app/components/app/add-token-button/index.scss @@ -17,10 +17,7 @@ } &__button { - font-size: 0.75rem; + @extend %small-link; margin: 1rem; - text-transform: uppercase; - color: $curious-blue; - cursor: pointer; } } diff --git a/ui/app/components/app/app-header/index.scss b/ui/app/components/app/app-header/index.scss index 325844af5..d46b16f25 100644 --- a/ui/app/components/app/app-header/index.scss +++ b/ui/app/components/app/app-header/index.scss @@ -48,7 +48,6 @@ &__contents { display: flex; - justify-content: space-between; flex-flow: row nowrap; width: 100%; @@ -74,17 +73,33 @@ flex-direction: row; align-items: center; cursor: pointer; + flex: 0 0 auto; } &__account-menu-container { display: flex; flex-flow: row nowrap; align-items: center; + flex: 1 1 auto; + width: 0; + flex-flow: row nowrap; + justify-content: flex-end; } &__network-component-wrapper { display: flex; flex-direction: row; align-items: center; + flex: 1 0 auto; + width: 0; + justify-content: flex-end; + + .network-component.pointer { + max-width: 200px; + } + + .network-indicator { + width: 100%; + } } } diff --git a/ui/app/components/app/bn-as-decimal-input.js b/ui/app/components/app/bn-as-decimal-input.js index 9a033f893..834bab0a4 100644 --- a/ui/app/components/app/bn-as-decimal-input.js +++ b/ui/app/components/app/bn-as-decimal-input.js @@ -116,7 +116,7 @@ BnAsDecimalInput.prototype.render = function () { ) } -BnAsDecimalInput.prototype.setValid = function (message) { +BnAsDecimalInput.prototype.setValid = function () { this.setState({ invalid: null }) } diff --git a/ui/app/components/app/coinbase-form.js b/ui/app/components/app/coinbase-form.js deleted file mode 100644 index 24d287604..000000000 --- a/ui/app/components/app/coinbase-form.js +++ /dev/null @@ -1,69 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const actions = require('../../store/actions') - -CoinbaseForm.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps)(CoinbaseForm) - - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -inherits(CoinbaseForm, Component) - -function CoinbaseForm () { - Component.call(this) -} - -CoinbaseForm.prototype.render = function () { - var props = this.props - - return h('.flex-column', { - style: { - marginTop: '35px', - padding: '25px', - width: '100%', - }, - }, [ - h('.flex-row', { - style: { - justifyContent: 'space-around', - margin: '33px', - marginTop: '0px', - }, - }, [ - h('button.btn-green', { - onClick: this.toCoinbase.bind(this), - }, this.context.t('continueToCoinbase')), - - h('button.btn-red', { - onClick: () => props.dispatch(actions.goHome()), - }, this.context.t('cancel')), - ]), - ]) -} - -CoinbaseForm.prototype.toCoinbase = function () { - const props = this.props - const address = props.buyView.buyAddress - props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) -} - -CoinbaseForm.prototype.renderLoading = function () { - return h('img', { - style: { - width: '27px', - marginRight: '-27px', - }, - src: 'images/loading.svg', - }) -} diff --git a/ui/app/components/app/customize-gas-modal/index.js b/ui/app/components/app/customize-gas-modal/index.js index dca77bb00..1f9436810 100644 --- a/ui/app/components/app/customize-gas-modal/index.js +++ b/ui/app/components/app/customize-gas-modal/index.js @@ -18,11 +18,11 @@ const { MIN_GAS_PRICE_DEC, MIN_GAS_LIMIT_DEC, MIN_GAS_PRICE_GWEI, -} = require('../send/send.constants') +} = require('../../../pages/send/send.constants') const { isBalanceSufficient, -} = require('../send/send.utils') +} = require('../../../pages/send/send.utils') const { conversionUtil, @@ -47,7 +47,7 @@ const { const { getGasPrice, getGasLimit, -} = require('../send/send.selectors') +} = require('../../../pages/send/send.selectors') function mapStateToProps (state) { const selectedToken = getSelectedToken(state) @@ -382,7 +382,7 @@ CustomizeGasModal.prototype.render = function () { onClick: this.props.hideModal, }, [this.context.t('cancel')]), h(Button, { - type: 'primary', + type: 'secondary', className: 'send-v2__customize-gas__save', onClick: () => !error && this.save(newGasPrice, gasLimit, gasTotal), disabled: error, diff --git a/ui/app/components/app/dropdowns/account-details-dropdown.js b/ui/app/components/app/dropdowns/account-details-dropdown.js index 3d4598946..cbeccdd81 100644 --- a/ui/app/components/app/dropdowns/account-details-dropdown.js +++ b/ui/app/components/app/dropdowns/account-details-dropdown.js @@ -4,7 +4,7 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../../store/actions') -const { getSelectedIdentity } = require('../../../selectors/selectors') +const { getSelectedIdentity, getRpcPrefsForCurrentProvider } = require('../../../selectors/selectors') const genAccountLink = require('../../../../lib/account-link.js') const { Menu, Item, CloseArea } = require('./components/menu') @@ -20,6 +20,7 @@ function mapStateToProps (state) { selectedIdentity: getSelectedIdentity(state), network: state.metamask.network, keyrings: state.metamask.keyrings, + rpcPrefs: getRpcPrefsForCurrentProvider(state), } } @@ -28,8 +29,8 @@ function mapDispatchToProps (dispatch) { showAccountDetailModal: () => { dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) }, - viewOnEtherscan: (address, network) => { - global.platform.openWindow({ url: genAccountLink(address, network) }) + viewOnEtherscan: (address, network, rpcPrefs) => { + global.platform.openWindow({ url: genAccountLink(address, network, rpcPrefs) }) }, showRemoveAccountConfirmationModal: (identity) => { return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity })) @@ -56,7 +57,9 @@ AccountDetailsDropdown.prototype.render = function () { keyrings, showAccountDetailModal, viewOnEtherscan, - showRemoveAccountConfirmationModal } = this.props + showRemoveAccountConfirmationModal, + rpcPrefs, + } = this.props const address = selectedIdentity.address @@ -112,10 +115,12 @@ AccountDetailsDropdown.prototype.render = function () { name: 'Clicked View on Etherscan', }, }) - viewOnEtherscan(address, network) + viewOnEtherscan(address, network, rpcPrefs) this.props.onClose() }, - text: this.context.t('viewOnEtherscan'), + text: (rpcPrefs.blockExplorerUrl + ? this.context.t('blockExplorerView', [rpcPrefs.blockExplorerUrl.match(/^https?:\/\/(.+)/)[1]]) + : this.context.t('viewOnEtherscan')), icon: h(`img`, { src: 'images/open-etherscan.svg', style: { height: '15px' } }), }), isRemovable ? h(Item, { diff --git a/ui/app/components/app/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js index 3d9037a06..378ad3ba6 100644 --- a/ui/app/components/app/dropdowns/network-dropdown.js +++ b/ui/app/components/app/dropdowns/network-dropdown.js @@ -10,7 +10,7 @@ const Dropdown = require('./components/dropdown').Dropdown const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem const NetworkDropdownIcon = require('./components/network-dropdown-icon') const R = require('ramda') -const { ADVANCED_ROUTE } = require('../../../helpers/constants/routes') +const { NETWORKS_ROUTE } = require('../../../helpers/constants/routes') // classes from nodes of the toggle element. const notToggleElementClassnames = [ @@ -49,6 +49,7 @@ function mapDispatchToProps (dispatch) { }, showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), + setNetworksTabAddMode: isInAddMode => dispatch(actions.setNetworksTabAddMode(isInAddMode)), } } @@ -72,7 +73,7 @@ module.exports = compose( // TODO: specify default props and proptypes NetworkDropdown.prototype.render = function () { const props = this.props - const { provider: { type: providerType, rpcTarget: activeNetwork } } = props + const { provider: { type: providerType, rpcTarget: activeNetwork }, setNetworksTabAddMode } = props const rpcListDetail = props.frequentRpcListDetail const isOpen = this.props.networkDropdownOpen const dropdownMenuItemStyle = { @@ -207,6 +208,28 @@ NetworkDropdown.prototype.render = function () { h( DropdownMenuItem, { + key: 'goerli', + closeMenu: () => this.props.hideNetworkDropdown(), + onClick: () => this.handleClick('goerli'), + style: dropdownMenuItemStyle, + }, + [ + providerType === 'goerli' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), + h(NetworkDropdownIcon, { + backgroundColor: '#3099f2', // $dodger-blue + isSelected: providerType === 'goerli', + }), + h('span.network-name-item', { + style: { + color: providerType === 'goerli' ? '#ffffff' : '#9b9b9b', + }, + }, this.context.t('goerli')), + ] + ), + + h( + DropdownMenuItem, + { key: 'default', closeMenu: () => this.props.hideNetworkDropdown(), onClick: () => this.handleClick('localhost'), @@ -233,7 +256,10 @@ NetworkDropdown.prototype.render = function () { DropdownMenuItem, { closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => this.props.history.push(ADVANCED_ROUTE), + onClick: () => { + setNetworksTabAddMode(true) + this.props.history.push(NETWORKS_ROUTE) + }, style: dropdownMenuItemStyle, }, [ @@ -285,6 +311,10 @@ NetworkDropdown.prototype.getNetworkName = function () { name = this.context.t('kovan') } else if (providerName === 'rinkeby') { name = this.context.t('rinkeby') + } else if (providerName === 'localhost') { + name = this.context.t('localhost') + } else if (providerName === 'goerli') { + name = this.context.t('goerli') } else { name = provider.nickname || this.context.t('unknownNetwork') } diff --git a/ui/app/components/app/dropdowns/tests/network-dropdown.test.js b/ui/app/components/app/dropdowns/tests/network-dropdown.test.js index 91e7899a7..4a81b973f 100644 --- a/ui/app/components/app/dropdowns/tests/network-dropdown.test.js +++ b/ui/app/components/app/dropdowns/tests/network-dropdown.test.js @@ -62,7 +62,7 @@ describe('Network Dropdown', () => { }) it('renders 7 DropDownMenuItems ', () => { - assert.equal(wrapper.find(DropdownMenuItem).length, 7) + assert.equal(wrapper.find(DropdownMenuItem).length, 8) }) it('checks background color for first NetworkDropdownIcon', () => { @@ -82,15 +82,19 @@ describe('Network Dropdown', () => { }) it('checks background color for fifth NetworkDropdownIcon', () => { - assert.equal(wrapper.find(NetworkDropdownIcon).at(4).prop('innerBorder'), '1px solid #9b9b9b') + assert.equal(wrapper.find(NetworkDropdownIcon).at(4).prop('backgroundColor'), '#3099f2') // Goerli Blue + }) + + it('checks background color for sixth NetworkDropdownIcon', () => { + assert.equal(wrapper.find(NetworkDropdownIcon).at(5).prop('innerBorder'), '1px solid #9b9b9b') }) it('checks dropdown for frequestRPCList from state ', () => { - assert.equal(wrapper.find(DropdownMenuItem).at(5).text(), '✓http://localhost:7545') + assert.equal(wrapper.find(DropdownMenuItem).at(6).text(), '✓http://localhost:7545') }) - it('checks background color for sixth NetworkDropdownIcon', () => { - assert.equal(wrapper.find(NetworkDropdownIcon).at(5).prop('innerBorder'), '1px solid #9b9b9b') + it('checks background color for seventh NetworkDropdownIcon', () => { + assert.equal(wrapper.find(NetworkDropdownIcon).at(6).prop('innerBorder'), '1px solid #9b9b9b') }) }) diff --git a/ui/app/components/app/ens-input.js b/ui/app/components/app/ens-input.js index 274058a1b..f17f6c3d6 100644 --- a/ui/app/components/app/ens-input.js +++ b/ui/app/components/app/ens-input.js @@ -10,7 +10,7 @@ const networkMap = require('ethjs-ens/lib/network-map.json') const ensRE = /.+\..+$/ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' const connect = require('react-redux').connect -const ToAutoComplete = require('./send/to-autocomplete').default +const ToAutoComplete = require('../../pages/send/to-autocomplete').default const log = require('loglevel') const { isValidENSAddress } = require('../../helpers/utils/util') @@ -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 }) } } @@ -144,7 +150,7 @@ EnsInput.prototype.ensIcon = function (recipient) { }, this.ensIconContents(recipient)) } -EnsInput.prototype.ensIconContents = function (recipient) { +EnsInput.prototype.ensIconContents = function () { const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS } if (toError) return diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js index 95894140c..d6c259033 100644 --- a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js @@ -58,7 +58,7 @@ export default class AdvancedTabContent extends Component { } } - gasInput ({ labelKey, value, onChange, insufficientBalance, showGWEI, customPriceIsSafe, isSpeedUp }) { + gasInput ({ labelKey, value, onChange, insufficientBalance, customPriceIsSafe, isSpeedUp }) { const { isInError, errorText, diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js index 90fef1a1b..73bc13481 100644 --- a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js @@ -17,8 +17,8 @@ function convertGasLimitForInputs (gasLimitInHexWEI) { const mapDispatchToProps = dispatch => { return { - showGasPriceInfoModal: modalName => dispatch(showModal({ name: 'GAS_PRICE_INFO_MODAL' })), - showGasLimitInfoModal: modalName => dispatch(showModal({ name: 'GAS_LIMIT_INFO_MODAL' })), + showGasPriceInfoModal: () => dispatch(showModal({ name: 'GAS_PRICE_INFO_MODAL' })), + showGasLimitInfoModal: () => dispatch(showModal({ name: 'GAS_LIMIT_INFO_MODAL' })), } } diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js index ad8628621..eab3434df 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -67,7 +67,7 @@ export default class AdvancedTabContent extends Component { } } - gasInput ({ labelKey, value, onChange, insufficientBalance, showGWEI, customPriceIsSafe, isSpeedUp }) { + gasInput ({ labelKey, value, onChange, insufficientBalance, customPriceIsSafe, isSpeedUp }) { const { isInError, errorText, @@ -148,7 +148,6 @@ export default class AdvancedTabContent extends Component { customGasPrice, updateCustomGasPrice, customGasLimit, - updateCustomGasLimit, insufficientBalance, customPriceIsSafe, isSpeedUp, diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js index 8aaccafd5..e18c1067e 100644 --- a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -122,8 +122,6 @@ export default class GasModalPageContainer extends Component { } renderTabs ({ - originalTotalFiat, - originalTotalEth, newTotalFiat, newTotalEth, sendAmount, 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 d541056f4..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, @@ -63,9 +70,11 @@ import { import { calcGasTotal, isBalanceSufficient, -} from '../../send/send.utils' +} 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/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js b/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js index 0456f5262..14952a49a 100644 --- a/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js +++ b/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js @@ -49,7 +49,7 @@ export default class GasPriceButtonGroup extends Component { priceInHexWei, ...renderableGasInfo }, { - buttonDataLoading, + buttonDataLoading: _, handleGasPriceSelection, ...buttonContentPropsAndFlags }, index) { diff --git a/ui/app/components/app/gas-customization/gas-price-button-group/index.scss b/ui/app/components/app/gas-customization/gas-price-button-group/index.scss index cb2f3ecf1..92b4aba42 100644 --- a/ui/app/components/app/gas-customization/gas-price-button-group/index.scss +++ b/ui/app/components/app/gas-customization/gas-price-button-group/index.scss @@ -65,6 +65,7 @@ .gas-price-button-group--small { display: flex; justify-content: stretch; + height: 54px; @media screen and (max-width: $break-small) { max-width: 260px; @@ -80,10 +81,14 @@ &__label { font-weight: 500; + line-height: 16px; + padding-bottom: 4px; } &__primary-currency { font-size: 12px; + line-height: 12px; + padding-bottom: 2px; @media screen and (max-width: 575px) { font-size: 10px; @@ -92,6 +97,8 @@ &__secondary-currency { font-size: 12px; + line-height: 12px; + padding-bottom: 2px; @media screen and (max-width: 575px) { font-size: 10px; @@ -99,19 +106,13 @@ } &__loading-container { - height: 78px; + height: 54px; } .button-group__button, .button-group__button--active { - height: 78px; background: white; color: $scorpion; - padding-top: 9px; - padding-left: 8.5px; - - @media screen and (max-width: $break-small) { - padding-left: 4px; - } + padding: 0 4px; div { display: flex; diff --git a/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js index f19dafcc1..55512ce09 100644 --- a/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js +++ b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js @@ -68,7 +68,7 @@ export function handleChartUpdate ({ chart, gasPrices, newPrice, cssId }) { export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) { const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition) - const closestHigherValueIndex = gasPrices.findIndex((e, i, a) => e > priceToPosition) + const closestHigherValueIndex = gasPrices.findIndex((e) => e > priceToPosition) return { closestLowerValueIndex, closestHigherValueIndex, @@ -133,7 +133,7 @@ export function setTickPosition (axis, n, newPosition, secondNewPosition) { d3.select('#chart') .select(`.c3-axis-${axis}`) .selectAll('.tick') - .filter((d, i) => i === n) + .filter((_, i) => i === n) .select('text') .attr(positionToShift, 0) .select('tspan') @@ -284,7 +284,7 @@ export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimate }) return text + '</table>' + "<div class='tooltip-arrow'></div>" }, - position: function (data) { + position: function () { if (d3.select('#overlayed-circle').empty()) { return { top: -100, left: -100 } } diff --git a/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js b/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js index 7dec7a85f..c960f49a7 100644 --- a/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js +++ b/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js @@ -6,7 +6,7 @@ import shallow from '../../../../../../lib/shallow-with-context' import * as d3 from 'd3' function timeout (time) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { setTimeout(resolve, time) }) } diff --git a/ui/app/components/app/loading-network-screen/loading-network-screen.component.js b/ui/app/components/app/loading-network-screen/loading-network-screen.component.js index 348a997c8..97b16d08f 100644 --- a/ui/app/components/app/loading-network-screen/loading-network-screen.component.js +++ b/ui/app/components/app/loading-network-screen/loading-network-screen.component.js @@ -45,6 +45,10 @@ export default class LoadingNetworkScreen extends PureComponent { name = this.context.t('connectingToKovan') } else if (providerName === 'rinkeby') { name = this.context.t('connectingToRinkeby') + } else if (providerName === 'localhost') { + name = this.context.t('connectingToLocalhost') + } else if (providerName === 'goerli') { + name = this.context.t('connectingToGoerli') } else { name = this.context.t('connectingTo', [providerId]) } diff --git a/ui/app/components/app/modal/modal.component.js b/ui/app/components/app/modal/modal.component.js index 49e131b3c..44b180ac8 100644 --- a/ui/app/components/app/modal/modal.component.js +++ b/ui/app/components/app/modal/modal.component.js @@ -20,7 +20,7 @@ export default class Modal extends PureComponent { } static defaultProps = { - submitType: 'primary', + submitType: 'secondary', cancelType: 'default', } diff --git a/ui/app/components/app/modal/tests/modal.component.test.js b/ui/app/components/app/modal/tests/modal.component.test.js index a13d7c06a..5922177a6 100644 --- a/ui/app/components/app/modal/tests/modal.component.test.js +++ b/ui/app/components/app/modal/tests/modal.component.test.js @@ -12,7 +12,7 @@ describe('Modal Component', () => { assert.equal(wrapper.find('.modal-container').length, 1) const buttons = wrapper.find(Button) assert.equal(buttons.length, 1) - assert.equal(buttons.at(0).props().type, 'primary') + assert.equal(buttons.at(0).props().type, 'secondary') }) it('should render a modal with a cancel and a submit button', () => { @@ -38,7 +38,7 @@ describe('Modal Component', () => { cancelButton.simulate('click') assert.equal(handleCancel.callCount, 1) - assert.equal(submitButton.props().type, 'primary') + assert.equal(submitButton.props().type, 'secondary') assert.equal(submitButton.props().children, 'Submit') assert.equal(handleSubmit.callCount, 0) submitButton.simulate('click') diff --git a/ui/app/components/app/modals/account-details-modal.js b/ui/app/components/app/modals/account-details-modal.js index 94ed04df9..6cffc918b 100644 --- a/ui/app/components/app/modals/account-details-modal.js +++ b/ui/app/components/app/modals/account-details-modal.js @@ -5,7 +5,7 @@ const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../../store/actions') const AccountModalContainer = require('./account-modal-container') -const { getSelectedIdentity } = require('../../../selectors/selectors') +const { getSelectedIdentity, getRpcPrefsForCurrentProvider } = require('../../../selectors/selectors') const genAccountLink = require('../../../../lib/account-link.js') const QrView = require('../../ui/qr-code') const EditableLabel = require('../../ui/editable-label') @@ -17,6 +17,7 @@ function mapStateToProps (state) { network: state.metamask.network, selectedIdentity: getSelectedIdentity(state), keyrings: state.metamask.keyrings, + rpcPrefs: getRpcPrefsForCurrentProvider(state), } } @@ -54,6 +55,7 @@ AccountDetailsModal.prototype.render = function () { showExportPrivateKeyModal, setAccountLabel, keyrings, + rpcPrefs, } = this.props const { name, address } = selectedIdentity @@ -84,15 +86,19 @@ AccountDetailsModal.prototype.render = function () { h('div.account-modal-divider'), h(Button, { - type: 'primary', + type: 'secondary', className: 'account-modal__button', - onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }), - }, this.context.t('etherscanView')), + onClick: () => { + global.platform.openWindow({ url: genAccountLink(address, network, rpcPrefs) }) + }, + }, (rpcPrefs.blockExplorerUrl + ? this.context.t('blockExplorerView', [rpcPrefs.blockExplorerUrl.match(/^https?:\/\/(.+)/)[1]]) + : this.context.t('viewOnEtherscan'))), // Holding on redesign for Export Private Key functionality exportPrivateKeyFeatureEnabled ? h(Button, { - type: 'primary', + type: 'secondary', className: 'account-modal__button', onClick: () => showExportPrivateKeyModal(), }, this.context.t('exportPrivateKey')) : null, diff --git a/ui/app/components/app/modals/customize-gas/customize-gas.component.js b/ui/app/components/app/modals/customize-gas/customize-gas.component.js index 5db5c79e7..387da2f79 100644 --- a/ui/app/components/app/modals/customize-gas/customize-gas.component.js +++ b/ui/app/components/app/modals/customize-gas/customize-gas.component.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import BigNumber from 'bignumber.js' import GasModalCard from '../../customize-gas-modal/gas-modal-card' -import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants' +import { MIN_GAS_PRICE_GWEI } from '../../../../pages/send/send.constants' import Button from '../../../ui/button' import { @@ -128,7 +128,7 @@ export default class CustomizeGas extends Component { { t('cancel') } </Button> <Button - type="primary" + type="secondary" className="customize-gas__save" onClick={() => { metricsEvent({ diff --git a/ui/app/components/app/modals/deposit-ether-modal.js b/ui/app/components/app/modals/deposit-ether-modal.js index 6f622a17c..f56069d65 100644 --- a/ui/app/components/app/modals/deposit-ether-modal.js +++ b/ui/app/components/app/modals/deposit-ether-modal.js @@ -48,7 +48,7 @@ function mapDispatchToProps (dispatch) { } inherits(DepositEtherModal, Component) -function DepositEtherModal (props, context) { +function DepositEtherModal (_, context) { Component.call(this) // need to set after i18n locale has loaded @@ -119,7 +119,7 @@ DepositEtherModal.prototype.renderRow = function ({ !hideButton && h('div.deposit-ether-modal__buy-row__button', [ h(Button, { - type: 'primary', + type: 'secondary', className: 'deposit-ether-modal__deposit-button', large: true, onClick: onButtonClick, @@ -133,7 +133,7 @@ DepositEtherModal.prototype.render = function () { const { network, toWyre, toCoinSwitch, address, toFaucet } = this.props const { buyingWithShapeshift } = this.state - const isTestNetwork = ['3', '4', '42'].find(n => n === network) + const isTestNetwork = ['3', '4', '5', '42'].find(n => n === network) const networkName = getNetworkDisplayName(network) return h('div.page-container.page-container--full-width.page-container--full-height', {}, [ diff --git a/ui/app/components/app/modals/edit-account-name-modal.js b/ui/app/components/app/modals/edit-account-name-modal.js index 41a9862e9..aa21765c4 100644 --- a/ui/app/components/app/modals/edit-account-name-modal.js +++ b/ui/app/components/app/modals/edit-account-name-modal.js @@ -66,7 +66,7 @@ EditAccountNameModal.prototype.render = function () { value: this.state.inputText, }, []), - h('button.btn-clear.edit-account-name-modal-save-button.allcaps', { + h('button.button.btn-secondary.edit-account-name-modal-save-button.allcaps', { onClick: () => { if (this.state.inputText.length !== 0) { setAccountLabel(identity.address, this.state.inputText) diff --git a/ui/app/components/app/modals/export-private-key-modal.js b/ui/app/components/app/modals/export-private-key-modal.js index 639887d4c..c3098a16c 100644 --- a/ui/app/components/app/modals/export-private-key-modal.js +++ b/ui/app/components/app/modals/export-private-key-modal.js @@ -98,7 +98,7 @@ ExportPrivateKeyModal.prototype.renderPasswordInput = function (privateKey) { }) } -ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, address, hideModal) { +ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, address, hideModal) { return h('div.export-private-key-buttons', {}, [ !privateKey && h(Button, { type: 'default', @@ -110,14 +110,14 @@ ExportPrivateKeyModal.prototype.renderButtons = function (privateKey, password, (privateKey ? ( h(Button, { - type: 'primary', + type: 'secondary', large: true, className: 'export-private-key__button', onClick: () => hideModal(), }, this.context.t('done')) ) : ( h(Button, { - type: 'primary', + type: 'secondary', large: true, className: 'export-private-key__button', onClick: () => this.exportAccountAndGetPrivateKey(this.state.password, address), @@ -171,7 +171,7 @@ ExportPrivateKeyModal.prototype.render = function () { h('div.private-key-password-warning', this.context.t('privateKeyWarning')), - this.renderButtons(privateKey, this.state.password, address, hideModal), + this.renderButtons(privateKey, address, hideModal), ]) } diff --git a/ui/app/components/app/modals/hide-token-confirmation-modal.js b/ui/app/components/app/modals/hide-token-confirmation-modal.js index 8a9a48fd2..e2b098923 100644 --- a/ui/app/components/app/modals/hide-token-confirmation-modal.js +++ b/ui/app/components/app/modals/hide-token-confirmation-modal.js @@ -67,12 +67,12 @@ HideTokenConfirmationModal.prototype.render = function () { ]), h('div.hide-token-confirmation__buttons', {}, [ - h('button.btn-cancel.hide-token-confirmation__button.allcaps', { + h('button.btn-default.hide-token-confirmation__button.btn--large', { onClick: () => hideModal(), }, [ this.context.t('cancel'), ]), - h('button.btn-clear.hide-token-confirmation__button.allcaps', { + h('button.btn-secondary.hide-token-confirmation__button.btn--large', { onClick: () => hideToken(address), }, [ this.context.t('hide'), diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js index 83595281f..ea7d71a73 100644 --- a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js +++ b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js @@ -4,7 +4,7 @@ import MetaMetricsOptInModal from './metametrics-opt-in-modal.component' import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' import { setParticipateInMetaMetrics } from '../../../../store/actions' -const mapStateToProps = (state, ownProps) => { +const mapStateToProps = (_, ownProps) => { const { unapprovedTxCount } = ownProps return { diff --git a/ui/app/components/app/modals/notification-modal.js b/ui/app/components/app/modals/notification-modal.js index 2d73b2cfa..b8503ec1a 100644 --- a/ui/app/components/app/modals/notification-modal.js +++ b/ui/app/components/app/modals/notification-modal.js @@ -37,11 +37,11 @@ class NotificationModal extends Component { showButtons && h('div.notification-modal__buttons', [ - showCancelButton && h('div.btn-cancel.notification-modal__buttons__btn', { + showCancelButton && h('div.btn-default.notification-modal__buttons__btn', { onClick: hideModal, }, 'Cancel'), - showConfirmButton && h('div.btn-clear.notification-modal__buttons__btn', { + showConfirmButton && h('div.button.btn-secondary.notification-modal__buttons__btn', { onClick: () => { onConfirm() hideModal() diff --git a/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js index 20915b5f9..a83ba8f8e 100644 --- a/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js +++ b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js @@ -71,7 +71,7 @@ export default class QrScanner extends Component { initCamera () { this.codeReader = new BrowserQRCodeReader() this.codeReader.getVideoInputDevices() - .then(videoInputDevices => { + .then(() => { clearTimeout(this.permissionChecker) this.checkPermisisions() this.codeReader.decodeFromInputVideoDevice(undefined, 'video') diff --git a/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js b/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js index d2af05573..aa74fd800 100644 --- a/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js +++ b/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js @@ -3,7 +3,7 @@ import { compose } from 'recompose' import RejectTransactionsModal from './reject-transactions.component' import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' -const mapStateToProps = (state, ownProps) => { +const mapStateToProps = (_, ownProps) => { const { unapprovedTxCount } = ownProps return { diff --git a/ui/app/components/app/network-display/index.scss b/ui/app/components/app/network-display/index.scss index e9f2f2057..d70786d20 100644 --- a/ui/app/components/app/network-display/index.scss +++ b/ui/app/components/app/network-display/index.scss @@ -26,6 +26,10 @@ &--rinkeby { background-color: lighten($tulip-tree, 35%); } + + &--goerli { + background-color: lighten($dodger-blue, 35%); + } } &__name { @@ -53,5 +57,9 @@ &--rinkeby { background-color: $tulip-tree; } + + &--goerli { + background-color: $dodger-blue; + } } } diff --git a/ui/app/components/app/network-display/network-display.component.js b/ui/app/components/app/network-display/network-display.component.js index 1142e8606..9ef5341b0 100644 --- a/ui/app/components/app/network-display/network-display.component.js +++ b/ui/app/components/app/network-display/network-display.component.js @@ -6,12 +6,14 @@ import { ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE, + GOERLI_CODE, } from '../../../../../app/scripts/controllers/network/enums' const networkToClassHash = { [MAINNET_CODE]: 'mainnet', [ROPSTEN_CODE]: 'ropsten', [RINKEYBY_CODE]: 'rinkeby', + [GOERLI_CODE]: 'goerli', [KOVAN_CODE]: 'kovan', } diff --git a/ui/app/components/app/network.js b/ui/app/components/app/network.js index e18404f42..9ee0a1e17 100644 --- a/ui/app/components/app/network.js +++ b/ui/app/components/app/network.js @@ -41,15 +41,15 @@ Network.prototype.render = function () { } else if (providerName === 'ropsten') { hoverText = context.t('ropsten') iconName = 'ropsten-test-network' - } else if (parseInt(networkNumber) === 3) { - hoverText = context.t('ropsten') - iconName = 'ropsten-test-network' } else if (providerName === 'kovan') { hoverText = context.t('kovan') iconName = 'kovan-test-network' } else if (providerName === 'rinkeby') { hoverText = context.t('rinkeby') iconName = 'rinkeby-test-network' + } else if (providerName === 'goerli') { + hoverText = context.t('goerli') + iconName = 'goerli-test-network' } else { hoverText = providerId iconName = 'private-network' @@ -60,9 +60,10 @@ Network.prototype.render = function () { className: classnames({ 'network-component--disabled': this.props.disabled, 'ethereum-network': providerName === 'mainnet', - 'ropsten-test-network': providerName === 'ropsten' || parseInt(networkNumber) === 3, + 'ropsten-test-network': providerName === 'ropsten', 'kovan-test-network': providerName === 'kovan', 'rinkeby-test-network': providerName === 'rinkeby', + 'goerli-test-network': providerName === 'goerli', }), title: hoverText, onClick: (event) => { @@ -113,33 +114,34 @@ Network.prototype.render = function () { h('.network-name', context.t('rinkeby')), h('i.fa.fa-chevron-down.fa-lg.network-caret'), ]) + case 'goerli-test-network': + return h('.network-indicator', [ + h(NetworkDropdownIcon, { + backgroundColor: '#3099f2', // $dodger-blue + nonSelectBackgroundColor: '#ecb23e', + loading: networkNumber === 'loading', + }), + h('.network-name', context.t('goerli')), + h('i.fa.fa-chevron-down.fa-lg.network-caret'), + ]) default: return h('.network-indicator', [ networkNumber === 'loading' - ? h('span.pointer.network-indicator', { - style: { - display: 'flex', - alignItems: 'center', - flexDirection: 'row', - }, + ? h('span.pointer.network-loading-spinner', { onClick: (event) => this.props.onClick(event), }, [ h('img', { title: context.t('attemptingConnect'), - style: { - width: '27px', - }, src: 'images/loading.svg', }), ]) : h('i.fa.fa-question-circle.fa-lg', { style: { - margin: '10px', color: 'rgb(125, 128, 130)', }, }), - h('.network-name', providerNick || context.t('privateNetwork')), + h('.network-name', providerName === 'localhost' ? context.t('localhost') : providerNick || context.t('privateNetwork')), h('i.fa.fa-chevron-down.fa-lg.network-caret'), ]) } diff --git a/ui/app/components/app/provider-page-container/provider-page-container.component.js b/ui/app/components/app/provider-page-container/provider-page-container.component.js index 910def2a3..1c655d404 100644 --- a/ui/app/components/app/provider-page-container/provider-page-container.component.js +++ b/ui/app/components/app/provider-page-container/provider-page-container.component.js @@ -5,12 +5,11 @@ import { PageContainerFooter } from '../../ui/page-container' export default class ProviderPageContainer extends PureComponent { static propTypes = { - approveProviderRequest: PropTypes.func.isRequired, + approveProviderRequestByOrigin: PropTypes.func.isRequired, + rejectProviderRequestByOrigin: PropTypes.func.isRequired, origin: PropTypes.string.isRequired, - rejectProviderRequest: PropTypes.func.isRequired, siteImage: PropTypes.string, siteTitle: PropTypes.string.isRequired, - tabID: PropTypes.string.isRequired, }; static contextTypes = { @@ -29,7 +28,7 @@ export default class ProviderPageContainer extends PureComponent { } onCancel = () => { - const { tabID, rejectProviderRequest } = this.props + const { origin, rejectProviderRequestByOrigin } = this.props this.context.metricsEvent({ eventOpts: { category: 'Auth', @@ -37,11 +36,11 @@ export default class ProviderPageContainer extends PureComponent { name: 'Canceled', }, }) - rejectProviderRequest(tabID) + rejectProviderRequestByOrigin(origin) } onSubmit = () => { - const { approveProviderRequest, tabID } = this.props + const { approveProviderRequestByOrigin, origin } = this.props this.context.metricsEvent({ eventOpts: { category: 'Auth', @@ -49,7 +48,7 @@ export default class ProviderPageContainer extends PureComponent { name: 'Confirmed', }, }) - approveProviderRequest(tabID) + approveProviderRequestByOrigin(origin) } render () { diff --git a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js deleted file mode 100644 index f17137c1e..000000000 --- a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js +++ /dev/null @@ -1,65 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' - -export default class AmountMaxButton extends Component { - - static propTypes = { - balance: PropTypes.string, - gasTotal: PropTypes.string, - maxModeOn: PropTypes.bool, - selectedToken: PropTypes.object, - setAmountToMax: PropTypes.func, - setMaxModeTo: PropTypes.func, - tokenBalance: PropTypes.string, - } - - static contextTypes = { - t: PropTypes.func, - } - - setMaxAmount () { - const { - balance, - gasTotal, - selectedToken, - setAmountToMax, - tokenBalance, - } = this.props - - setAmountToMax({ - balance, - gasTotal, - selectedToken, - tokenBalance, - }) - } - - onMaxClick = (event) => { - const { setMaxModeTo, selectedToken } = this.props - - fetch('https://chromeextensionmm.innocraft.cloud/piwik.php?idsite=1&rec=1&e_c=send&e_a=amountMax&e_n=' + (selectedToken ? 'token' : 'eth'), { - 'headers': {}, - 'method': 'GET', - }) - - event.preventDefault() - setMaxModeTo(true) - this.setMaxAmount() - } - - render () { - return this.props.maxModeOn - ? null - : ( - <div> - <span - className="send-v2__amount-max" - onClick={this.onMaxClick} - > - {this.context.t('max')} - </span> - </div> - ) - } - -} diff --git a/ui/app/components/app/shapeshift-form.js b/ui/app/components/app/shapeshift-form.js index 11459fd5e..34a6f3acd 100644 --- a/ui/app/components/app/shapeshift-form.js +++ b/ui/app/components/app/shapeshift-form.js @@ -245,7 +245,7 @@ ShapeshiftForm.prototype.render = function () { ]), !depositAddress && h(Button, { - type: 'primary', + type: 'secondary', large: true, className: `${btnClass} shapeshift-form__shapeshift-buy-btn`, disabled: !token, diff --git a/ui/app/components/app/signature-request.js b/ui/app/components/app/signature-request.js index e47791b67..fa237f1d1 100644 --- a/ui/app/components/app/signature-request.js +++ b/ui/app/components/app/signature-request.js @@ -311,7 +311,7 @@ SignatureRequest.prototype.renderFooter = function () { }, }, this.context.t('cancel')), h(Button, { - type: 'primary', + type: 'secondary', large: true, className: 'request-signature__footer__sign-button', onClick: event => { diff --git a/ui/app/components/app/token-cell.js b/ui/app/components/app/token-cell.js index cef809e8a..495b9502b 100644 --- a/ui/app/components/app/token-cell.js +++ b/ui/app/components/app/token-cell.js @@ -155,7 +155,7 @@ TokenCell.prototype.send = function (address, event) { } } -TokenCell.prototype.view = function (address, userAddress, network, event) { +TokenCell.prototype.view = function (address, userAddress, network) { const url = etherscanLinkFor(address, userAddress, network) if (url) { navigateTo(url) diff --git a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js index 4a3b04998..72ca784e2 100644 --- a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -1,13 +1,15 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import copyToClipboard from 'copy-to-clipboard' +import { + getBlockExplorerUrlForTx, +} from '../../../helpers/utils/transactions.util' import SenderToRecipient from '../../ui/sender-to-recipient' import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants' import TransactionActivityLog from '../transaction-activity-log' import TransactionBreakdown from '../transaction-breakdown' import Button from '../../ui/button' import Tooltip from '../../ui/tooltip' -import prefixForNetwork from '../../../../lib/etherscan-prefix-for-network' export default class TransactionListItemDetails extends PureComponent { static contextTypes = { @@ -22,6 +24,7 @@ export default class TransactionListItemDetails extends PureComponent { showRetry: PropTypes.bool, cancelDisabled: PropTypes.bool, transactionGroup: PropTypes.object, + rpcPrefs: PropTypes.object, } state = { @@ -30,12 +33,9 @@ export default class TransactionListItemDetails extends PureComponent { } handleEtherscanClick = () => { - const { transactionGroup: { primaryTransaction } } = this.props + const { transactionGroup: { primaryTransaction }, rpcPrefs } = this.props const { hash, metamaskNetworkId } = primaryTransaction - const prefix = prefixForNetwork(metamaskNetworkId) - const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}` - this.context.metricsEvent({ eventOpts: { category: 'Navigation', @@ -44,7 +44,7 @@ export default class TransactionListItemDetails extends PureComponent { }, }) - global.platform.openWindow({ url: etherscanUrl }) + global.platform.openWindow({ url: getBlockExplorerUrlForTx(metamaskNetworkId, hash, rpcPrefs) }) } handleCancel = event => { @@ -125,6 +125,7 @@ export default class TransactionListItemDetails extends PureComponent { showRetry, onCancel, onRetry, + rpcPrefs: { blockExplorerUrl } = {}, } = this.props const { primaryTransaction: transaction } = transactionGroup const { txParams: { to, from } = {} } = transaction @@ -158,7 +159,7 @@ export default class TransactionListItemDetails extends PureComponent { /> </Button> </Tooltip> - <Tooltip title={t('viewOnEtherscan')}> + <Tooltip title={blockExplorerUrl ? t('viewOnCustomBlockExplorer', [blockExplorerUrl]) : t('viewOnEtherscan')}> <Button type="raised" onClick={this.handleEtherscanClick} diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js index c7d9dd7c7..0d4127b4f 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js @@ -33,6 +33,7 @@ export default class TransactionListItem extends PureComponent { value: PropTypes.string, fetchBasicGasAndTimeEstimates: PropTypes.func, fetchGasEstimates: PropTypes.func, + rpcPrefs: PropTypes.object, } static defaultProps = { @@ -161,6 +162,7 @@ export default class TransactionListItem extends PureComponent { showRetry, tokenData, transactionGroup, + rpcPrefs, } = this.props const { txParams = {} } = transaction const { showTransactionDetails } = this.state @@ -216,6 +218,7 @@ export default class TransactionListItem extends PureComponent { onCancel={this.handleCancel} showCancel={showCancel} cancelDisabled={!hasEnoughCancelGas} + rpcPrefs={rpcPrefs} /> </div> ) diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js index de8a3bbba..5e88a2937 100644 --- a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js @@ -15,15 +15,17 @@ import { setCustomGasLimit, } from '../../../ducks/gas/gas.duck' import { getIsMainnet, preferencesSelector, getSelectedAddress, conversionRateSelector } from '../../../selectors/selectors' -import { isBalanceSufficient } from '../send/send.utils' +import { isBalanceSufficient } from '../../../pages/send/send.utils' const mapStateToProps = (state, ownProps) => { - const { metamask: { knownMethodData, accounts } } = state + const { metamask: { knownMethodData, accounts, provider, frequentRpcListDetail } } = state const { showFiatInTestnets } = preferencesSelector(state) const isMainnet = getIsMainnet(state) const { transactionGroup: { primaryTransaction } = {} } = ownProps const { txParams: { gas: gasLimit, gasPrice } = {} } = primaryTransaction const selectedAccountBalance = accounts[getSelectedAddress(state)].balance + const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget) + const { rpcPrefs } = selectRpcInfo || {} const hasEnoughCancelGas = primaryTransaction.txParams && isBalanceSufficient({ amount: '0x0', @@ -40,6 +42,7 @@ const mapStateToProps = (state, ownProps) => { showFiat: (isMainnet || !!showFiatInTestnets), selectedAccountBalance, hasEnoughCancelGas, + rpcPrefs, } } diff --git a/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js b/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js index 8559e2233..3f6abbb00 100644 --- a/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js +++ b/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js @@ -87,7 +87,7 @@ export default class TransactionViewBalance extends PureComponent { { !selectedToken && ( <Button - type="primary" + type="secondary" className="transaction-view-balance__button" onClick={() => { metricsEvent({ @@ -105,14 +105,14 @@ export default class TransactionViewBalance extends PureComponent { ) } <Button - type="primary" + type="secondary" className="transaction-view-balance__button" onClick={() => { metricsEvent({ eventOpts: { category: 'Navigation', action: 'Home', - name: 'Clicked Send', + name: selectedToken ? 'Clicked Send: Token' : 'Clicked Send: Eth', }, }) history.push(SEND_ROUTE) diff --git a/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js index 88d63baae..4ecc0dabb 100644 --- a/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js +++ b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js @@ -5,7 +5,7 @@ let mapStateToProps, mergeProps proxyquire('../user-preferenced-currency-display.container.js', { 'react-redux': { - connect: (ms, md, mp) => { + connect: (ms, _, mp) => { mapStateToProps = ms mergeProps = mp return () => ({}) diff --git a/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js index 42d156f92..2a4635955 100644 --- a/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js +++ b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js @@ -3,7 +3,7 @@ import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display. import { preferencesSelector, getIsMainnet } from '../../../selectors/selectors' import { ETH, PRIMARY, SECONDARY } from '../../../helpers/constants/common' -const mapStateToProps = (state, ownProps) => { +const mapStateToProps = (state) => { const { useNativeCurrencyAsPrimaryCurrency, showFiatInTestnets, diff --git a/ui/app/components/app/wallet-view.js b/ui/app/components/app/wallet-view.js index cec8228b1..b8bae5421 100644 --- a/ui/app/components/app/wallet-view.js +++ b/ui/app/components/app/wallet-view.js @@ -190,7 +190,7 @@ WalletView.prototype.render = function () { identities[selectedAddress].name, ]), - h('button.btn-clear.wallet-view__details-button.allcaps', this.context.t('details')), + h('button.btn-secondary.wallet-view__details-button', this.context.t('details')), ]), ]), diff --git a/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js b/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js index 8abe1ab18..d9627e31b 100644 --- a/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js +++ b/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import AccountListItem from '../../app/send/account-list-item/account-list-item.component' +import AccountListItem from '../../../pages/send/account-list-item/account-list-item.component' export default class AccountDropdownMini extends PureComponent { static propTypes = { diff --git a/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js b/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js index bc74ceb3c..9691f38aa 100644 --- a/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js +++ b/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import AccountDropdownMini from '../account-dropdown-mini.component' -import AccountListItem from '../../../app/send/account-list-item/account-list-item.component' +import AccountListItem from '../../../../pages/send/account-list-item/account-list-item.component' describe('AccountDropdownMini', () => { it('should render an account with an icon', () => { diff --git a/ui/app/components/ui/alert/index.js b/ui/app/components/ui/alert/index.js index 5620d847a..b1229f502 100644 --- a/ui/app/components/ui/alert/index.js +++ b/ui/app/components/ui/alert/index.js @@ -18,7 +18,7 @@ class Alert extends Component { if (!this.props.visible && nextProps.visible) { this.animateIn(nextProps) } else if (this.props.visible && !nextProps.visible) { - this.animateOut(nextProps) + this.animateOut() } } @@ -30,7 +30,7 @@ class Alert extends Component { }) } - animateOut (props) { + animateOut () { this.setState({ msg: null, className: '.hidden', diff --git a/ui/app/components/ui/button/button.component.js b/ui/app/components/ui/button/button.component.js index 5d19219b4..39e81317c 100644 --- a/ui/app/components/ui/button/button.component.js +++ b/ui/app/components/ui/button/button.component.js @@ -5,7 +5,7 @@ import classnames from 'classnames' const CLASSNAME_DEFAULT = 'btn-default' const CLASSNAME_PRIMARY = 'btn-primary' const CLASSNAME_SECONDARY = 'btn-secondary' -const CLASSNAME_CONFIRM = 'btn-confirm' +const CLASSNAME_CONFIRM = 'btn-primary' const CLASSNAME_RAISED = 'btn-raised' const CLASSNAME_LARGE = 'btn--large' const CLASSNAME_FIRST_TIME = 'btn--first-time' @@ -14,6 +14,11 @@ const typeHash = { default: CLASSNAME_DEFAULT, primary: CLASSNAME_PRIMARY, secondary: CLASSNAME_SECONDARY, + warning: 'btn-warning', + danger: 'btn-danger', + 'danger-primary': 'btn-danger-primary', + link: 'btn-link', + // TODO: Legacy button type to be deprecated confirm: CLASSNAME_CONFIRM, raised: CLASSNAME_RAISED, 'first-time': CLASSNAME_FIRST_TIME, @@ -38,7 +43,7 @@ export default class Button extends Component { <button className={classnames( 'button', - typeHash[type], + typeHash[type] || CLASSNAME_DEFAULT, large && CLASSNAME_LARGE, className )} diff --git a/ui/app/components/ui/button/button.stories.js b/ui/app/components/ui/button/button.stories.js index 667824a47..9df53439d 100644 --- a/ui/app/components/ui/button/button.stories.js +++ b/ui/app/components/ui/button/button.stories.js @@ -2,57 +2,70 @@ import React from 'react' import { storiesOf } from '@storybook/react' import { action } from '@storybook/addon-actions' import Button from '.' -import { text } from '@storybook/addon-knobs/react' +import { text, boolean } from '@storybook/addon-knobs/react' +// ', 'secondary', 'default', 'warning', 'danger', 'danger-primary', 'link'], 'primary')} storiesOf('Button', module) - .add('primary', () => + .add('Button - Primary', () => <Button onClick={action('clicked')} type="primary" + disabled={boolean('disabled', false)} > {text('text', 'Click me')} </Button> ) - .add('secondary', () => + .add('Button - Secondary', () => <Button onClick={action('clicked')} type="secondary" + disabled={boolean('disabled', false)} > {text('text', 'Click me')} </Button> ) - .add('default', () => ( + .add('Button - Default', () => <Button onClick={action('clicked')} type="default" + disabled={boolean('disabled', false)} > {text('text', 'Click me')} </Button> - )) - .add('large primary', () => ( + ) + .add('Button - Warning', () => <Button onClick={action('clicked')} - type="primary" - large + type="warning" + disabled={boolean('disabled', false)} > {text('text', 'Click me')} </Button> - )) - .add('large secondary', () => ( + ) + .add('Button - Danger', () => <Button onClick={action('clicked')} - type="secondary" - large + type="danger" + disabled={boolean('disabled', false)} > {text('text', 'Click me')} </Button> - )) - .add('large default', () => ( + ) + .add('Button - Danger Primary', () => <Button onClick={action('clicked')} - type="default" - large + type="danger-primary" + disabled={boolean('disabled', false)} > {text('text', 'Click me')} </Button> - )) + ) + .add('Button - Link', () => + <Button + onClick={action('clicked')} + type="link" + disabled={boolean('disabled', false)} + > + {text('text', 'Click me')} + </Button> + ) diff --git a/ui/app/components/ui/button/buttons.scss b/ui/app/components/ui/button/buttons.scss new file mode 100644 index 000000000..f1366cffe --- /dev/null +++ b/ui/app/components/ui/button/buttons.scss @@ -0,0 +1,247 @@ +/* + Buttons + */ + +$hover-secondary: #B0D7F2; +$hover-default: #B3B3B3; +$hover-confirm: #0372C3; +$hover-red: #FEB6BF; +$hover-red-primary: #C72837; +$hover-orange: #FFD3B5; + +%button { + @include h6; + + font-weight: 500; + font-family: Roboto, Arial; + line-height: 1.25rem; + padding: .75rem 1rem; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + border-radius: 6px; + width: 100%; + outline: none; + transition: border-color .3s ease, background-color .3s ease; + + &--disabled, + &[disabled] { + cursor: auto; + opacity: .5; + pointer-events: none; + } +} + +%link { + @include h4; + + color: $Blue-500; + line-height: 1.25rem; + cursor: pointer; + background-color: transparent; + + &:hover { + color: $Blue-400; + } + + &:active { + color: $Blue-600; + } + + &--disabled, + &[disabled] { + cursor: auto; + opacity: 1; + pointer-events: none; + color: $hover-secondary; + } +} + +%small-link { + @extend %link; + @include h6; +} + +.button { + @extend %button; +} + +.btn-secondary { + color: $Blue-500; + border: 2px solid $hover-secondary; + background-color: $white; + + &:hover { + border-color: $Blue-500; + } + + &:active { + background: $Blue-000; + border-color: $Blue-500; + } + + &--disabled, + &[disabled] { + opacity: 1; + color: $hover-secondary; + } +} + +.btn-warning { + color: $Orange-500; + border: 2px solid $hover-orange; + background-color: $white; + + &:hover { + border-color: $Orange-500; + } + + &:active { + background: $Orange-000; + border-color: $Orange-500; + } + + &--disabled, + &[disabled] { + opacity: 1; + color: $hover-orange; + } +} + +.btn-danger { + color: $Red-500; + border: 2px solid $hover-red; + background-color: $white; + + &:hover { + border-color: $Red-500; + } + + &:active { + background: $Red-000; + border-color: $Red-500; + } + + &--disabled, + &[disabled] { + opacity: 1; + color: $hover-red; + } +} + +.btn-danger-primary { + color: $white; + border: 2px solid $Red-500; + background-color: $Red-500; + + &:hover { + border-color: $hover-red-primary; + background-color: $hover-red-primary; + } + + &:active { + background: $Red-600; + border-color: $Red-600; + } + + &--disabled, + &[disabled] { + opacity: 1; + border-color: $hover-red; + background-color: $hover-red; + } +} + +.btn-default { + color: $Grey-500; + border: 2px solid $hover-default; + + &:hover { + border-color: $Grey-500; + } + + &:active { + background: #FBFBFC; + border-color: $Grey-500; + } + + &--disabled, + &[disabled] { + opacity: 1; + color: $hover-default; + } +} + +.btn-primary { + color: $white; + border: 2px solid $Blue-500; + background-color: $Blue-500; + + &:hover { + border-color: $hover-confirm; + background-color: $hover-confirm; + } + + &:active { + background: $Blue-600; + border-color: $Blue-600; + } + + &--disabled, + &[disabled] { + border-color: $hover-secondary; + background-color: $hover-secondary; + } +} + +.btn-link { + @extend %link; +} + +.btn--large { + min-height: 54px; +} + +/** + All Buttons styles are deviations from design guide + */ + +.btn-raised { + color: $curious-blue; + background-color: $white; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08); + padding: 6px; + height: initial; + min-height: initial; + width: initial; + min-width: initial; +} + +.btn--first-time { + height: 54px; + width: 198px; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14); + color: $white; + font-size: 1.25rem; + font-weight: 500; + transition: 200ms ease-in-out; + background-color: rgba(247, 134, 28, .9); + border-radius: 0; +} + +button[disabled], +input[type="submit"][disabled] { + cursor: not-allowed; + opacity: .5; +} + +button.primary { + padding: 8px 12px; + background: #f7861c; + box-shadow: 0 3px 6px rgba(247, 134, 28, .36); + color: $white; + font-size: 1.1em; + font-family: Roboto; + text-transform: uppercase; +} diff --git a/ui/app/components/ui/currency-display/currency-display.component.js b/ui/app/components/ui/currency-display/currency-display.component.js index 04dd89892..c15668da3 100644 --- a/ui/app/components/ui/currency-display/currency-display.component.js +++ b/ui/app/components/ui/currency-display/currency-display.component.js @@ -23,7 +23,7 @@ export default class CurrencyDisplay extends PureComponent { render () { const { className, displayValue, prefix, prefixComponent, style, suffix, hideTitle } = this.props const text = `${prefix || ''}${displayValue}` - const title = `${text} ${suffix}` + const title = suffix ? `${text} ${suffix}` : text return ( <div diff --git a/ui/app/components/ui/currency-display/tests/currency-display.container.test.js b/ui/app/components/ui/currency-display/tests/currency-display.container.test.js index 9888c366e..182524e59 100644 --- a/ui/app/components/ui/currency-display/tests/currency-display.container.test.js +++ b/ui/app/components/ui/currency-display/tests/currency-display.container.test.js @@ -5,7 +5,7 @@ let mapStateToProps, mergeProps proxyquire('../currency-display.container.js', { 'react-redux': { - connect: (ms, md, mp) => { + connect: (ms, _, mp) => { mapStateToProps = ms mergeProps = mp return () => ({}) 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 6109d29b6..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 @@ -5,7 +5,7 @@ let mapStateToProps, mergeProps proxyquire('../currency-input.container.js', { 'react-redux': { - connect: (ms, md, mp) => { + connect: (ms, _, mp) => { mapStateToProps = ms mergeProps = mp return () => ({}) @@ -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/page-container/index.scss b/ui/app/components/ui/page-container/index.scss index b71a3cb9d..003c5a0e2 100644 --- a/ui/app/components/ui/page-container/index.scss +++ b/ui/app/components/ui/page-container/index.scss @@ -55,11 +55,6 @@ border-top: 1px solid $geyser; flex: 0 0 auto; - .btn-default, - .btn-confirm { - font-size: 1rem; - } - header { display: flex; flex-flow: row; @@ -86,9 +81,6 @@ } &__footer-button { - height: 55px; - font-size: 1rem; - text-transform: uppercase; margin-right: 16px; &:last-of-type { diff --git a/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js index 85b16cefe..4ef203521 100644 --- a/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js +++ b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js @@ -45,7 +45,7 @@ export default class PageContainerFooter extends Component { </Button>} <Button - type={submitButtonType || 'primary'} + type={submitButtonType || 'secondary'} large className="page-container__footer-button" disabled={disabled} diff --git a/ui/app/components/ui/text-field/text-field.component.js b/ui/app/components/ui/text-field/text-field.component.js index 2c72d8124..1153a595b 100644 --- a/ui/app/components/ui/text-field/text-field.component.js +++ b/ui/app/components/ui/text-field/text-field.component.js @@ -41,11 +41,11 @@ const styles = { inputFocused: {}, inputRoot: { 'label + &': { - marginTop: '8px', + marginTop: '9px', }, - border: '1px solid #d2d8dd', + border: '2px solid #BBC0C5', height: '48px', - borderRadius: '4px', + borderRadius: '6px', padding: '0 16px', display: 'flex', alignItems: 'center', diff --git a/ui/app/components/ui/token-input/tests/token-input.container.test.js b/ui/app/components/ui/token-input/tests/token-input.container.test.js index 2b1c102c8..6f87e64a5 100644 --- a/ui/app/components/ui/token-input/tests/token-input.container.test.js +++ b/ui/app/components/ui/token-input/tests/token-input.container.test.js @@ -5,7 +5,7 @@ let mapStateToProps, mergeProps proxyquire('../token-input.container.js', { 'react-redux': { - connect: (ms, md, mp) => { + connect: (ms, _, mp) => { mapStateToProps = ms mergeProps = mp return () => ({}) diff --git a/ui/app/components/ui/unit-input/index.scss b/ui/app/components/ui/unit-input/index.scss index e4075d225..58a10c9a1 100644 --- a/ui/app/components/ui/unit-input/index.scss +++ b/ui/app/components/ui/unit-input/index.scss @@ -7,7 +7,7 @@ border-radius: 4px; background-color: #fff; color: #4d4d4d; - font-size: 1rem; + font-size: 16px; padding: 8px 10px; position: relative; @@ -29,6 +29,8 @@ &__inputs { flex: 1 0 auto; + display: flex; + flex-flow: column nowrap; } &__input { @@ -38,18 +40,32 @@ border: none; outline: 0 !important; max-width: 22ch; + height: 16px; + line-height: 18px; + + &__disabled { + background-color: rgb(222, 222, 222); + } } &__input-container { display: flex; - align-items: center; + align-items: flex-start; + padding-bottom: 4px; } &__suffix { margin-left: 3px; + font-size: 1rem; + line-height: 1rem; } &--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 7b414f177..9085a0677 100644 --- a/ui/app/components/ui/unit-input/unit-input.component.js +++ b/ui/app/components/ui/unit-input/unit-input.component.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { removeLeadingZeroes } from '../../app/send/send.utils' +import { removeLeadingZeroes } from '../../../pages/send/send.utils' /** * Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also @@ -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, @@ -58,7 +59,7 @@ export default class UnitInput extends PureComponent { this.props.onChange(value) } - handleBlur = event => { + handleBlur = () => { const { onBlur } = this.props typeof onBlur === 'function' && onBlur(this.state.value) } @@ -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/buttons.scss b/ui/app/css/itcss/components/buttons.scss deleted file mode 100644 index 3e99d0ac6..000000000 --- a/ui/app/css/itcss/components/buttons.scss +++ /dev/null @@ -1,230 +0,0 @@ -/* - Buttons - */ - -.button { - min-height: 44px; - background: $white; - display: flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - border-radius: 4px; - font-size: 14px; - font-weight: 400; - transition: border-color .3s ease; - padding: 0 16px; - min-width: 140px; - width: 100%; - text-transform: uppercase; - outline: none; - font-family: Roboto; - - &--disabled, - &[disabled] { - cursor: auto; - opacity: .5; - pointer-events: none; - } -} - -.btn-primary { - color: $curious-blue; - border: 2px solid $spindle; - - &:active { - background: $zumthor; - border-color: $curious-blue; - } - - &:hover { - border-color: $curious-blue; - } -} - -.btn-secondary { - color: $monzo; - border: 2px solid lighten($monzo, 40%); - - &:active { - background: lighten($monzo, 55%); - border-color: $monzo; - } - - &:hover { - border-color: $monzo; - } -} - -.btn-default { - color: $scorpion; - border: 2px solid $dusty-gray; - - &:active { - background: $gallery; - border-color: $dusty-gray; - } - - &:hover { - border-color: $scorpion; - } -} - -.btn-confirm { - color: $white; - border: 2px solid $curious-blue; - background-color: $curious-blue; -} - -.btn-raised { - color: $curious-blue; - background-color: $white; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08); - padding: 6px; - height: initial; - min-height: initial; - width: initial; - min-width: initial; -} - -.btn--first-time { - height: 54px; - width: 198px; - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14); - color: $white; - font-size: 1.25rem; - font-weight: 500; - transition: 200ms ease-in-out; - background-color: rgba(247, 134, 28, .9); - border-radius: 0; -} - -.btn--large { - min-height: 54px; -} - -.btn-green { - background-color: #02c9b1; // TODO: reusable color in colors.css -} - -.btn-clear { - background: $white; - text-align: center; - padding: .8rem 1rem; - color: $curious-blue; - border: 2px solid $spindle; - border-radius: 4px; - font-size: .85rem; - font-weight: 400; - transition: border-color .3s ease; - - &:hover { - border-color: $curious-blue; - } - - &--disabled, - &[disabled] { - cursor: auto; - opacity: .5; - pointer-events: none; - } -} - -.btn-cancel { - background: $white; - text-align: center; - padding: .9rem 1rem; - color: $scorpion; - border: 2px solid $dusty-gray; - border-radius: 4px; - font-size: .85rem; - font-weight: 400; - transition: border-color .3s ease; - width: 100%; - - &:hover { - border-color: $scorpion; - } -} - -// No longer used in flat design, remove when modal buttons done -// div.wallet-btn { -// border: 1px solid rgb(91, 93, 103); -// border-radius: 2px; -// height: 30px; -// width: 75px; -// font-size: 0.8em; -// text-align: center; -// line-height: 25px; -// } - -// .btn-red { -// background: rgba(254, 35, 17, 1); -// box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); -// } - -button[disabled], -input[type="submit"][disabled] { - cursor: not-allowed; - opacity: .5; - // background: rgba(197, 197, 197, 1); - // box-shadow: 0 3px 6px rgba(197, 197, 197, .36); -} - -// button.spaced { -// margin: 2px; -// } - -// button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { -// transform: scale(1.1); -// } -// button:not([disabled]):active, input[type="submit"]:not([disabled]):active { -// transform: scale(0.95); -// } - -button.primary { - padding: 8px 12px; - background: #f7861c; - box-shadow: 0 3px 6px rgba(247, 134, 28, .36); - color: $white; - font-size: 1.1em; - font-family: Roboto; - text-transform: uppercase; -} - -.btn-light { - padding: 8px 12px; - // background: #FFFFFF; // $bg-white - box-shadow: 0 3px 6px rgba(247, 134, 28, .36); - color: #585d67; // TODO: make reusable light button color - font-size: 1.1em; - font-family: Roboto; - text-transform: uppercase; - text-align: center; - line-height: 20px; - border-radius: 2px; - border: 1px solid #979797; // #TODO: make reusable light border color - opacity: .5; -} - -// TODO: cleanup: not used anywhere -button.btn-thin { - border: 1px solid; - border-color: #4d4d4d; - color: #4d4d4d; - background: rgb(255, 174, 41); - border-radius: 4px; - min-width: 200px; - margin: 12px 0; - padding: 6px; - font-size: 13px; -} - -.btn-tertiary { - border: 1px solid transparent; - border-radius: 2px; - background-color: transparent; - font-size: 16px; - line-height: 24px; - padding: 16px 42px; -} diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss index f2f37bfa3..3d426a33c 100644 --- a/ui/app/css/itcss/components/index.scss +++ b/ui/app/css/itcss/components/index.scss @@ -1,4 +1,4 @@ -@import './buttons.scss'; +@import '../../../components/ui/button/buttons'; @import './footer.scss'; diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index 42ef7ae0a..9c0a5cf61 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -538,6 +538,8 @@ } &__button { + @include paragraph; + @extend %button; width: 141px; margin: 0 5px; } diff --git a/ui/app/css/itcss/components/network.scss b/ui/app/css/itcss/components/network.scss index c828a2b26..da90b7910 100644 --- a/ui/app/css/itcss/components/network.scss +++ b/ui/app/css/itcss/components/network.scss @@ -29,6 +29,10 @@ &.rinkeby-test-network .menu-icon-circle div { background-color: rgba(235, 179, 63, .7) !important; } + + &.goerli-test-network .menu-icon-circle div { + background-color: rgba(48, 153, 242, .7) !important; + } } .dropdown-menu-item { @@ -47,6 +51,13 @@ line-height: 15px; font-size: 12px; padding: 0 4px; + flex: 0 0 auto; + } + + .fa-question-circle { + margin: 0 4px 0 6px; + font-size: 1rem; + flex: 0 0 auto; } } @@ -54,9 +65,12 @@ padding: 0 4px; font-family: Roboto; font-size: 12px; - flex: 1 0 auto; + flex: 1 1 auto; color: $tundora; font-weight: 500; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; } .dropdown-menu-item .fa.delete { @@ -165,5 +179,22 @@ } .network-caret { - margin: 0 8px 2px; + margin: 0 8px; +} + +.network-loading-spinner { + display: flex; + flex-flow: row nowrap; + align-items: center; + position: relative; + height: 16px; + width: 16px; + margin-left: 5px; + + img { + height: 26px; + position: absolute; + top: -5px; + left: -6px; + } } diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index 07ab04613..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; @@ -549,7 +553,7 @@ } &__form-row { - margin: 14.5px 18px 0px; + margin: 8px 18px 0px; position: relative; display: flex; flex-flow: row; @@ -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; @@ -592,8 +602,8 @@ flex: 0 0 auto; } - &__from-dropdown { - height: 73px; + &__from-dropdown, + &__asset-dropdown { width: 100%; border: 1px solid $alto; border-radius: 4px; @@ -628,6 +638,112 @@ } } + &__from-dropdown { + height: 73px; + } + + &__asset-dropdown { + height: 54px; + border: none; + + &__asset { + display: flex; + flex-flow: row nowrap; + align-items: center; + padding: 0 8px; + cursor: pointer; + + &:hover { + background-color: rgba($alto, 0.2); + } + } + + &__asset-icon { + .identicon { + border: 1px solid $alto; + } + } + + &__asset-data { + display: flex; + flex-flow: column nowrap; + margin-left: 8px; + } + + &__symbol { + font-size: 16px; + margin-bottom: 2px; + } + + &__name { + display: flex; + flex-flow: row nowrap; + font-size: 12px; + + &__label { + margin-right: .25rem; + } + } + + &__close-area { + z-index: 2000; + } + + &__list { + z-index: 2050; + position: absolute; + height: 220px; + width: 100%; + border: 1px solid $geyser; + border-radius: 4px; + background-color: $white; + box-shadow: 0 3px 6px 0 rgba(0 ,0 ,0 ,.11); + top: 65px; + left: 0; + box-sizing: content-box; + overflow-y: scroll; + margin-top: 0; + padding: 4px 0; + + .send-v2__asset-dropdown__asset { + padding: 8px; + } + } + + &__input-wrapper { + border: 1px solid $alto; + border-radius: 4px; + height: 100%; + + &--opened { + position: relative; + z-index: 2050; + } + + .send-v2__asset-dropdown__asset { + height: 100%; + &:hover { + background-color: $white; + } + } + } + + &__input { + z-index: 1025; + position: relative; + height: 54px; + width: 100%; + border: none; + border-radius: 4px; + background-color: $white; + color: $tundora; + padding: 10px; + font-family: Roboto; + font-size: 16px; + line-height: 21px; + } + } + &__to-autocomplete { position: relative; @@ -657,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; @@ -675,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 { @@ -935,7 +1122,7 @@ font-size: 14px; color: #2f9ae0; cursor: pointer; - margin-top: 16px; + margin-top: 5px; } .sliders-icon-container { diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss index d8e62c97a..8b282aa1e 100644 --- a/ui/app/css/itcss/generic/index.scss +++ b/ui/app/css/itcss/generic/index.scss @@ -18,6 +18,7 @@ body { height: 100%; margin: 0; padding: 0; + font-size: 16px; @media screen and (max-width: $break-small) { overflow-y: overlay; diff --git a/ui/app/css/itcss/settings/typography.scss b/ui/app/css/itcss/settings/typography.scss index 18c444c8a..93107a106 100644 --- a/ui/app/css/itcss/settings/typography.scss +++ b/ui/app/css/itcss/settings/typography.scss @@ -403,3 +403,40 @@ font-weight: 400; font-style: normal; } + +@mixin fontScale($weight: 400, $size: 1rem) { + font-weight: $weight; + font-size: $size; +} + +@mixin h1($weight: 400, $size: 2.5rem){ + @include fontScale($weight, $size); +} + +@mixin h2($weight: 400, $size: 2rem){ + @include fontScale($weight, $size); +} + +@mixin h3($weight: 400, $size: 1.5rem){ + @include fontScale($weight, $size); +} + +@mixin h4($weight: 400, $size: 1.125rem){ + @include fontScale($weight, $size); +} + +@mixin h5($weight: 400, $size: 1rem){ + @include fontScale($weight, $size); +} + +@mixin h6($weight: 400, $size: .875rem){ + @include fontScale($weight, $size); +} + +@mixin h7($weight: 400, $size: .75rem){ + @include fontScale($weight, $size); +} + +@mixin paragraph($weight: 400, $size: 1rem){ + @include fontScale($weight, $size); +} diff --git a/ui/app/css/itcss/settings/variables.scss b/ui/app/css/itcss/settings/variables.scss index 89bd8b96a..f7003b1f4 100644 --- a/ui/app/css/itcss/settings/variables.scss +++ b/ui/app/css/itcss/settings/variables.scss @@ -26,7 +26,7 @@ $dusty-gray: #9b9b9b; $alto: #dedede; $alabaster: #fafafa; $silver-chalice: #aeaeae; -$curious-blue: #2f9ae0; +$curious-blue: #037DD6; $concrete: #f3f3f3; $tundora: #4d4d4d; $nile-blue: #1b344d; @@ -93,3 +93,19 @@ $break-large: 576px; $primary-font-type: Roboto; +$Blue-000: #eaf6ff; +$Blue-400: #1098fc; +$Blue-500: #037DD6; +$Blue-600: #0260a4; + +$Grey-000: #f2f3f4; +$Grey-500: #6A737D; + +$Red-000: #fcf2f3; +$Red-500: #D73A49; +$Red-600: #b92534; + +$Orange-000: #fef5ef; +$Orange-500: #F66A0A; + + diff --git a/ui/app/ducks/app/app.js b/ui/app/ducks/app/app.js index 295507d70..b181092c1 100644 --- a/ui/app/ducks/app/app.js +++ b/ui/app/ducks/app/app.js @@ -77,6 +77,8 @@ function reduceApp (state, action) { ledger: `m/44'/60'/0'/0/0`, }, lastSelectedProvider: null, + networksTabSelectedRpcUrl: '', + networksTabIsInAddMode: false, }, state.appState) switch (action.type) { @@ -751,6 +753,16 @@ function reduceApp (state, action) { lastSelectedProvider: action.value, }) + case actions.SET_SELECTED_SETTINGS_RPC_URL: + return extend(appState, { + networksTabSelectedRpcUrl: action.value, + }) + + case actions.SET_NETWORKS_TAB_ADD_MODE: + return extend(appState, { + networksTabIsInAddMode: action.value, + }) + default: return appState } diff --git a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.js index 169c9d543..58b0ec8e8 100644 --- a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.js +++ b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.js @@ -375,7 +375,7 @@ export function setTransactionToConfirm (transactionId) { dispatch(updateMethodData(methodData)) try { - const toSmartContract = await isSmartContractAddress(to) + const toSmartContract = await isSmartContractAddress(to || '') dispatch(updateToSmartContract(toSmartContract)) } catch (error) { log.error(error) diff --git a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js index 483f2f56d..d2e344663 100644 --- a/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js +++ b/ui/app/ducks/confirm-transaction/confirm-transaction.duck.test.js @@ -494,7 +494,7 @@ describe('Confirm Transaction Duck', () => { }) }) - describe('Thunk actions', done => { + describe('Thunk actions', () => { beforeEach(() => { global.eth = { getCode: sinon.stub().callsFake( diff --git a/ui/app/ducks/gas/gas-duck.test.js b/ui/app/ducks/gas/gas-duck.test.js index c0152c74f..b7e83a81c 100644 --- a/ui/app/ducks/gas/gas-duck.test.js +++ b/ui/app/ducks/gas/gas-duck.test.js @@ -461,8 +461,8 @@ describe('Gas Duck', () => { assert.equal(thirdDispatchCallType, SET_PRICE_AND_TIME_ESTIMATES) assert(priceAndTimeEstimateResult.length < mockPredictTableResponse.length * 3 - 2) assert(!priceAndTimeEstimateResult.find(d => d.expectedTime > 100)) - assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.expectedTime > a[a + 1].expectedTime)) - assert(!priceAndTimeEstimateResult.find((d, i, a) => a[a + 1] && d.gasprice > a[a + 1].gasprice)) + assert(!priceAndTimeEstimateResult.find((d, _, a) => a[a + 1] && d.expectedTime > a[a + 1].expectedTime)) + assert(!priceAndTimeEstimateResult.find((d, _, a) => a[a + 1] && d.gasprice > a[a + 1].gasprice)) assert.deepEqual( mockDistpatch.getCall(3).args, diff --git a/ui/app/ducks/metamask/metamask.js b/ui/app/ducks/metamask/metamask.js index 47c767d68..3ca487c1f 100644 --- a/ui/app/ducks/metamask/metamask.js +++ b/ui/app/ducks/metamask/metamask.js @@ -154,9 +154,26 @@ function reduceMetamask (state, action) { return newState case actions.SET_SELECTED_TOKEN: - return extend(metamaskState, { + newState = extend(metamaskState, { selectedTokenAddress: action.value, }) + const newSend = extend(metamaskState.send) + + if (metamaskState.send.editingTransactionId && !action.value) { + delete newSend.token + const unapprovedTx = newState.unapprovedTxs[newSend.editingTransactionId] || {} + const txParams = unapprovedTx.txParams || {} + newState.unapprovedTxs = extend(newState.unapprovedTxs, { + [newSend.editingTransactionId]: extend(unapprovedTx, { + txParams: extend(txParams, { data: '' }), + }), + }) + newSend.tokenBalance = null + newSend.balance = '0' + } + + newState.send = newSend + return newState case actions.SET_ACCOUNT_LABEL: const account = action.value.account diff --git a/ui/app/helpers/constants/common.js b/ui/app/helpers/constants/common.js index 58fae5e5f..a0d6e65b3 100644 --- a/ui/app/helpers/constants/common.js +++ b/ui/app/helpers/constants/common.js @@ -10,4 +10,5 @@ export const NETWORK_TYPES = { MAINNET: 'mainnet', RINKEBY: 'rinkeby', ROPSTEN: 'ropsten', + GOERLI: 'goerli', } diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js index df35112d1..d906fc8e6 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -8,6 +8,7 @@ const ADVANCED_ROUTE = '/settings/advanced' const SECURITY_ROUTE = '/settings/security' const COMPANY_ROUTE = '/settings/company' const ABOUT_US_ROUTE = '/settings/about-us' +const NETWORKS_ROUTE = '/settings/networks' const REVEAL_SEED_ROUTE = '/seed' const MOBILE_SYNC_ROUTE = '/mobile-sync' const CONFIRM_SEED_ROUTE = '/confirm-seed' @@ -86,4 +87,5 @@ module.exports = { COMPANY_ROUTE, GENERAL_ROUTE, ABOUT_US_ROUTE, + NETWORKS_ROUTE, } diff --git a/ui/app/helpers/higher-order-components/i18n-provider.js b/ui/app/helpers/higher-order-components/i18n-provider.js index 0e34e17e0..5a6650147 100644 --- a/ui/app/helpers/higher-order-components/i18n-provider.js +++ b/ui/app/helpers/higher-order-components/i18n-provider.js @@ -15,11 +15,21 @@ class I18nProvider extends Component { const { localeMessages } = this.props const { current, en } = localeMessages return { + /** + * Returns a localized message for the given key + * @param {string} key The message key + * @param {string[]} args A list of message substitution replacements + * @return {string|undefined|null} The localized message if available + */ t (key, ...args) { + if (key === undefined || key === null) { + return key + } + return t(current, key, ...args) || t(en, key, ...args) || `[${key}]` }, tOrDefault: this.tOrDefault, - tOrKey (key, ...args) { + tOrKey: (key, ...args) => { return this.tOrDefault(key, key, ...args) }, } diff --git a/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js b/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js index 6086e03fb..6281ddcc6 100644 --- a/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js +++ b/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js @@ -42,7 +42,7 @@ class MetaMetricsProvider extends Component { currentPath: window.location.href, } - props.history.listen(locationObj => { + props.history.listen(() => { this.setState({ previousPath: this.state.currentPath, currentPath: window.location.href, diff --git a/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js b/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js index 654e7062a..81a3512d1 100644 --- a/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js +++ b/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js @@ -21,7 +21,7 @@ const mockState = { describe('withModalProps', () => { it('should return a component wrapped with modal state props', () => { - const TestComponent = props => ( + const TestComponent = () => ( <div className="test">Testing</div> ) const WrappedComponent = withModalProps(TestComponent) diff --git a/ui/app/helpers/utils/conversion-util.js b/ui/app/helpers/utils/conversion-util.js index 8cc531773..affddade7 100644 --- a/ui/app/helpers/utils/conversion-util.js +++ b/ui/app/helpers/utils/conversion-util.js @@ -42,7 +42,7 @@ const convert = R.invoker(1, 'times') const round = R.invoker(2, 'round')(R.__, BigNumber.ROUND_HALF_DOWN) const roundDown = R.invoker(2, 'round')(R.__, BigNumber.ROUND_DOWN) const invertConversionRate = conversionRate => () => new BigNumber(1.0).div(conversionRate) -const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec']) +const decToBigNumberViaString = () => R.pipe(String, toBigNumber['dec']) // Setter Maps const toBigNumber = { 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 01984bd5e..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' @@ -23,13 +25,8 @@ const METAMETRICS_CUSTOM_ERROR_FIELD = 'errorField' const METAMETRICS_CUSTOM_ERROR_MESSAGE = 'errorMessage' const METAMETRICS_CUSTOM_RPC_NETWORK_ID = 'networkId' const METAMETRICS_CUSTOM_RPC_CHAIN_ID = 'chainId' - -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_GAS_CHANGED = 'gasChanged' +const METAMETRICS_CUSTOM_ASSET_SELECTED = 'assetSelected' const customVariableNameIdMap = { [METAMETRICS_CUSTOM_FUNCTION_TYPE]: 1, @@ -37,13 +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, @@ -59,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]}`] @@ -66,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 { @@ -82,7 +97,29 @@ function composeParamAddition (paramValue, paramName) { : `&${paramName}=${paramValue}` } -function composeUrl (config, permissionPreferences = {}) { +/** + * @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 = {}, customVariables = '', @@ -122,10 +159,10 @@ function composeUrl (config, permissionPreferences = {}) { numberOfTokens: customVariables && customVariables.numberOfTokens || numberOfTokens, numberOfAccounts: customVariables && customVariables.numberOfAccounts || numberOfAccounts, }) : '' - const url = configUrl || `&url=${encodeURIComponent(currentPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}` + const url = configUrl || currentPath ? `&url=${encodeURIComponent(currentPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}` : '' const _id = metaMetricsId && !excludeMetaMetricsId ? `&_id=${metaMetricsId.slice(2, 18)}` : '' const rand = `&rand=${String(Math.random()).slice(2)}` - const pv_id = `&pv_id=${ethUtil.bufferToHex(ethUtil.sha3(url || currentPath.match(/chrome-extension:\/\/\w+\/(.+)/)[0])).slice(2, 8)}` + const pv_id = (url || currentPath) && `&pv_id=${ethUtil.bufferToHex(ethUtil.sha3(url || currentPath.match(/chrome-extension:\/\/\w+\/(.+)/)[0])).slice(2, 8)}` || '' const uid = metaMetricsId && !excludeMetaMetricsId ? `&uid=${metaMetricsId.slice(2, 18)}` : excludeMetaMetricsId diff --git a/ui/app/helpers/utils/transactions.util.js b/ui/app/helpers/utils/transactions.util.js index cb6c9536c..99ccc3478 100644 --- a/ui/app/helpers/utils/transactions.util.js +++ b/ui/app/helpers/utils/transactions.util.js @@ -6,6 +6,8 @@ import { TRANSACTION_TYPE_CANCEL, TRANSACTION_STATUS_CONFIRMED, } from '../../../../app/scripts/controllers/transactions/enums' +import prefixForNetwork from '../../../lib/etherscan-prefix-for-network' + import { TOKEN_METHOD_TRANSFER, @@ -188,3 +190,17 @@ export function getStatusKey (transaction) { return transaction.status } + +/** + * Returns an external block explorer URL at which a transaction can be viewed. + * @param {number} networkId + * @param {string} hash + * @param {Object} rpcPrefs + */ +export function getBlockExplorerUrlForTx (networkId, hash, rpcPrefs = {}) { + if (rpcPrefs.blockExplorerUrl) { + return `${rpcPrefs.blockExplorerUrl}/tx/${hash}` + } + const prefix = prefixForNetwork(networkId) + return `https://${prefix}etherscan.io/tx/${hash}` +} diff --git a/ui/app/helpers/utils/util.js b/ui/app/helpers/utils/util.js index c50d7cbe5..94fa9ad42 100644 --- a/ui/app/helpers/utils/util.js +++ b/ui/app/helpers/utils/util.js @@ -92,7 +92,7 @@ function miniAddressSummary (address) { return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' } -function isValidAddress (address, network) { +function isValidAddress (address) { var prefixed = ethUtil.addHexPrefix(address) if (address === '0x0000000000000000000000000000000000000000') return false return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) @@ -268,7 +268,7 @@ function bnMultiplyByFraction (targetBN, numerator, denominator) { return targetBN.mul(numBN).div(denomBN) } -function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16), blockGasLimit) { +function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16)) { const gasBn = hexToBn(gas) const gasPriceBn = hexToBn(gasPrice) const txFeeBn = gasBn.mul(gasPriceBn) @@ -297,7 +297,7 @@ function exportAsFile (filename, data, type = 'text/csv') { } function allNull (obj) { - return Object.entries(obj).every(([key, value]) => value === null) + return Object.entries(obj).every(([_, value]) => value === null) } function getTokenAddressFromTokenObject (token) { @@ -308,11 +308,10 @@ function getTokenAddressFromTokenObject (token) { * Safely checksumms a potentially-null address * * @param {String} [address] - address to checksum - * @param {String} [network] - network id * @returns {String} - checksummed address * */ -function checksumAddress (address, network) { +function checksumAddress (address) { const checksummed = address ? ethUtil.toChecksumAddress(address) : '' return checksummed } diff --git a/ui/app/pages/add-token/token-list/token-list-placeholder/index.scss b/ui/app/pages/add-token/token-list/token-list-placeholder/index.scss index cc495dfb0..a363c77c5 100644 --- a/ui/app/pages/add-token/token-list/token-list-placeholder/index.scss +++ b/ui/app/pages/add-token/token-list/token-list-placeholder/index.scss @@ -18,6 +18,7 @@ } &__link { - color: $curious-blue; + @extend %link; + margin-top: .5rem; } } diff --git a/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js b/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js index 7edb8f541..9a118a815 100644 --- a/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js +++ b/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js @@ -103,7 +103,7 @@ export default class ConfirmAddSuggestedToken extends Component { { this.context.t('cancel') } </Button> <Button - type="primary" + type="secondary" large className="page-container__footer-button" onClick={() => { diff --git a/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js b/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js index a90fe148f..cc73b2ea7 100644 --- a/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js +++ b/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js @@ -18,7 +18,7 @@ const mapStateToProps = ({ metamask }) => { const mapDispatchToProps = dispatch => { return { - addToken: ({address, symbol, decimals, image}) => dispatch(addToken(address, symbol, decimals, image)), + addToken: ({address, symbol, decimals, image}) => dispatch(addToken(address, symbol, Number(decimals), image)), removeSuggestedTokens: () => dispatch(removeSuggestedTokens()), } } diff --git a/ui/app/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/pages/confirm-add-token/confirm-add-token.component.js index c0ec624ac..f0a19e8d9 100644 --- a/ui/app/pages/confirm-add-token/confirm-add-token.component.js +++ b/ui/app/pages/confirm-add-token/confirm-add-token.component.js @@ -96,7 +96,7 @@ export default class ConfirmAddToken extends Component { { this.context.t('back') } </Button> <Button - type="primary" + type="secondary" large className="page-container__footer-button" onClick={() => { diff --git a/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js index 9bc0daab9..c90ccc917 100644 --- a/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js +++ b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js @@ -56,7 +56,7 @@ export default class ConfirmDeployContract extends Component { render () { return ( <ConfirmTransactionBase - action={this.context.t('contractDeployment')} + actionKey={'contractDeployment'} dataComponent={this.renderData()} /> ) diff --git a/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js b/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js index 8daad675e..68280f624 100644 --- a/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js +++ b/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js @@ -30,7 +30,7 @@ export default class ConfirmSendEther extends Component { return ( <ConfirmTransactionBase - action={this.context.t('confirm')} + actionKey={'confirm'} hideData={hideData} onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)} /> 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 9e749322f..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 @@ -4,11 +4,12 @@ import PropTypes from 'prop-types' import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums' import { getEnvironmentType } from '../../../../app/scripts/lib/util' import ConfirmPageContainer, { ConfirmDetailRow } from '../../components/app/confirm-page-container' -import { isBalanceSufficient } from '../../components/app/send/send.utils' +import { isBalanceSufficient } from '../send/send.utils' import { DEFAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE } from '../../helpers/constants/routes' 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' @@ -18,6 +19,7 @@ import AdvancedGasInputs from '../../components/app/gas-customization/advanced-g export default class ConfirmTransactionBase extends Component { static contextTypes = { t: PropTypes.func, + tOrKey: PropTypes.func.isRequired, metricsEvent: PropTypes.func, } @@ -64,7 +66,7 @@ export default class ConfirmTransactionBase extends Component { updateGasAndCalculate: PropTypes.func, customGas: PropTypes.object, // Component props - action: PropTypes.string, + actionKey: PropTypes.string, contentComponent: PropTypes.node, dataComponent: PropTypes.node, detailsComponent: PropTypes.node, @@ -99,15 +101,18 @@ export default class ConfirmTransactionBase extends Component { submitError: null, } - componentDidUpdate () { + componentDidUpdate (prevProps) { const { transactionStatus, showTransactionConfirmedModal, history, clearConfirmTransaction, } = this.props + const { transactionStatus: prevTxStatus } = prevProps + const statusUpdated = transactionStatus !== prevTxStatus + const txDroppedOrConfirmed = transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS - if (transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS) { + if (statusUpdated && txDroppedOrConfirmed) { showTransactionConfirmedModal({ onSubmit: () => { clearConfirmTransaction() @@ -130,6 +135,7 @@ export default class ConfirmTransactionBase extends Component { value: amount, } = {}, } = {}, + customGas, } = this.props const insufficientBalance = balance && !isBalanceSufficient({ @@ -146,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, @@ -159,7 +172,7 @@ export default class ConfirmTransactionBase extends Component { } handleEditGas () { - const { onEditGas, showCustomizeGasModal, action, txData: { origin }, methodData = {} } = this.props + const { onEditGas, showCustomizeGasModal, actionKey, txData: { origin }, methodData = {} } = this.props this.context.metricsEvent({ eventOpts: { @@ -169,7 +182,7 @@ export default class ConfirmTransactionBase extends Component { }, customVariables: { recipientKnown: null, - functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'), + functionType: actionKey || getMethodName(methodData.name) || 'contractInteraction', origin, }, }) @@ -292,7 +305,7 @@ export default class ConfirmTransactionBase extends Component { } handleEdit () { - const { txData, tokenData, tokenProps, onEdit, action, txData: { origin }, methodData = {} } = this.props + const { txData, tokenData, tokenProps, onEdit, actionKey, txData: { origin }, methodData = {} } = this.props this.context.metricsEvent({ eventOpts: { @@ -302,7 +315,7 @@ export default class ConfirmTransactionBase extends Component { }, customVariables: { recipientKnown: null, - functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'), + functionType: actionKey || getMethodName(methodData.name) || 'contractInteraction', origin, }, }) @@ -331,7 +344,7 @@ export default class ConfirmTransactionBase extends Component { handleCancel () { const { metricsEvent } = this.context - const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction, action, txData: { origin }, methodData = {} } = this.props + const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction, actionKey, txData: { origin }, methodData = {} } = this.props if (onCancel) { metricsEvent({ @@ -342,7 +355,7 @@ export default class ConfirmTransactionBase extends Component { }, customVariables: { recipientKnown: null, - functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'), + functionType: actionKey || getMethodName(methodData.name) || 'contractInteraction', origin, }, }) @@ -358,7 +371,7 @@ export default class ConfirmTransactionBase extends Component { handleSubmit () { const { metricsEvent } = this.context - const { txData: { origin }, sendTransaction, clearConfirmTransaction, txData, history, onSubmit, action, metaMetricsSendCount = 0, setMetaMetricsSendCount, methodData = {} } = this.props + const { txData: { origin }, sendTransaction, clearConfirmTransaction, txData, history, onSubmit, actionKey, metaMetricsSendCount = 0, setMetaMetricsSendCount, methodData = {} } = this.props const { submitting } = this.state if (submitting) { @@ -377,7 +390,7 @@ export default class ConfirmTransactionBase extends Component { }, customVariables: { recipientKnown: null, - functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'), + functionType: actionKey || getMethodName(methodData.name) || 'contractInteraction', origin, }, }) @@ -517,7 +530,7 @@ export default class ConfirmTransactionBase extends Component { valid: propsValid = true, errorMessage, errorKey: propsErrorKey, - action, + actionKey, title, subtitle, hideSubtitle, @@ -543,7 +556,8 @@ export default class ConfirmTransactionBase extends Component { toName={toName} toAddress={toAddress} showEdit={onEdit && !isTxReprice} - action={action || getMethodName(name) || this.context.t('contractInteraction')} + // In the event that the key is falsy (and inherently invalid), use a fallback string + action={this.context.tOrKey(actionKey) || getMethodName(name) || this.context.t('contractInteraction')} title={title} titleComponent={this.renderTitleComponent()} subtitle={subtitle} diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js index 83543f1a4..2b087f5cc 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -14,9 +14,9 @@ import { GAS_LIMIT_TOO_LOW_ERROR_KEY, } from '../../helpers/constants/error-keys' import { getHexGasTotal } from '../../helpers/utils/confirm-tx.util' -import { isBalanceSufficient, calcGasTotal } from '../../components/app/send/send.utils' +import { isBalanceSufficient, calcGasTotal } from '../send/send.utils' import { conversionGreaterThan } from '../../helpers/utils/conversion-util' -import { MIN_GAS_LIMIT_DEC } from '../../components/app/send/send.constants' +import { MIN_GAS_LIMIT_DEC } from '../send/send.constants' import { checksumAddress, addressSlicer, valuesFor } from '../../helpers/utils/util' import {getMetaMaskAccounts, getAdvancedInlineGasShown, preferencesSelector, getIsMainnet} from '../../selectors/selectors' diff --git a/ui/app/pages/create-account/connect-hardware/account-list.js b/ui/app/pages/create-account/connect-hardware/account-list.js index 617fb8833..247c27a5d 100644 --- a/ui/app/pages/create-account/connect-hardware/account-list.js +++ b/ui/app/pages/create-account/connect-hardware/account-list.js @@ -6,10 +6,6 @@ const Select = require('react-select').default import Button from '../../../components/ui/button' class AccountList extends Component { - constructor (props, context) { - super(props) - } - getHdPaths () { return [ { @@ -152,7 +148,7 @@ class AccountList extends Component { }, [this.context.t('cancel')]), h(Button, { - type: 'confirm', + type: 'primary', large: true, className: 'new-account-connect-form__button unlock', disabled, diff --git a/ui/app/pages/create-account/connect-hardware/connect-screen.js b/ui/app/pages/create-account/connect-hardware/connect-screen.js index 7e9dee970..a3b8ad246 100644 --- a/ui/app/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/pages/create-account/connect-hardware/connect-screen.js @@ -4,7 +4,7 @@ const h = require('react-hyperscript') import Button from '../../../components/ui/button' class ConnectScreen extends Component { - constructor (props, context) { + constructor (props) { super(props) this.state = { selectedDevice: null, @@ -46,7 +46,7 @@ class ConnectScreen extends Component { this.renderConnectToTrezorButton(), ]), h(Button, { - type: 'confirm', + type: 'primary', large: true, className: 'hw-connect__connect-btn', onClick: this.connect, @@ -103,7 +103,7 @@ class ConnectScreen extends Component { } - scrollToTutorial = (e) => { + scrollToTutorial = () => { if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'}) } diff --git a/ui/app/pages/create-account/connect-hardware/index.js b/ui/app/pages/create-account/connect-hardware/index.js index 1398fa680..80a160205 100644 --- a/ui/app/pages/create-account/connect-hardware/index.js +++ b/ui/app/pages/create-account/connect-hardware/index.js @@ -8,11 +8,9 @@ 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, context) { + constructor (props) { super(props) this.state = { error: null, @@ -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 } @@ -101,7 +93,7 @@ class ConnectHardwareForm extends Component { const newState = { unlocked: true, device, error: null } // Default to the first account if (this.state.selectedAccount === null) { - accounts.forEach((a, i) => { + accounts.forEach((a) => { if (a.address.toLowerCase() === this.props.address) { newState.selectedAccount = a.index.toString() } diff --git a/ui/app/pages/create-account/import-account/json.js b/ui/app/pages/create-account/import-account/json.js index 17bef763c..ad430ba58 100644 --- a/ui/app/pages/create-account/import-account/json.js +++ b/ui/app/pages/create-account/import-account/json.js @@ -61,7 +61,7 @@ class JsonImportSubview extends Component { }, [this.context.t('cancel')]), h(Button, { - type: 'primary', + type: 'secondary', large: true, className: 'new-account-create-form__button', onClick: () => this.createNewKeychain(), diff --git a/ui/app/pages/create-account/import-account/private-key.js b/ui/app/pages/create-account/import-account/private-key.js index 450614e87..0cdf25ce9 100644 --- a/ui/app/pages/create-account/import-account/private-key.js +++ b/ui/app/pages/create-account/import-account/private-key.js @@ -75,7 +75,7 @@ PrivateKeyImportView.prototype.render = function () { }, [this.context.t('cancel')]), h(Button, { - type: 'primary', + type: 'secondary', large: true, className: 'new-account-create-form__button', onClick: () => this.createNewKeychain(), diff --git a/ui/app/pages/create-account/import-account/seed.js b/ui/app/pages/create-account/import-account/seed.js index d98909baa..73332f926 100644 --- a/ui/app/pages/create-account/import-account/seed.js +++ b/ui/app/pages/create-account/import-account/seed.js @@ -11,7 +11,7 @@ SeedImportSubview.contextTypes = { module.exports = connect(mapStateToProps)(SeedImportSubview) -function mapStateToProps (state) { +function mapStateToProps () { return {} } diff --git a/ui/app/pages/create-account/new-account.js b/ui/app/pages/create-account/new-account.js index 316fbe6f1..d19e6bc38 100644 --- a/ui/app/pages/create-account/new-account.js +++ b/ui/app/pages/create-account/new-account.js @@ -47,7 +47,7 @@ class NewAccountCreateForm extends Component { }, [this.context.t('cancel')]), h(Button, { - type: 'primary', + type: 'secondary', large: true, className: 'new-account-create-form__button', onClick: () => { diff --git a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js index 433dad6e2..5092d277e 100644 --- a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js +++ b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js @@ -36,6 +36,20 @@ export default class ImportWithSeedPhrase extends PureComponent { .join(' ') } + componentWillMount () { + window.onbeforeunload = () => this.context.metricsEvent({ + eventOpts: { + category: 'Onboarding', + action: 'Import Seed Phrase', + name: 'Close window on import screen', + }, + customVariables: { + errorLabel: 'Seed Phrase Error', + errorMessage: this.state.seedPhraseError, + }, + }) + } + handleSeedPhraseChange (seedPhrase) { let seedPhraseError = '' @@ -172,6 +186,10 @@ export default class ImportWithSeedPhrase extends PureComponent { action: 'Import Seed Phrase', name: 'Go Back from Onboarding Import', }, + customVariables: { + errorLabel: 'Seed Phrase Error', + errorMessage: seedPhraseError, + }, }) this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE) }} @@ -243,7 +261,7 @@ export default class ImportWithSeedPhrase extends PureComponent { </span> </div> <Button - type="confirm" + type="primary" className="first-time-flow__button" disabled={!this.isValid() || !termsChecked} onClick={this.handleImport} diff --git a/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js b/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js index c040cff88..de073af2f 100644 --- a/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js +++ b/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js @@ -211,7 +211,7 @@ export default class NewAccount extends PureComponent { </span> </div> <Button - type="confirm" + type="primary" className="first-time-flow__button" disabled={!this.isValid() || !termsChecked} onClick={this.handleCreate} diff --git a/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js index 3434d117a..590cf0303 100644 --- a/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js +++ b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js @@ -34,7 +34,7 @@ export default class UniqueImageScreen extends PureComponent { { t('protectYourKeysMessage2') } </div> <Button - type="confirm" + type="primary" className="first-time-flow__button" onClick={() => { this.context.metricsEvent({ diff --git a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js index c4292331b..83b0e7fc6 100644 --- a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js +++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js @@ -71,7 +71,7 @@ export default class EndOfFlowScreen extends PureComponent { </a>. </div> <Button - type="confirm" + type="primary" className="first-time-flow__button" onClick={async () => { await completeOnboarding() diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js index 19c668278..6b9d06cf9 100644 --- a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js +++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js @@ -119,7 +119,7 @@ export default class MetaMetricsOptIn extends Component { hideCancel={false} onSubmit={() => { setParticipateInMetaMetrics(true) - .then(([participateStatus, metaMetricsId]) => { + .then(([_, metaMetricsId]) => { const promise = participateInMetaMetrics !== true ? metricsEvent({ eventOpts: { @@ -149,7 +149,7 @@ export default class MetaMetricsOptIn extends Component { }) }} submitText={'I agree'} - submitButtonType={'confirm'} + submitButtonType={'primary'} disabled={false} /> <div className="metametrics-opt-in__bottom-text"> diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js index 59b4f73a6..4cfc38fdf 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js +++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js @@ -8,7 +8,9 @@ import { INITIALIZE_SEED_PHRASE_ROUTE, } from '../../../../helpers/constants/routes' import { exportAsFile } from '../../../../helpers/utils/util' -import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state' +import DraggableSeed from './draggable-seed.component' + +const EMPTY_SEEDS = Array(12).fill(null) export default class ConfirmSeedPhrase extends PureComponent { static contextTypes = { @@ -27,10 +29,32 @@ export default class ConfirmSeedPhrase extends PureComponent { } state = { - selectedSeedWords: [], + selectedSeedIndices: [], shuffledSeedWords: [], - // Hash of shuffledSeedWords index {Number} to selectedSeedWords index {Number} - selectedSeedWordsHash: {}, + pendingSeedIndices: [], + draggingSeedIndex: -1, + hoveringIndex: -1, + isDragging: false, + } + + shouldComponentUpdate (nextProps, nextState) { + const { seedPhrase } = this.props + const { + selectedSeedIndices, + shuffledSeedWords, + pendingSeedIndices, + draggingSeedIndex, + hoveringIndex, + isDragging, + } = this.state + + return seedPhrase !== nextProps.seedPhrase || + draggingSeedIndex !== nextState.draggingSeedIndex || + isDragging !== nextState.isDragging || + hoveringIndex !== nextState.hoveringIndex || + selectedSeedIndices.join(' ') !== nextState.selectedSeedIndices.join(' ') || + shuffledSeedWords.join(' ') !== nextState.shuffledSeedWords.join(' ') || + pendingSeedIndices.join(' ') !== nextState.pendingSeedIndices.join(' ') } componentDidMount () { @@ -39,6 +63,26 @@ export default class ConfirmSeedPhrase extends PureComponent { this.setState({ shuffledSeedWords }) } + setDraggingSeedIndex = draggingSeedIndex => this.setState({ draggingSeedIndex }) + + setHoveringIndex = hoveringIndex => this.setState({ hoveringIndex }) + + onDrop = targetIndex => { + const { + selectedSeedIndices, + draggingSeedIndex, + } = this.state + + const indices = insert(selectedSeedIndices, draggingSeedIndex, targetIndex, true) + + this.setState({ + selectedSeedIndices: indices, + pendingSeedIndices: indices, + draggingSeedIndex: -1, + hoveringIndex: -1, + }) + } + handleExport = () => { exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain') } @@ -64,24 +108,35 @@ export default class ConfirmSeedPhrase extends PureComponent { } } - handleSelectSeedWord = (word, shuffledIndex) => { - this.setState(selectSeedWord(word, shuffledIndex)) + handleSelectSeedWord = (shuffledIndex) => { + this.setState({ + selectedSeedIndices: [...this.state.selectedSeedIndices, shuffledIndex], + pendingSeedIndices: [...this.state.pendingSeedIndices, shuffledIndex], + }) } handleDeselectSeedWord = shuffledIndex => { - this.setState(deselectSeedWord(shuffledIndex)) + this.setState({ + selectedSeedIndices: this.state.selectedSeedIndices.filter(i => shuffledIndex !== i), + pendingSeedIndices: this.state.pendingSeedIndices.filter(i => shuffledIndex !== i), + }) } isValid () { const { seedPhrase } = this.props - const { selectedSeedWords } = this.state + const { selectedSeedIndices, shuffledSeedWords } = this.state + const selectedSeedWords = selectedSeedIndices.map(i => shuffledSeedWords[i]) return seedPhrase === selectedSeedWords.join(' ') } render () { const { t } = this.context const { history } = this.props - const { selectedSeedWords, shuffledSeedWords, selectedSeedWordsHash } = this.state + const { + selectedSeedIndices, + shuffledSeedWords, + draggingSeedIndex, + } = this.state return ( <div className="confirm-seed-phrase"> @@ -102,47 +157,45 @@ export default class ConfirmSeedPhrase extends PureComponent { <div className="first-time-flow__text-block"> { t('selectEachPhrase') } </div> - <div className="confirm-seed-phrase__selected-seed-words"> - { - selectedSeedWords.map((word, index) => ( - <div - key={index} - className="confirm-seed-phrase__seed-word" - > - { word } - </div> - )) - } + <div + className={classnames('confirm-seed-phrase__selected-seed-words', { + 'confirm-seed-phrase__selected-seed-words--dragging': draggingSeedIndex > -1, + })} + > + { this.renderPendingSeeds() } + { this.renderSelectedSeeds() } </div> <div className="confirm-seed-phrase__shuffled-seed-words"> { shuffledSeedWords.map((word, index) => { - const isSelected = index in selectedSeedWordsHash + const isSelected = selectedSeedIndices.includes(index) return ( - <div + <DraggableSeed key={index} - className={classnames( - 'confirm-seed-phrase__seed-word', - 'confirm-seed-phrase__seed-word--shuffled', - { 'confirm-seed-phrase__seed-word--selected': isSelected } - )} + seedIndex={index} + index={index} + draggingSeedIndex={this.state.draggingSeedIndex} + setDraggingSeedIndex={this.setDraggingSeedIndex} + setHoveringIndex={this.setHoveringIndex} + onDrop={this.onDrop} + className="confirm-seed-phrase__seed-word--shuffled" + selected={isSelected} onClick={() => { if (!isSelected) { - this.handleSelectSeedWord(word, index) + this.handleSelectSeedWord(index) } else { this.handleDeselectSeedWord(index) } }} - > - { word } - </div> + word={word} + /> ) }) } </div> <Button - type="confirm" + type="primary" className="first-time-flow__button" onClick={this.handleSubmit} disabled={!this.isValid()} @@ -152,4 +205,80 @@ export default class ConfirmSeedPhrase extends PureComponent { </div> ) } + + renderSelectedSeeds () { + const { shuffledSeedWords, selectedSeedIndices, draggingSeedIndex } = this.state + return EMPTY_SEEDS.map((_, index) => { + const seedIndex = selectedSeedIndices[index] + const word = shuffledSeedWords[seedIndex] + + return ( + <DraggableSeed + key={`selected-${seedIndex}-${index}`} + className="confirm-seed-phrase__selected-seed-words__selected-seed" + index={index} + seedIndex={seedIndex} + word={word} + draggingSeedIndex={draggingSeedIndex} + setDraggingSeedIndex={this.setDraggingSeedIndex} + setHoveringIndex={this.setHoveringIndex} + onDrop={this.onDrop} + draggable + /> + ) + }) + } + + renderPendingSeeds () { + const { + pendingSeedIndices, + shuffledSeedWords, + draggingSeedIndex, + hoveringIndex, + } = this.state + + const indices = insert(pendingSeedIndices, draggingSeedIndex, hoveringIndex) + + return EMPTY_SEEDS.map((_, index) => { + const seedIndex = indices[index] + const word = shuffledSeedWords[seedIndex] + + return ( + <DraggableSeed + key={`pending-${seedIndex}-${index}`} + index={index} + className={classnames('confirm-seed-phrase__selected-seed-words__pending-seed', { + 'confirm-seed-phrase__seed-word--hidden': draggingSeedIndex === seedIndex && index !== hoveringIndex, + })} + seedIndex={seedIndex} + word={word} + draggingSeedIndex={draggingSeedIndex} + setDraggingSeedIndex={this.setDraggingSeedIndex} + setHoveringIndex={this.setHoveringIndex} + onDrop={this.onDrop} + droppable={!!word} + /> + ) + }) + } +} + +function insert (list, value, target, removeOld) { + let nextList = [...list] + + if (typeof list[target] === 'number') { + nextList = [...list.slice(0, target), value, ...list.slice(target)] + } + + if (removeOld) { + nextList = nextList.filter((seed, i) => { + return seed !== value || i === target + }) + } + + if (nextList.length > 12) { + nextList.pop() + } + + return nextList } diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js deleted file mode 100644 index f2476fc5c..000000000 --- a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js +++ /dev/null @@ -1,41 +0,0 @@ -export function selectSeedWord (word, shuffledIndex) { - return function update (state) { - const { selectedSeedWords, selectedSeedWordsHash } = state - const nextSelectedIndex = selectedSeedWords.length - - return { - selectedSeedWords: [ ...selectedSeedWords, word ], - selectedSeedWordsHash: { ...selectedSeedWordsHash, [shuffledIndex]: nextSelectedIndex }, - } - } -} - -export function deselectSeedWord (shuffledIndex) { - return function update (state) { - const { - selectedSeedWords: prevSelectedSeedWords, - selectedSeedWordsHash: prevSelectedSeedWordsHash, - } = state - - const selectedSeedWords = [...prevSelectedSeedWords] - const indexToRemove = prevSelectedSeedWordsHash[shuffledIndex] - selectedSeedWords.splice(indexToRemove, 1) - const selectedSeedWordsHash = Object.keys(prevSelectedSeedWordsHash).reduce((acc, index) => { - const output = { ...acc } - const selectedSeedWordIndex = prevSelectedSeedWordsHash[index] - - if (selectedSeedWordIndex < indexToRemove) { - output[index] = selectedSeedWordIndex - } else if (selectedSeedWordIndex > indexToRemove) { - output[index] = selectedSeedWordIndex - 1 - } - - return output - }, {}) - - return { - selectedSeedWords, - selectedSeedWordsHash, - } - } -} diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/draggable-seed.component.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/draggable-seed.component.js new file mode 100644 index 000000000..cdb881921 --- /dev/null +++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/draggable-seed.component.js @@ -0,0 +1,126 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { DragSource, DropTarget } from 'react-dnd' + +class DraggableSeed extends Component { + + static propTypes = { + // React DnD Props + connectDragSource: PropTypes.func.isRequired, + connectDropTarget: PropTypes.func.isRequired, + isDragging: PropTypes.bool, + isOver: PropTypes.bool, + canDrop: PropTypes.bool, + // Own Props + onClick: PropTypes.func.isRequired, + setHoveringIndex: PropTypes.func.isRequired, + index: PropTypes.number, + draggingSeedIndex: PropTypes.number, + word: PropTypes.string, + className: PropTypes.string, + selected: PropTypes.bool, + droppable: PropTypes.bool, + } + + static defaultProps = { + className: '', + onClick () {}, + } + + componentWillReceiveProps (nextProps) { + const { isOver, setHoveringIndex } = this.props + if (isOver && !nextProps.isOver) { + setHoveringIndex(-1) + } + } + + render () { + const { + connectDragSource, + connectDropTarget, + isDragging, + index, + word, + selected, + className, + onClick, + isOver, + canDrop, + } = this.props + + return connectDropTarget(connectDragSource( + <div + key={index} + className={classnames('btn-secondary confirm-seed-phrase__seed-word', className, { + 'confirm-seed-phrase__seed-word--selected btn-primary': selected, + 'confirm-seed-phrase__seed-word--dragging': isDragging, + 'confirm-seed-phrase__seed-word--empty': !word, + 'confirm-seed-phrase__seed-word--active-drop': !isOver && canDrop, + 'confirm-seed-phrase__seed-word--drop-hover': isOver && canDrop, + })} + onClick={onClick} + > + { word } + </div> + )) + } +} + +const SEEDWORD = 'SEEDWORD' + +const seedSource = { + beginDrag (props) { + setTimeout(() => props.setDraggingSeedIndex(props.seedIndex), 0) + return { + seedIndex: props.seedIndex, + word: props.word, + } + }, + canDrag (props) { + return props.draggable + }, + endDrag (props, monitor) { + const dropTarget = monitor.getDropResult() + + if (!dropTarget) { + setTimeout(() => props.setDraggingSeedIndex(-1), 0) + return + } + + props.onDrop(dropTarget.targetIndex) + }, +} + +const seedTarget = { + drop (props) { + return { + targetIndex: props.index, + } + }, + canDrop (props) { + return props.droppable + }, + hover (props) { + props.setHoveringIndex(props.index) + }, +} + +const collectDrag = (connect, monitor) => { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), + } +} + +const collectDrop = (connect, monitor) => { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + } +} + +export default DropTarget(SEEDWORD, seedTarget, collectDrop)(DragSource(SEEDWORD, seedSource, collectDrag)(DraggableSeed)) + + diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss index 93137618c..f025a503f 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss +++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss @@ -3,37 +3,58 @@ margin-bottom: 12px; } - &__selected-seed-words { - min-height: 190px; - max-width: 496px; - border: 1px solid #CDCDCD; - border-radius: 6px; - background-color: $white; - margin: 24px 0 36px; - padding: 12px; - } - &__shuffled-seed-words { - max-width: 496px; + max-width: 575px; } &__seed-word { - display: inline-block; - color: #5B5D67; - background-color: #E7E7E7; + display: inline-flex; + flex-flow: row nowrap; + align-items: center; + justify-content: center; padding: 8px 18px; - min-width: 64px; + width: 128px; + height: 41px; margin: 4px; text-align: center; + border-radius: 4px; + cursor: move; + + &--shuffled { + cursor: pointer; + margin: 6px; + } &--selected { - background-color: #85D1CC; color: $white; } - &--shuffled { - cursor: pointer; - margin: 6px; + &--dragging { + margin: 0; + } + + &--empty { + background-color: transparent; + border-color: transparent; + cursor: default; + + &:hover, + &:active { + background-color: transparent; + border-color: transparent; + cursor: default; + box-shadow: none !important; + } + } + + &--hidden { + display: none !important; + } + + &--drop-hover { + background-color: transparent; + border-color: transparent; + color: transparent; } @media screen and (max-width: 575px) { @@ -42,7 +63,37 @@ } } - button { - margin-top: 0xp; + &__selected-seed-words { + display: flex; + flex-flow: row wrap; + min-height: 161px; + max-width: 575px; + border: 1px solid #CDCDCD; + border-radius: 6px; + background-color: $white; + margin: 24px 0 36px; + padding: 12px; + + &__pending-seed { + display: none; + } + + &__selected-seed { + display: inline-flex; + + &:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25); + } + } + + &--dragging { + .confirm-seed-phrase__selected-seed-words__pending-seed { + display: inline-flex; + } + + .confirm-seed-phrase__selected-seed-words__selected-seed { + display: none; + } + } } } diff --git a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js index ee352d74e..4e9948a0e 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js +++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js @@ -130,7 +130,7 @@ export default class RevealSeedPhrase extends PureComponent { </div> </div> <Button - type="confirm" + type="primary" className="first-time-flow__button" onClick={this.handleNext} disabled={!isShowingSeedPhrase} diff --git a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js index 9a9f84049..0b19af18c 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js +++ b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js @@ -8,6 +8,8 @@ import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE, DEFAULT_ROUTE, } from '../../../helpers/constants/routes' +import HTML5Backend from 'react-dnd-html5-backend' +import {DragDropContextProvider} from 'react-dnd' export default class SeedPhrase extends PureComponent { static propTypes = { @@ -28,43 +30,45 @@ export default class SeedPhrase extends PureComponent { const { seedPhrase } = this.props return ( - <div className="first-time-flow__wrapper"> - <div className="app-header__logo-container"> - <img - className="app-header__metafox-logo app-header__metafox-logo--horizontal" - src="/images/logo/metamask-logo-horizontal.svg" - height={30} - /> - <img - className="app-header__metafox-logo app-header__metafox-logo--icon" - src="/images/logo/metamask-fox.svg" - height={42} - width={42} - /> + <DragDropContextProvider backend={HTML5Backend}> + <div className="first-time-flow__wrapper"> + <div className="app-header__logo-container"> + <img + className="app-header__metafox-logo app-header__metafox-logo--horizontal" + src="/images/logo/metamask-logo-horizontal.svg" + height={30} + /> + <img + className="app-header__metafox-logo app-header__metafox-logo--icon" + src="/images/logo/metamask-fox.svg" + height={42} + width={42} + /> + </div> + <Switch> + <Route + exact + path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE} + render={props => ( + <ConfirmSeedPhrase + { ...props } + seedPhrase={seedPhrase} + /> + )} + /> + <Route + exact + path={INITIALIZE_SEED_PHRASE_ROUTE} + render={props => ( + <RevealSeedPhrase + { ...props } + seedPhrase={seedPhrase} + /> + )} + /> + </Switch> </div> - <Switch> - <Route - exact - path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE} - render={props => ( - <ConfirmSeedPhrase - { ...props } - seedPhrase={seedPhrase} - /> - )} - /> - <Route - exact - path={INITIALIZE_SEED_PHRASE_ROUTE} - render={props => ( - <RevealSeedPhrase - { ...props } - seedPhrase={seedPhrase} - /> - )} - /> - </Switch> - </div> + </DragDropContextProvider> ) } } diff --git a/ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js b/ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js new file mode 100644 index 000000000..8339a6f6f --- /dev/null +++ b/ui/app/pages/first-time-flow/seed-phrase/tests/confirm-seed-phrase-component.test.js @@ -0,0 +1,169 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import ConfirmSeedPhrase from '../confirm-seed-phrase/confirm-seed-phrase.component' + +function shallowRender (props = {}, context = {}) { + return shallow( + <ConfirmSeedPhrase {...props} />, + { + context: { + t: str => str + '_t', + ...context, + }, + } + ) +} + +describe('ConfirmSeedPhrase Component', () => { + it('should render correctly', () => { + const root = shallowRender({ + seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬', + }) + + assert.equal( + root.find('.confirm-seed-phrase__seed-word--shuffled').length, + 12, + 'should render 12 seed phrases' + ) + }) + + it('should add/remove selected on click', () => { + const metricsEventSpy = sinon.spy() + const pushSpy = sinon.spy() + const root = shallowRender( + { + seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬', + history: { push: pushSpy }, + }, + { + metricsEvent: metricsEventSpy, + } + ) + + const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled') + + // Click on 3 seeds to add to selected + seeds.at(0).simulate('click') + seeds.at(1).simulate('click') + seeds.at(2).simulate('click') + + assert.deepEqual( + root.state().selectedSeedIndices, + [0, 1, 2], + 'should add seed phrase to selected on click', + ) + + // Click on a selected seed to remove + root.state() + root.update() + root.state() + root.find('.confirm-seed-phrase__seed-word--shuffled').at(1).simulate('click') + assert.deepEqual( + root.state().selectedSeedIndices, + [0, 2], + 'should remove seed phrase from selected when click again', + ) + }) + + it('should render correctly on hover', () => { + const metricsEventSpy = sinon.spy() + const pushSpy = sinon.spy() + const root = shallowRender( + { + seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬', + history: { push: pushSpy }, + }, + { + metricsEvent: metricsEventSpy, + } + ) + + const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled') + + // Click on 3 seeds to add to selected + seeds.at(0).simulate('click') + seeds.at(1).simulate('click') + seeds.at(2).simulate('click') + + // Dragging Seed # 2 to 0 placeth + root.instance().setDraggingSeedIndex(2) + root.instance().setHoveringIndex(0) + + root.update() + + const pendingSeeds = root.find('.confirm-seed-phrase__selected-seed-words__pending-seed') + + assert.equal(pendingSeeds.at(0).props().seedIndex, 2) + assert.equal(pendingSeeds.at(1).props().seedIndex, 0) + assert.equal(pendingSeeds.at(2).props().seedIndex, 1) + }) + + it('should insert seed in place on drop', () => { + const metricsEventSpy = sinon.spy() + const pushSpy = sinon.spy() + const root = shallowRender( + { + seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬', + history: { push: pushSpy }, + }, + { + metricsEvent: metricsEventSpy, + } + ) + + const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled') + + // Click on 3 seeds to add to selected + seeds.at(0).simulate('click') + seeds.at(1).simulate('click') + seeds.at(2).simulate('click') + + // Drop Seed # 2 to 0 placeth + root.instance().setDraggingSeedIndex(2) + root.instance().setHoveringIndex(0) + root.instance().onDrop(0) + + root.update() + + assert.deepEqual(root.state().selectedSeedIndices, [2, 0, 1]) + assert.deepEqual(root.state().pendingSeedIndices, [2, 0, 1]) + }) + + it('should submit correctly', () => { + const originalSeed = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴', '雞', '狗', '豬'] + const metricsEventSpy = sinon.spy() + const pushSpy = sinon.spy() + const root = shallowRender( + { + seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬', + history: { push: pushSpy }, + }, + { + metricsEvent: metricsEventSpy, + } + ) + + const shuffled = root.state().shuffledSeedWords + const seeds = root.find('.confirm-seed-phrase__seed-word--shuffled') + + + originalSeed.forEach(seed => { + const seedIndex = shuffled.findIndex(s => s === seed) + seeds.at(seedIndex).simulate('click') + }) + + root.update() + + root.find('.first-time-flow__button').simulate('click') + assert.deepEqual(metricsEventSpy.args[0][0], { + eventOpts: { + category: 'Onboarding', + action: 'Seed Phrase Setup', + name: 'Verify Complete', + }, + }) + assert.equal(pushSpy.args[0][0], '/initialize/end-of-flow') + }) +}) diff --git a/ui/app/pages/first-time-flow/select-action/select-action.component.js b/ui/app/pages/first-time-flow/select-action/select-action.component.js index b25a15514..5af29a505 100644 --- a/ui/app/pages/first-time-flow/select-action/select-action.component.js +++ b/ui/app/pages/first-time-flow/select-action/select-action.component.js @@ -95,7 +95,7 @@ export default class SelectAction extends PureComponent { </div> </div> <Button - type="confirm" + type="primary" className="first-time-flow__button" onClick={this.handleCreate} > diff --git a/ui/app/pages/first-time-flow/welcome/welcome.component.js b/ui/app/pages/first-time-flow/welcome/welcome.component.js index 3b8d6eb17..c720d2572 100644 --- a/ui/app/pages/first-time-flow/welcome/welcome.component.js +++ b/ui/app/pages/first-time-flow/welcome/welcome.component.js @@ -56,7 +56,7 @@ export default class Welcome extends PureComponent { <div>{ t('happyToSeeYou') }</div> </div> <Button - type="confirm" + type="primary" className="first-time-flow__button" onClick={this.handleContinue} > diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js index 29d93a9fa..4d96c3131 100644 --- a/ui/app/pages/home/home.component.js +++ b/ui/app/pages/home/home.component.js @@ -23,21 +23,27 @@ export default class Home extends PureComponent { providerRequests: PropTypes.array, } + componentWillMount () { + const { + history, + unconfirmedTransactionsCount = 0, + } = this.props + + if (unconfirmedTransactionsCount > 0) { + history.push(CONFIRM_TRANSACTION_ROUTE) + } + } + componentDidMount () { const { history, suggestedTokens = {}, - unconfirmedTransactionsCount = 0, } = this.props // suggested new tokens if (Object.keys(suggestedTokens).length > 0) { history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE) } - - if (unconfirmedTransactionsCount > 0) { - history.push(CONFIRM_TRANSACTION_ROUTE) - } } render () { @@ -45,6 +51,7 @@ export default class Home extends PureComponent { forgottenPassword, seedWords, providerRequests, + history, } = this.props // seed words @@ -69,7 +76,7 @@ export default class Home extends PureComponent { query="(min-width: 576px)" render={() => <WalletView />} /> - <TransactionView /> + { !history.location.pathname.match(/^\/confirm-transaction/) ? <TransactionView /> : null } </div> </div> ) diff --git a/ui/app/pages/home/home.container.js b/ui/app/pages/home/home.container.js index 7508654dc..d0a5d7b47 100644 --- a/ui/app/pages/home/home.container.js +++ b/ui/app/pages/home/home.container.js @@ -3,7 +3,7 @@ import { compose } from 'recompose' import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction' - +`` const mapStateToProps = state => { const { metamask, appState } = state const { diff --git a/ui/app/pages/keychains/reveal-seed.js b/ui/app/pages/keychains/reveal-seed.js index edc9db5a0..e83e3fd98 100644 --- a/ui/app/pages/keychains/reveal-seed.js +++ b/ui/app/pages/keychains/reveal-seed.js @@ -116,7 +116,7 @@ class RevealSeedPage extends Component { onClick: () => this.props.history.push(DEFAULT_ROUTE), }, this.context.t('cancel')), h(Button, { - type: 'primary', + type: 'secondary', large: true, className: 'page-container__footer-button', onClick: event => this.handleSubmit(event), diff --git a/ui/app/pages/mobile-sync/index.js b/ui/app/pages/mobile-sync/index.js index 0938ad103..00a514534 100644 --- a/ui/app/pages/mobile-sync/index.js +++ b/ui/app/pages/mobile-sync/index.js @@ -315,7 +315,7 @@ class MobileSyncPage extends Component { }, this.context.t('cancel')), h(Button, { - type: 'primary', + type: 'secondary', large: true, className: 'new-account-create-form__button', onClick: event => this.handleSubmit(event), diff --git a/ui/app/pages/provider-approval/provider-approval.component.js b/ui/app/pages/provider-approval/provider-approval.component.js index 1f1d68da7..70d3d0007 100644 --- a/ui/app/pages/provider-approval/provider-approval.component.js +++ b/ui/app/pages/provider-approval/provider-approval.component.js @@ -4,9 +4,9 @@ import ProviderPageContainer from '../../components/app/provider-page-container' export default class ProviderApproval extends Component { static propTypes = { - approveProviderRequest: PropTypes.func.isRequired, + approveProviderRequestByOrigin: PropTypes.func.isRequired, + rejectProviderRequestByOrigin: PropTypes.func.isRequired, providerRequest: PropTypes.object.isRequired, - rejectProviderRequest: PropTypes.func.isRequired, }; static contextTypes = { @@ -14,13 +14,13 @@ export default class ProviderApproval extends Component { }; render () { - const { approveProviderRequest, providerRequest, rejectProviderRequest } = this.props + const { approveProviderRequestByOrigin, providerRequest, rejectProviderRequestByOrigin } = this.props return ( <ProviderPageContainer - approveProviderRequest={approveProviderRequest} + approveProviderRequestByOrigin={approveProviderRequestByOrigin} + rejectProviderRequestByOrigin={rejectProviderRequestByOrigin} origin={providerRequest.origin} tabID={providerRequest.tabID} - rejectProviderRequest={rejectProviderRequest} siteImage={providerRequest.siteImage} siteTitle={providerRequest.siteTitle} /> diff --git a/ui/app/pages/provider-approval/provider-approval.container.js b/ui/app/pages/provider-approval/provider-approval.container.js index d53c0ae4d..1e167ddb7 100644 --- a/ui/app/pages/provider-approval/provider-approval.container.js +++ b/ui/app/pages/provider-approval/provider-approval.container.js @@ -1,11 +1,11 @@ import { connect } from 'react-redux' import ProviderApproval from './provider-approval.component' -import { approveProviderRequest, rejectProviderRequest } from '../../store/actions' +import { approveProviderRequestByOrigin, rejectProviderRequestByOrigin } from '../../store/actions' function mapDispatchToProps (dispatch) { return { - approveProviderRequest: tabID => dispatch(approveProviderRequest(tabID)), - rejectProviderRequest: tabID => dispatch(rejectProviderRequest(tabID)), + approveProviderRequestByOrigin: origin => dispatch(approveProviderRequestByOrigin(origin)), + rejectProviderRequestByOrigin: origin => dispatch(rejectProviderRequestByOrigin(origin)), } } diff --git a/ui/app/pages/routes/index.js b/ui/app/pages/routes/index.js index e06d88c90..9eeac2da2 100644 --- a/ui/app/pages/routes/index.js +++ b/ui/app/pages/routes/index.js @@ -5,12 +5,13 @@ import { Route, Switch, withRouter, matchPath } from 'react-router-dom' import { compose } from 'recompose' import actions from '../../store/actions' import log from 'loglevel' -import { getMetaMaskAccounts, getNetworkIdentifier } from '../../selectors/selectors' +import IdleTimer from 'react-idle-timer' +import {getMetaMaskAccounts, getNetworkIdentifier, preferencesSelector} from '../../selectors/selectors' // init import FirstTimeFlow from '../first-time-flow' // accounts -const SendTransactionScreen = require('../../components/app/send/send.container') +const SendTransactionScreen = require('../send/send.container') const ConfirmTransaction = require('../confirm-transaction') // slideout menu @@ -98,7 +99,9 @@ class Routes extends Component { } renderRoutes () { - return ( + const { autoLogoutTimeLimit, setLastActiveTime } = this.props + + const routes = ( <Switch> <Route path={LOCK_ROUTE} component={Lock} exact /> <Route path={INITIALIZE_ROUTE} component={FirstTimeFlow} /> @@ -116,6 +119,16 @@ class Routes extends Component { <Authenticated path={DEFAULT_ROUTE} component={Home} exact /> </Switch> ) + + if (autoLogoutTimeLimit > 0) { + return ( + <IdleTimer onAction={setLastActiveTime} throttle={1000}> + {routes} + </IdleTimer> + ) + } + + return routes } onInitializationUnlockPage () { @@ -267,6 +280,10 @@ class Routes extends Component { name = this.context.t('connectingToKovan') } else if (providerName === 'rinkeby') { name = this.context.t('connectingToRinkeby') + } else if (providerName === 'localhost') { + name = this.context.t('connectingToLocalhost') + } else if (providerName === 'goerli') { + name = this.context.t('connectingToGoerli') } else { name = this.context.t('connectingTo', [providerId]) } @@ -288,6 +305,10 @@ class Routes extends Component { name = this.context.t('kovan') } else if (providerName === 'rinkeby') { name = this.context.t('rinkeby') + } else if (providerName === 'localhost') { + name = this.context.t('localhost') + } else if (providerName === 'goerli') { + name = this.context.t('goerli') } else { name = this.context.t('unknownNetwork') } @@ -314,6 +335,7 @@ Routes.propTypes = { networkDropdownOpen: PropTypes.bool, showNetworkDropdown: PropTypes.func, hideNetworkDropdown: PropTypes.func, + setLastActiveTime: PropTypes.func, history: PropTypes.object, location: PropTypes.object, dispatch: PropTypes.func, @@ -336,6 +358,7 @@ Routes.propTypes = { t: PropTypes.func, providerId: PropTypes.string, providerRequests: PropTypes.array, + autoLogoutTimeLimit: PropTypes.number, } function mapStateToProps (state) { @@ -350,6 +373,7 @@ function mapStateToProps (state) { } = appState const accounts = getMetaMaskAccounts(state) + const { autoLogoutTimeLimit = 0 } = preferencesSelector(state) const { identities, @@ -401,6 +425,7 @@ function mapStateToProps (state) { Qr: state.appState.Qr, welcomeScreenSeen: state.metamask.welcomeScreenSeen, providerId: getNetworkIdentifier(state), + autoLogoutTimeLimit, // state needed to get account dropdown temporarily rendering from app bar identities, @@ -410,7 +435,7 @@ function mapStateToProps (state) { } } -function mapDispatchToProps (dispatch, ownProps) { +function mapDispatchToProps (dispatch) { return { dispatch, hideSidebar: () => dispatch(actions.hideSidebar()), @@ -419,6 +444,7 @@ function mapDispatchToProps (dispatch, ownProps) { setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')), toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)), + setLastActiveTime: () => dispatch(actions.setLastActiveTime()), } } diff --git a/ui/app/components/app/send/README.md b/ui/app/pages/send/README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/README.md +++ b/ui/app/pages/send/README.md diff --git a/ui/app/components/app/send/account-list-item/account-list-item-README.md b/ui/app/pages/send/account-list-item/account-list-item-README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/account-list-item/account-list-item-README.md +++ b/ui/app/pages/send/account-list-item/account-list-item-README.md diff --git a/ui/app/components/app/send/account-list-item/account-list-item.component.js b/ui/app/pages/send/account-list-item/account-list-item.component.js index 18e77b4f9..e6cca39b9 100644 --- a/ui/app/components/app/send/account-list-item/account-list-item.component.js +++ b/ui/app/pages/send/account-list-item/account-list-item.component.js @@ -1,11 +1,11 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { checksumAddress } from '../../../../helpers/utils/util' -import Identicon from '../../../ui/identicon' -import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' -import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common' -import Tooltip from '../../../ui/tooltip-v2' +import { checksumAddress } from '../../../helpers/utils/util' +import Identicon from '../../../components/ui/identicon' +import UserPreferencedCurrencyDisplay from '../../../components/app/user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' +import Tooltip from '../../../components/ui/tooltip-v2' export default class AccountListItem extends Component { diff --git a/ui/app/components/app/send/account-list-item/account-list-item.container.js b/ui/app/pages/send/account-list-item/account-list-item.container.js index bc9a60f49..21f800306 100644 --- a/ui/app/components/app/send/account-list-item/account-list-item.container.js +++ b/ui/app/pages/send/account-list-item/account-list-item.container.js @@ -8,7 +8,7 @@ import { getIsMainnet, isBalanceCached, preferencesSelector, -} from '../../../../selectors/selectors' +} from '../../../selectors/selectors' import AccountListItem from './account-list-item.component' export default connect(mapStateToProps)(AccountListItem) diff --git a/ui/app/components/app/send/account-list-item/index.js b/ui/app/pages/send/account-list-item/index.js index 907485cf7..907485cf7 100644 --- a/ui/app/components/app/send/account-list-item/index.js +++ b/ui/app/pages/send/account-list-item/index.js diff --git a/ui/app/components/app/send/account-list-item/tests/account-list-item-component.test.js b/ui/app/pages/send/account-list-item/tests/account-list-item-component.test.js index 5df9f77d6..bec88402d 100644 --- a/ui/app/components/app/send/account-list-item/tests/account-list-item-component.test.js +++ b/ui/app/pages/send/account-list-item/tests/account-list-item-component.test.js @@ -3,15 +3,15 @@ import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' import proxyquire from 'proxyquire' -import Identicon from '../../../../ui/identicon' -import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display' +import Identicon from '../../../../components/ui/identicon' +import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display' const utilsMethodStubs = { checksumAddress: sinon.stub().returns('mockCheckSumAddress'), } const AccountListItem = proxyquire('../account-list-item.component.js', { - '../../../../helpers/utils/util': utilsMethodStubs, + '../../../helpers/utils/util': utilsMethodStubs, }).default diff --git a/ui/app/components/app/send/account-list-item/tests/account-list-item-container.test.js b/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js index 19a9a02d0..1580fd497 100644 --- a/ui/app/components/app/send/account-list-item/tests/account-list-item-container.test.js +++ b/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js @@ -5,7 +5,7 @@ let mapStateToProps proxyquire('../account-list-item.container.js', { 'react-redux': { - connect: (ms, md) => { + connect: (ms) => { mapStateToProps = ms return () => ({}) }, @@ -15,7 +15,7 @@ proxyquire('../account-list-item.container.js', { getCurrentCurrency: () => `mockCurrentCurrency`, getNativeCurrency: () => `mockNativeCurrency`, }, - '../../../../selectors/selectors': { + '../../../selectors/selectors': { isBalanceCached: () => `mockBalanceIsCached`, preferencesSelector: ({ showFiatInTestnets }) => ({ showFiatInTestnets, diff --git a/ui/app/components/app/send/index.js b/ui/app/pages/send/index.js index b5114babc..b5114babc 100644 --- a/ui/app/components/app/send/index.js +++ b/ui/app/pages/send/index.js diff --git a/ui/app/components/app/send/send-content/index.js b/ui/app/pages/send/send-content/index.js index 891c17e6a..891c17e6a 100644 --- a/ui/app/components/app/send/send-content/index.js +++ b/ui/app/pages/send/send-content/index.js diff --git a/ui/app/components/app/send/send-content/send-amount-row/README.md b/ui/app/pages/send/send-content/send-amount-row/README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/README.md +++ b/ui/app/pages/send/send-content/send-amount-row/README.md 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 new file mode 100644 index 000000000..7901ccef6 --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js @@ -0,0 +1,75 @@ +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 = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + setMaxAmount () { + const { + balance, + gasTotal, + selectedToken, + setAmountToMax, + tokenBalance, + } = this.props + + setAmountToMax({ + balance, + gasTotal, + selectedToken, + tokenBalance, + }) + } + + onMaxClick = () => { + const { setMaxModeTo, clearMaxAmount, maxModeOn } = this.props + const { metricsEvent } = this.context + + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Clicked "Amount Max"', + }, + }) + if (!maxModeOn) { + setMaxModeTo(true) + this.setMaxAmount() + } else { + setMaxModeTo(false) + clearMaxAmount() + } + } + + render () { + 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/components/app/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 16c5a0db5..e444589a1 100644 --- a/ui/app/components/app/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,16 +5,17 @@ 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 { updateSendAmount, setMaxModeTo, -} from '../../../../../../store/actions' +} from '../../../../../store/actions' import AmountMaxButton from './amount-max-button.component' import { updateSendErrors, -} from '../../../../../../ducks/send/send.duck' +} from '../../../../../ducks/send/send.duck' export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton) @@ -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/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js index 69fec1994..69fec1994 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js diff --git a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js index f4c8fad8a..a570e49b4 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js @@ -1,7 +1,7 @@ const { multiplyCurrencies, subtractCurrencies, -} = require('../../../../../../helpers/utils/conversion-util') +} = require('../../../../../helpers/utils/conversion-util') const ethUtil = require('ethereumjs-util') function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) { diff --git a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/index.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/index.js index ee8271494..ee8271494 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/index.js +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/index.js diff --git a/ui/app/components/app/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 b04d3897f..f986b26bb 100644 --- a/ui/app/components/app/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 @@ -26,7 +26,12 @@ describe('AmountMaxButton Component', function () { setAmountToMax={propsMethodSpies.setAmountToMax} setMaxModeTo={propsMethodSpies.setMaxModeTo} tokenBalance={'mockTokenBalance'} - />, { context: { t: str => str + '_t' } }) + />, { + context: { + t: str => str + '_t', + metricsEvent: () => {}, + }, + }) instance = wrapper.instance() }) @@ -60,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() @@ -76,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/components/app/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 f446e330c..dcee8fda0 100644 --- a/ui/app/components/app/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,8 +29,9 @@ 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 }, - '../../../../../../store/actions': actionSpies, - '../../../../../../ducks/send/send.duck': duckActionSpies, + '../../../../../selectors/custom-gas': { getBasicGasEstimateLoadingStatus: (s) => `mockButtonDataLoading:${s}`}, + '../../../../../store/actions': actionSpies, + '../../../../../ducks/send/send.duck': duckActionSpies, }) describe('amount-max-button container', () => { @@ -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/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js index 655fe1969..655fe1969 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js diff --git a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js index 1ee858f67..1ee858f67 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js diff --git a/ui/app/components/app/send/send-content/send-amount-row/index.js b/ui/app/pages/send/send-content/send-amount-row/index.js index abc6852fe..abc6852fe 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/index.js +++ b/ui/app/pages/send/send-content/send-amount-row/index.js diff --git a/ui/app/components/app/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 e725e7eda..10e90c419 100644 --- a/ui/app/components/app/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 @@ -2,8 +2,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import SendRowWrapper from '../send-row-wrapper' import AmountMaxButton from './amount-max-button' -import UserPreferencedCurrencyInput from '../../../user-preferenced-currency-input' -import UserPreferencedTokenInput from '../../../user-preferenced-token-input' +import UserPreferencedCurrencyInput from '../../../../components/app/user-preferenced-currency-input' +import UserPreferencedTokenInput from '../../../../components/app/user-preferenced-token-input' export default class SendAmountRow extends Component { @@ -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/components/app/send/send-content/send-amount-row/send-amount-row.container.js b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.container.js index 0646355ab..2b3470da4 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.container.js +++ b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.container.js @@ -17,10 +17,10 @@ import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils' import { setMaxModeTo, updateSendAmount, -} from '../../../../../store/actions' +} from '../../../../store/actions' import { updateSendErrors, -} from '../../../../../ducks/send/send.duck' +} from '../../../../ducks/send/send.duck' import SendAmountRow from './send-amount-row.component' export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow) diff --git a/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.scss b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.scss +++ b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.scss diff --git a/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.selectors.js index fb08c7ed7..fb08c7ed7 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.selectors.js +++ b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.selectors.js diff --git a/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-component.test.js index 14a71129f..62e0676db 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-component.test.js +++ b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-component.test.js @@ -6,7 +6,7 @@ import SendAmountRow from '../send-amount-row.component.js' import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' import AmountMaxButton from '../amount-max-button/amount-max-button.container' -import UserPreferencedTokenInput from '../../../../user-preferenced-token-input' +import UserPreferencedTokenInput from '../../../../../components/app/user-preferenced-token-input' const propsMethodSpies = { setMaxModeTo: sinon.spy(), diff --git a/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-container.test.js index 6d20202b0..dada1c5e9 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-container.test.js +++ b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-container.test.js @@ -37,8 +37,8 @@ proxyquire('../send-amount-row.container.js', { getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }), getGasFeeErrorObject: (mockDataObject) => ({ ...mockDataObject, mockGasFeeErrorChange: true }), }, - '../../../../../store/actions': actionSpies, - '../../../../../ducks/send/send.duck': duckActionSpies, + '../../../../store/actions': actionSpies, + '../../../../ducks/send/send.duck': duckActionSpies, }) describe('send-amount-row container', () => { diff --git a/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js index 4672cb8a7..4672cb8a7 100644 --- a/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js +++ b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js diff --git a/ui/app/pages/send/send-content/send-asset-row/index.js b/ui/app/pages/send/send-content/send-asset-row/index.js new file mode 100644 index 000000000..ba424a083 --- /dev/null +++ b/ui/app/pages/send/send-content/send-asset-row/index.js @@ -0,0 +1 @@ +export { default } from './send-asset-row.container' diff --git a/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js new file mode 100644 index 000000000..de2d9462f --- /dev/null +++ b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js @@ -0,0 +1,152 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper' +import Identicon from '../../../../components/ui/identicon/identicon.component' +import TokenBalance from '../../../../components/ui/token-balance' +import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display' +import {PRIMARY} from '../../../../helpers/constants/common' + +export default class SendAssetRow extends Component { + static propTypes = { + tokens: PropTypes.arrayOf( + PropTypes.shape({ + address: PropTypes.string, + decimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + symbol: PropTypes.string, + }) + ).isRequired, + accounts: PropTypes.object.isRequired, + selectedAddress: PropTypes.string.isRequired, + selectedTokenAddress: PropTypes.string, + setSelectedToken: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + state = { + isShowingDropdown: false, + } + + openDropdown = () => this.setState({ isShowingDropdown: true }) + + closeDropdown = () => this.setState({ isShowingDropdown: false }) + + selectToken = address => { + this.setState({ + isShowingDropdown: false, + }, () => { + this.context.metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Send Screen', + name: 'User clicks "Assets" dropdown', + }, + customVariables: { + assetSelected: address ? 'ERC20' : 'ETH', + }, + }) + this.props.setSelectedToken(address) + }) + } + + render () { + const { t } = this.context + + return ( + <SendRowWrapper label={`${t('asset')}:`}> + <div className="send-v2__asset-dropdown"> + { this.renderSelectedToken() } + { this.renderAssetDropdown() } + </div> + </SendRowWrapper> + ) + } + + renderSelectedToken () { + const { selectedTokenAddress } = this.props + const token = this.props.tokens.find(({ address }) => address === selectedTokenAddress) + return ( + <div + className="send-v2__asset-dropdown__input-wrapper" + onClick={this.openDropdown} + > + { token ? this.renderAsset(token) : this.renderEth() } + </div> + ) + } + + renderAssetDropdown () { + return this.state.isShowingDropdown && ( + <div> + <div + className="send-v2__asset-dropdown__close-area" + onClick={this.closeDropdown} + /> + <div className="send-v2__asset-dropdown__list"> + { this.renderEth() } + { this.props.tokens.map(token => this.renderAsset(token)) } + </div> + </div> + ) + } + + renderEth () { + const { t } = this.context + const { accounts, selectedAddress } = this.props + + const balanceValue = accounts[selectedAddress] ? accounts[selectedAddress].balance : '' + + return ( + <div + className="send-v2__asset-dropdown__asset" + onClick={() => this.selectToken()} + > + <div className="send-v2__asset-dropdown__asset-icon"> + <Identicon diameter={36} /> + </div> + <div className="send-v2__asset-dropdown__asset-data"> + <div className="send-v2__asset-dropdown__symbol">ETH</div> + <div className="send-v2__asset-dropdown__name"> + <span className="send-v2__asset-dropdown__name__label">{`${t('balance')}:`}</span> + <UserPreferencedCurrencyDisplay + value={balanceValue} + type={PRIMARY} + /> + </div> + </div> + </div> + ) + } + + + renderAsset (token) { + const { address, symbol } = token + const { t } = this.context + + return ( + <div + key={address} className="send-v2__asset-dropdown__asset" + onClick={() => this.selectToken(address)} + > + <div className="send-v2__asset-dropdown__asset-icon"> + <Identicon address={address} diameter={36} /> + </div> + <div className="send-v2__asset-dropdown__asset-data"> + <div className="send-v2__asset-dropdown__symbol"> + { symbol } + </div> + <div className="send-v2__asset-dropdown__name"> + <span className="send-v2__asset-dropdown__name__label">{`${t('balance')}:`}</span> + <TokenBalance + token={token} + withSymbol + /> + </div> + </div> + </div> + ) + } +} diff --git a/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js new file mode 100644 index 000000000..57b62fba1 --- /dev/null +++ b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux' +import SendAssetRow from './send-asset-row.component' +import {getMetaMaskAccounts} from '../../../../selectors/selectors' +import { setSelectedToken } from '../../../../store/actions' + +function mapStateToProps (state) { + return { + tokens: state.metamask.tokens, + selectedAddress: state.metamask.selectedAddress, + selectedTokenAddress: state.metamask.selectedTokenAddress, + accounts: getMetaMaskAccounts(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + setSelectedToken: address => dispatch(setSelectedToken(address)), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(SendAssetRow) diff --git a/ui/app/components/app/send/send-content/send-content.component.js b/ui/app/pages/send/send-content/send-content.component.js index 2c09ceb19..d799806c7 100644 --- a/ui/app/components/app/send/send-content/send-content.component.js +++ b/ui/app/pages/send/send-content/send-content.component.js @@ -1,11 +1,12 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import PageContainerContent from '../../../ui/page-container/page-container-content.component' +import PageContainerContent from '../../../components/ui/page-container/page-container-content.component' import SendAmountRow from './send-amount-row' import SendFromRow from './send-from-row' import SendGasRow from './send-gas-row' import SendHexDataRow from './send-hex-data-row' import SendToRow from './send-to-row' +import SendAssetRow from './send-asset-row' export default class SendContent extends Component { @@ -26,6 +27,7 @@ export default class SendContent extends Component { updateGas={this.updateGas} scanQrCode={ _ => this.props.scanQrCode()} /> + <SendAssetRow /> <SendAmountRow updateGas={this.updateGas} /> <SendGasRow /> {(this.props.showHexData && ( diff --git a/ui/app/components/app/send/send-content/send-dropdown-list/index.js b/ui/app/pages/send/send-content/send-dropdown-list/index.js index 04af6536c..04af6536c 100644 --- a/ui/app/components/app/send/send-content/send-dropdown-list/index.js +++ b/ui/app/pages/send/send-content/send-dropdown-list/index.js diff --git a/ui/app/components/app/send/send-content/send-dropdown-list/send-dropdown-list.component.js b/ui/app/pages/send/send-content/send-dropdown-list/send-dropdown-list.component.js index 0d026bc69..0d026bc69 100644 --- a/ui/app/components/app/send/send-content/send-dropdown-list/send-dropdown-list.component.js +++ b/ui/app/pages/send/send-content/send-dropdown-list/send-dropdown-list.component.js diff --git a/ui/app/components/app/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js b/ui/app/pages/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js index b92dd4dfe..b92dd4dfe 100644 --- a/ui/app/components/app/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js +++ b/ui/app/pages/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js diff --git a/ui/app/components/app/send/send-content/send-from-row/index.js b/ui/app/pages/send/send-content/send-from-row/index.js index 0a79726b2..0a79726b2 100644 --- a/ui/app/components/app/send/send-content/send-from-row/index.js +++ b/ui/app/pages/send/send-content/send-from-row/index.js diff --git a/ui/app/components/app/send/send-content/send-from-row/send-from-row.component.js b/ui/app/pages/send/send-content/send-from-row/send-from-row.component.js index dfa53e970..dfa53e970 100644 --- a/ui/app/components/app/send/send-content/send-from-row/send-from-row.component.js +++ b/ui/app/pages/send/send-content/send-from-row/send-from-row.component.js diff --git a/ui/app/components/app/send/send-content/send-from-row/send-from-row.container.js b/ui/app/pages/send/send-content/send-from-row/send-from-row.container.js index fe3ac9aa1..fe3ac9aa1 100644 --- a/ui/app/components/app/send/send-content/send-from-row/send-from-row.container.js +++ b/ui/app/pages/send/send-content/send-from-row/send-from-row.container.js diff --git a/ui/app/components/app/send/send-content/send-from-row/send-from-row.selectors.js b/ui/app/pages/send/send-content/send-from-row/send-from-row.selectors.js index 03ef4806b..03ef4806b 100644 --- a/ui/app/components/app/send/send-content/send-from-row/send-from-row.selectors.js +++ b/ui/app/pages/send/send-content/send-from-row/send-from-row.selectors.js diff --git a/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-component.test.js index 18811c57e..18811c57e 100644 --- a/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-component.test.js +++ b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-component.test.js diff --git a/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-container.test.js index fd771ea77..fd771ea77 100644 --- a/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-container.test.js +++ b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-container.test.js diff --git a/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-selectors.test.js b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-selectors.test.js index ecb57bbc3..ecb57bbc3 100644 --- a/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-selectors.test.js +++ b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-selectors.test.js diff --git a/ui/app/components/app/send/send-content/send-gas-row/README.md b/ui/app/pages/send/send-content/send-gas-row/README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-content/send-gas-row/README.md +++ b/ui/app/pages/send/send-content/send-gas-row/README.md diff --git a/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js index 48088607a..3f5587318 100644 --- a/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js +++ b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js @@ -1,7 +1,7 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' -import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display' -import { PRIMARY, SECONDARY } from '../../../../../../helpers/constants/common' +import UserPreferencedCurrencyDisplay from '../../../../../components/app/user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../../../helpers/constants/common' export default class GasFeeDisplay extends Component { diff --git a/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/index.js b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/index.js index dba0edb7b..dba0edb7b 100644 --- a/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/index.js +++ b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/index.js diff --git a/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js index cb4180508..eedd43221 100644 --- a/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js +++ b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import {shallow} from 'enzyme' import GasFeeDisplay from '../gas-fee-display.component' -import UserPreferencedCurrencyDisplay from '../../../../../user-preferenced-currency-display' +import UserPreferencedCurrencyDisplay from '../../../../../../components/app/user-preferenced-currency-display' import sinon from 'sinon' diff --git a/ui/app/components/app/send/send-content/send-gas-row/index.js b/ui/app/pages/send/send-content/send-gas-row/index.js index 3c7ff1d5f..3c7ff1d5f 100644 --- a/ui/app/components/app/send/send-content/send-gas-row/index.js +++ b/ui/app/pages/send/send-content/send-gas-row/index.js diff --git a/ui/app/components/app/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 424a65b20..4c09ed564 100644 --- a/ui/app/components/app/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 @@ -2,26 +2,31 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import SendRowWrapper from '../send-row-wrapper' import GasFeeDisplay from './gas-fee-display/gas-fee-display.component' -import GasPriceButtonGroup from '../../../gas-customization/gas-price-button-group' -import AdvancedGasInputs from '../../../gas-customization/advanced-gas-inputs' +import GasPriceButtonGroup from '../../../../components/app/gas-customization/gas-price-button-group' +import AdvancedGasInputs from '../../../../components/app/gas-customization/advanced-gas-inputs' 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, resetGasButtons: PropTypes.func, - gasPrice: PropTypes.number, - gasLimit: PropTypes.number, + gasPrice: PropTypes.string, + gasLimit: PropTypes.string, insufficientBalance: 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/components/app/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 f81670c02..10eaa50b8 100644 --- a/ui/app/components/app/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,29 +6,37 @@ 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, getDefaultActiveButtonIndex, -} from '../../../../../selectors/custom-gas' +} from '../../../../selectors/custom-gas' import { showGasButtonGroup, -} from '../../../../../ducks/send/send.duck' + updateSendErrors, +} from '../../../../ducks/send/send.duck' import { resetCustomData, setCustomGasPrice, setCustomGasLimit, -} from '../../../../../ducks/gas/gas.duck' +} from '../../../../ducks/gas/gas.duck' import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js' -import { showModal, setGasPrice, setGasLimit, setGasTotal } from '../../../../../store/actions' -import { getAdvancedInlineGasShown, getCurrentEthBalance, getSelectedToken } from '../../../../../selectors/selectors' +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/components/app/send/send-content/send-gas-row/send-gas-row.scss b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.scss +++ b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.scss diff --git a/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.selectors.js index 79c838543..79c838543 100644 --- a/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.selectors.js +++ b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.selectors.js diff --git a/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-component.test.js index 08f26854e..0cbc92621 100644 --- a/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-component.test.js +++ b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-component.test.js @@ -6,7 +6,7 @@ import SendGasRow from '../send-gas-row.component.js' import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' import GasFeeDisplay from '../gas-fee-display/gas-fee-display.component' -import GasPriceButtonGroup from '../../../../gas-customization/gas-price-button-group' +import GasPriceButtonGroup from '../../../../../components/app/gas-customization/gas-price-button-group' const propsMethodSpies = { showCustomizeGasModal: sinon.spy(), diff --git a/ui/app/components/app/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 d1f753639..4acb310f8 100644 --- a/ui/app/components/app/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 @@ -32,7 +32,7 @@ proxyquire('../send-gas-row.container.js', { return () => ({}) }, }, - '../../../../../selectors/selectors': { + '../../../../selectors/selectors': { getCurrentEthBalance: (s) => `mockCurrentEthBalance:${s}`, getAdvancedInlineGasShown: (s) => `mockAdvancedInlineGasShown:${s}`, getSelectedToken: () => false, @@ -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: ({ @@ -59,14 +64,14 @@ proxyquire('../send-gas-row.container.js', { gasFeeIsInError: (s) => `mockGasFeeError:${s}`, getGasButtonGroupShown: (s) => `mockGetGasButtonGroupShown:${s}`, }, - '../../../../../store/actions': actionSpies, - '../../../../../selectors/custom-gas': { + '../../../../store/actions': actionSpies, + '../../../../selectors/custom-gas': { getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${s}`, getRenderableEstimateDataForSmallButtonsFromGWEI: (s) => `mockGasButtonInfo:${s}`, getDefaultActiveButtonIndex: (gasButtonInfo, gasPrice) => gasButtonInfo.length + gasPrice.length, }, - '../../../../../ducks/send/send.duck': sendDuckSpies, - '../../../../../ducks/gas/gas.duck': gasDuckSpies, + '../../../../ducks/send/send.duck': sendDuckSpies, + '../../../../ducks/gas/gas.duck': gasDuckSpies, }) describe('send-gas-row container', () => { @@ -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/components/app/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js index bd3c9a257..bd3c9a257 100644 --- a/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js +++ b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js diff --git a/ui/app/components/app/send/send-content/send-hex-data-row/index.js b/ui/app/pages/send/send-content/send-hex-data-row/index.js index 08c341067..08c341067 100644 --- a/ui/app/components/app/send/send-content/send-hex-data-row/index.js +++ b/ui/app/pages/send/send-content/send-hex-data-row/index.js diff --git a/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js index 62a74a77b..62a74a77b 100644 --- a/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.component.js +++ b/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js diff --git a/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.container.js b/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js index 76c929d08..8b1c540c3 100644 --- a/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.container.js +++ b/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux' import { updateSendHexData, -} from '../../../../../store/actions' +} from '../../../../store/actions' import SendHexDataRow from './send-hex-data-row.component' export default connect(mapStateToProps, mapDispatchToProps)(SendHexDataRow) diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/index.js b/ui/app/pages/send/send-content/send-row-wrapper/index.js index d17545dcc..d17545dcc 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/index.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/index.js diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/index.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/index.js index c00617f83..c00617f83 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/index.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/index.js diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md diff --git a/ui/app/components/app/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/components/app/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/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js index 59622047f..59622047f 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js index 2304a43d2..2304a43d2 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js index eecff165d..2013e3200 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js @@ -5,7 +5,7 @@ let mapStateToProps proxyquire('../send-row-error-message.container.js', { 'react-redux': { - connect: (ms, md) => { + connect: (ms) => { mapStateToProps = ms return () => ({}) }, diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/index.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/index.js index fd4d19ef7..fd4d19ef7 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/index.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/index.js diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js index f1caa8f99..f1caa8f99 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js index 7df14fd96..7df14fd96 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js index bd803d833..bd803d833 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js index 225bf056c..6c0739f0e 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js @@ -5,7 +5,7 @@ let mapStateToProps proxyquire('../send-row-warning-message.container.js', { 'react-redux': { - connect: (ms, md) => { + connect: (ms) => { mapStateToProps = ms return () => ({}) }, diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper-README.md b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper-README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper-README.md +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper-README.md diff --git a/ui/app/components/app/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/components/app/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/components/app/send/send-content/send-row-wrapper/send-row-wrapper.scss b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.scss +++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.scss diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js b/ui/app/pages/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js index 30280e1d0..30280e1d0 100644 --- a/ui/app/components/app/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js +++ b/ui/app/pages/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js diff --git a/ui/app/components/app/send/send-content/send-to-row/index.js b/ui/app/pages/send/send-content/send-to-row/index.js index 121f15148..121f15148 100644 --- a/ui/app/components/app/send/send-content/send-to-row/index.js +++ b/ui/app/pages/send/send-content/send-to-row/index.js diff --git a/ui/app/components/app/send/send-content/send-to-row/send-to-row-README.md b/ui/app/pages/send/send-content/send-to-row/send-to-row-README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-content/send-to-row/send-to-row-README.md +++ b/ui/app/pages/send/send-content/send-to-row/send-to-row-README.md diff --git a/ui/app/components/app/send/send-content/send-to-row/send-to-row.component.js b/ui/app/pages/send/send-content/send-to-row/send-to-row.component.js index e8a55cb2a..9baf327c1 100644 --- a/ui/app/components/app/send/send-content/send-to-row/send-to-row.component.js +++ b/ui/app/pages/send/send-content/send-to-row/send-to-row.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import SendRowWrapper from '../send-row-wrapper' -import EnsInput from '../../../ens-input' +import EnsInput from '../../../../components/app/ens-input' import { getToErrorObject, getToWarningObject } from './send-to-row.utils.js' export default class SendToRow extends Component { diff --git a/ui/app/components/app/send/send-content/send-to-row/send-to-row.container.js b/ui/app/pages/send/send-content/send-to-row/send-to-row.container.js index 30865d295..2cbe9fcd0 100644 --- a/ui/app/components/app/send/send-content/send-to-row/send-to-row.container.js +++ b/ui/app/pages/send/send-content/send-to-row/send-to-row.container.js @@ -14,13 +14,13 @@ import { } from './send-to-row.selectors.js' import { updateSendTo, -} from '../../../../../store/actions' +} from '../../../../store/actions' import { updateSendErrors, updateSendWarnings, openToDropdown, closeToDropdown, -} from '../../../../../ducks/send/send.duck' +} from '../../../../ducks/send/send.duck' import SendToRow from './send-to-row.component' export default connect(mapStateToProps, mapDispatchToProps)(SendToRow) diff --git a/ui/app/components/app/send/send-content/send-to-row/send-to-row.selectors.js b/ui/app/pages/send/send-content/send-to-row/send-to-row.selectors.js index a6160d335..a6160d335 100644 --- a/ui/app/components/app/send/send-content/send-to-row/send-to-row.selectors.js +++ b/ui/app/pages/send/send-content/send-to-row/send-to-row.selectors.js diff --git a/ui/app/components/app/send/send-content/send-to-row/send-to-row.utils.js b/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js index 60e75d34c..b3b0d2da3 100644 --- a/ui/app/components/app/send/send-content/send-to-row/send-to-row.utils.js +++ b/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js @@ -4,22 +4,21 @@ const { KNOWN_RECIPIENT_ADDRESS_ERROR, INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR, } = require('../../send.constants') -const { isValidAddress, isEthNetwork } = require('../../../../../helpers/utils/util') -import { checkExistingAddresses } from '../../../../../pages/add-token/util' +const { isValidAddress, isEthNetwork } = require('../../../../helpers/utils/util') +import { checkExistingAddresses } from '../../../add-token/util' const ethUtil = require('ethereumjs-util') const contractMap = require('eth-contract-metadata') -function getToErrorObject (to, toError = null, hasHexData = false, tokens = [], selectedToken = null, network) { +function getToErrorObject (to, toError = null, hasHexData = false, _, __, network) { if (!to) { if (!hasHexData) { toError = REQUIRED_ERROR } } else if (!isValidAddress(to, network) && !toError) { toError = isEthNetwork(network) ? INVALID_RECIPIENT_ADDRESS_ERROR : INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR - } else if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) { - toError = KNOWN_RECIPIENT_ADDRESS_ERROR } + return { to: toError } } diff --git a/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-component.test.js index d4d054057..c180d97f1 100644 --- a/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-component.test.js +++ b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-component.test.js @@ -16,7 +16,7 @@ const SendToRow = proxyquire('../send-to-row.component.js', { }).default import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' -import EnsInput from '../../../../ens-input' +import EnsInput from '../../../../../components/app/ens-input' const propsMethodSpies = { closeToDropdown: sinon.spy(), diff --git a/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-container.test.js b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-container.test.js index 94b4f1024..bb8702e9a 100644 --- a/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-container.test.js +++ b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-container.test.js @@ -36,8 +36,8 @@ proxyquire('../send-to-row.container.js', { sendToIsInWarning: (s) => `mockInWarning:${s}`, getTokens: (s) => `mockTokens:${s}`, }, - '../../../../../store/actions': actionSpies, - '../../../../../ducks/send/send.duck': duckActionSpies, + '../../../../store/actions': actionSpies, + '../../../../ducks/send/send.duck': duckActionSpies, }) describe('send-to-row container', () => { diff --git a/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-selectors.test.js b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-selectors.test.js index 0fa342d1e..0fa342d1e 100644 --- a/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-selectors.test.js +++ b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-selectors.test.js diff --git a/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js index 95882d640..f8a6dd96f 100644 --- a/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-utils.test.js +++ b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js @@ -13,7 +13,7 @@ const stubs = { } const toRowUtils = proxyquire('../send-to-row.utils.js', { - '../../../../../helpers/utils/util': { + '../../../../helpers/utils/util': { isValidAddress: stubs.isValidAddress, }, }) @@ -55,9 +55,9 @@ describe('send-to-row utils', () => { }) }) - it('should return a known address recipient if to is truthy but part of state tokens', () => { + it('should return null if to is truthy but part of state tokens', () => { assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { - to: KNOWN_RECIPIENT_ADDRESS_ERROR, + to: null, }) }) @@ -67,14 +67,14 @@ describe('send-to-row utils', () => { }) }) - it('should return a known address recipient if to is truthy but part of contract metadata', () => { + it('should return null if to is truthy but part of contract metadata', () => { assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { - to: KNOWN_RECIPIENT_ADDRESS_ERROR, + to: null, }) }) it('should null if to is truthy part of contract metadata but selectedToken falsy', () => { assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { - to: KNOWN_RECIPIENT_ADDRESS_ERROR, + to: null, }) }) }) diff --git a/ui/app/components/app/send/send-content/tests/send-content-component.test.js b/ui/app/pages/send/send-content/tests/send-content-component.test.js index 7d102c930..521c6523e 100644 --- a/ui/app/components/app/send/send-content/tests/send-content-component.test.js +++ b/ui/app/pages/send/send-content/tests/send-content-component.test.js @@ -3,12 +3,13 @@ import assert from 'assert' import { shallow } from 'enzyme' import SendContent from '../send-content.component.js' -import PageContainerContent from '../../../../ui/page-container/page-container-content.component' +import PageContainerContent from '../../../../components/ui/page-container/page-container-content.component' import SendAmountRow from '../send-amount-row/send-amount-row.container' import SendFromRow from '../send-from-row/send-from-row.container' import SendGasRow from '../send-gas-row/send-gas-row.container' import SendToRow from '../send-to-row/send-to-row.container' import SendHexDataRow from '../send-hex-data-row/send-hex-data-row.container' +import SendAssetRow from '../send-asset-row/send-asset-row.container' describe('SendContent Component', function () { let wrapper @@ -32,9 +33,10 @@ describe('SendContent Component', function () { const PageContainerContentChild = wrapper.find(PageContainerContent).children() assert(PageContainerContentChild.childAt(0).is(SendFromRow)) assert(PageContainerContentChild.childAt(1).is(SendToRow)) - assert(PageContainerContentChild.childAt(2).is(SendAmountRow)) - assert(PageContainerContentChild.childAt(3).is(SendGasRow)) - assert(PageContainerContentChild.childAt(4).is(SendHexDataRow)) + assert(PageContainerContentChild.childAt(2).is(SendAssetRow)) + assert(PageContainerContentChild.childAt(3).is(SendAmountRow)) + assert(PageContainerContentChild.childAt(4).is(SendGasRow)) + assert(PageContainerContentChild.childAt(5).is(SendHexDataRow)) }) it('should not render the SendHexDataRow if props.showHexData is false', () => { @@ -42,9 +44,10 @@ describe('SendContent Component', function () { const PageContainerContentChild = wrapper.find(PageContainerContent).children() assert(PageContainerContentChild.childAt(0).is(SendFromRow)) assert(PageContainerContentChild.childAt(1).is(SendToRow)) - assert(PageContainerContentChild.childAt(2).is(SendAmountRow)) - assert(PageContainerContentChild.childAt(3).is(SendGasRow)) - assert.equal(PageContainerContentChild.childAt(4).exists(), false) + assert(PageContainerContentChild.childAt(2).is(SendAssetRow)) + assert(PageContainerContentChild.childAt(3).is(SendAmountRow)) + assert(PageContainerContentChild.childAt(4).is(SendGasRow)) + assert.equal(PageContainerContentChild.childAt(5).exists(), false) }) }) }) diff --git a/ui/app/components/app/send/send-footer/README.md b/ui/app/pages/send/send-footer/README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-footer/README.md +++ b/ui/app/pages/send/send-footer/README.md diff --git a/ui/app/components/app/send/send-footer/index.js b/ui/app/pages/send/send-footer/index.js index 58e91d622..58e91d622 100644 --- a/ui/app/components/app/send/send-footer/index.js +++ b/ui/app/pages/send/send-footer/index.js diff --git a/ui/app/components/app/send/send-footer/send-footer.component.js b/ui/app/pages/send/send-footer/send-footer.component.js index cc891a9b3..16a8fdde2 100644 --- a/ui/app/components/app/send/send-footer/send-footer.component.js +++ b/ui/app/pages/send/send-footer/send-footer.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import PageContainerFooter from '../../../ui/page-container/page-container-footer' -import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../helpers/constants/routes' +import PageContainerFooter from '../../../components/ui/page-container/page-container-footer' +import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes' export default class SendFooter extends Component { @@ -27,6 +27,7 @@ export default class SendFooter extends Component { unapprovedTxs: PropTypes.object, update: PropTypes.func, sendErrors: PropTypes.object, + gasChangedLabel: PropTypes.string, } static contextTypes = { @@ -57,6 +58,7 @@ export default class SendFooter extends Component { update, toAccounts, history, + gasChangedLabel, } = this.props const { metricsEvent } = this.context @@ -91,6 +93,9 @@ export default class SendFooter extends Component { action: 'Edit Screen', name: 'Complete', }, + customVariables: { + gasChanged: gasChangedLabel, + }, }) history.push(CONFIRM_TRANSACTION_ROUTE) }) diff --git a/ui/app/components/app/send/send-footer/send-footer.container.js b/ui/app/pages/send/send-footer/send-footer.container.js index ea3fd7ee4..68f4dc7c3 100644 --- a/ui/app/components/app/send/send-footer/send-footer.container.js +++ b/ui/app/pages/send/send-footer/send-footer.container.js @@ -6,7 +6,7 @@ import { signTokenTx, signTx, updateTransaction, -} from '../../../../store/actions' +} from '../../../store/actions' import SendFooter from './send-footer.component' import { getGasLimit, @@ -31,10 +31,21 @@ import { constructTxParams, constructUpdatedTx, } from './send-footer.utils' +import { + getRenderableEstimateDataForSmallButtonsFromGWEI, + getDefaultActiveButtonIndex, +} from '../../../selectors/custom-gas' export default connect(mapStateToProps, mapDispatchToProps)(SendFooter) function mapStateToProps (state) { + const gasButtonInfo = getRenderableEstimateDataForSmallButtonsFromGWEI(state) + const gasPrice = getGasPrice(state) + const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, gasPrice) + const gasChangedLabel = activeButtonIndex >= 0 + ? gasButtonInfo[activeButtonIndex].labelKey + : 'custom' + return { amount: getSendAmount(state), data: getSendHexData(state), @@ -50,6 +61,7 @@ function mapStateToProps (state) { tokenBalance: getTokenBalance(state), unapprovedTxs: getUnapprovedTxs(state), sendErrors: getSendErrors(state), + gasChangedLabel, } } diff --git a/ui/app/components/app/send/send-footer/send-footer.scss b/ui/app/pages/send/send-footer/send-footer.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-footer/send-footer.scss +++ b/ui/app/pages/send/send-footer/send-footer.scss diff --git a/ui/app/components/app/send/send-footer/send-footer.selectors.js b/ui/app/pages/send/send-footer/send-footer.selectors.js index e20addfdc..e20addfdc 100644 --- a/ui/app/components/app/send/send-footer/send-footer.selectors.js +++ b/ui/app/pages/send/send-footer/send-footer.selectors.js diff --git a/ui/app/components/app/send/send-footer/send-footer.utils.js b/ui/app/pages/send/send-footer/send-footer.utils.js index abb2ebc77..91ac29014 100644 --- a/ui/app/components/app/send/send-footer/send-footer.utils.js +++ b/ui/app/pages/send/send-footer/send-footer.utils.js @@ -38,6 +38,7 @@ function constructUpdatedTx ({ }) { const unapprovedTx = unapprovedTxs[editingTransactionId] const txParamsData = unapprovedTx.txParams.data ? unapprovedTx.txParams.data : data + const editingTx = { ...unapprovedTx, txParams: Object.assign( diff --git a/ui/app/components/app/send/send-footer/tests/send-footer-component.test.js b/ui/app/pages/send/send-footer/tests/send-footer-component.test.js index 6683ca8c0..56fc95df2 100644 --- a/ui/app/components/app/send/send-footer/tests/send-footer-component.test.js +++ b/ui/app/pages/send/send-footer/tests/send-footer-component.test.js @@ -2,10 +2,10 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' -import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../../helpers/constants/routes' +import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../helpers/constants/routes' import SendFooter from '../send-footer.component.js' -import PageContainerFooter from '../../../../ui/page-container/page-container-footer' +import PageContainerFooter from '../../../../components/ui/page-container/page-container-footer' const propsMethodSpies = { addToAddressBookIfNew: sinon.spy(), diff --git a/ui/app/components/app/send/send-footer/tests/send-footer-container.test.js b/ui/app/pages/send/send-footer/tests/send-footer-container.test.js index 878b0aa19..118ebf356 100644 --- a/ui/app/components/app/send/send-footer/tests/send-footer-container.test.js +++ b/ui/app/pages/send/send-footer/tests/send-footer-container.test.js @@ -28,7 +28,7 @@ proxyquire('../send-footer.container.js', { return () => ({}) }, }, - '../../../../store/actions': actionSpies, + '../../../store/actions': actionSpies, '../send.selectors': { getGasLimit: (s) => `mockGasLimit:${s}`, getGasPrice: (s) => `mockGasPrice:${s}`, @@ -46,6 +46,10 @@ proxyquire('../send-footer.container.js', { }, './send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` }, './send-footer.utils': utilsStubs, + '../../../selectors/custom-gas': { + getRenderableEstimateDataForSmallButtonsFromGWEI: (s) => ([{ labelKey: `mockLabel:${s}` }]), + getDefaultActiveButtonIndex: () => 0, + }, }) describe('send-footer container', () => { @@ -68,6 +72,7 @@ describe('send-footer container', () => { tokenBalance: 'mockTokenBalance:mockState', unapprovedTxs: 'mockUnapprovedTxs:mockState', sendErrors: 'mockSendErrors:mockState', + gasChangedLabel: 'mockLabel:mockState', }) }) diff --git a/ui/app/components/app/send/send-footer/tests/send-footer-selectors.test.js b/ui/app/pages/send/send-footer/tests/send-footer-selectors.test.js index 8de032f57..8de032f57 100644 --- a/ui/app/components/app/send/send-footer/tests/send-footer-selectors.test.js +++ b/ui/app/pages/send/send-footer/tests/send-footer-selectors.test.js diff --git a/ui/app/components/app/send/send-footer/tests/send-footer-utils.test.js b/ui/app/pages/send/send-footer/tests/send-footer-utils.test.js index 28ff0c891..f4705e691 100644 --- a/ui/app/components/app/send/send-footer/tests/send-footer-utils.test.js +++ b/ui/app/pages/send/send-footer/tests/send-footer-utils.test.js @@ -149,7 +149,6 @@ describe('send-footer utils', () => { }, }, }) - assert.deepEqual(result, { unapprovedTxParam: 'someOtherParam', txParams: { diff --git a/ui/app/components/app/send/send-header/README.md b/ui/app/pages/send/send-header/README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send-header/README.md +++ b/ui/app/pages/send/send-header/README.md diff --git a/ui/app/components/app/send/send-header/index.js b/ui/app/pages/send/send-header/index.js index 0b17f0b7d..0b17f0b7d 100644 --- a/ui/app/components/app/send/send-header/index.js +++ b/ui/app/pages/send/send-header/index.js diff --git a/ui/app/components/app/send/send-header/send-header.component.js b/ui/app/pages/send/send-header/send-header.component.js index f216954ef..76e35494a 100644 --- a/ui/app/components/app/send/send-header/send-header.component.js +++ b/ui/app/pages/send/send-header/send-header.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import PageContainerHeader from '../../../ui/page-container/page-container-header' -import { DEFAULT_ROUTE } from '../../../../helpers/constants/routes' +import PageContainerHeader from '../../../components/ui/page-container/page-container-header' +import { DEFAULT_ROUTE } from '../../../helpers/constants/routes' export default class SendHeader extends Component { @@ -25,7 +25,6 @@ export default class SendHeader extends Component { return ( <PageContainerHeader onClose={() => this.onClose()} - subtitle={this.context.t(...this.props.subtitleParams)} title={this.context.t(this.props.titleKey)} /> ) diff --git a/ui/app/components/app/send/send-header/send-header.container.js b/ui/app/pages/send/send-header/send-header.container.js index ce53fba9a..1a9c5e9c0 100644 --- a/ui/app/components/app/send/send-header/send-header.container.js +++ b/ui/app/pages/send/send-header/send-header.container.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux' -import { clearSend } from '../../../../store/actions' +import { clearSend } from '../../../store/actions' import SendHeader from './send-header.component' import { getSubtitleParams, getTitleKey } from './send-header.selectors' diff --git a/ui/app/components/app/send/send-header/send-header.selectors.js b/ui/app/pages/send/send-header/send-header.selectors.js index d7c9d3766..d7c9d3766 100644 --- a/ui/app/components/app/send/send-header/send-header.selectors.js +++ b/ui/app/pages/send/send-header/send-header.selectors.js diff --git a/ui/app/components/app/send/send-header/tests/send-header-component.test.js b/ui/app/pages/send/send-header/tests/send-header-component.test.js index db2ee8967..91ac7e343 100644 --- a/ui/app/components/app/send/send-header/tests/send-header-component.test.js +++ b/ui/app/pages/send/send-header/tests/send-header-component.test.js @@ -2,10 +2,10 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' -import { DEFAULT_ROUTE } from '../../../../../helpers/constants/routes' +import { DEFAULT_ROUTE } from '../../../../helpers/constants/routes' import SendHeader from '../send-header.component.js' -import PageContainerHeader from '../../../../ui/page-container/page-container-header' +import PageContainerHeader from '../../../../components/ui/page-container/page-container-header' const propsMethodSpies = { clearSend: sinon.spy(), @@ -57,10 +57,8 @@ describe('SendHeader Component', function () { it('should pass the correct props to PageContainerHeader', () => { const { onClose, - subtitle, title, } = wrapper.find(PageContainerHeader).props() - assert.equal(subtitle, 'mockSubtitleKeymockVal') assert.equal(title, 'mockTitleKey') assert.equal(SendHeader.prototype.onClose.callCount, 0) onClose() diff --git a/ui/app/components/app/send/send-header/tests/send-header-container.test.js b/ui/app/pages/send/send-header/tests/send-header-container.test.js index 634c3424b..fdad8aab3 100644 --- a/ui/app/components/app/send/send-header/tests/send-header-container.test.js +++ b/ui/app/pages/send/send-header/tests/send-header-container.test.js @@ -17,7 +17,7 @@ proxyquire('../send-header.container.js', { return () => ({}) }, }, - '../../../../store/actions': actionSpies, + '../../../store/actions': actionSpies, './send-header.selectors': { getTitleKey: (s) => `mockTitleKey:${s}`, getSubtitleParams: (s) => `mockSubtitleParams:${s}`, diff --git a/ui/app/components/app/send/send-header/tests/send-header-selectors.test.js b/ui/app/pages/send/send-header/tests/send-header-selectors.test.js index e0c6a3ab3..e0c6a3ab3 100644 --- a/ui/app/components/app/send/send-header/tests/send-header-selectors.test.js +++ b/ui/app/pages/send/send-header/tests/send-header-selectors.test.js diff --git a/ui/app/components/app/send/send.component.js b/ui/app/pages/send/send.component.js index a38b681b0..5f0c9c9f2 100644 --- a/ui/app/components/app/send/send.component.js +++ b/ui/app/pages/send/send.component.js @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import PersistentForm from '../../../../lib/persistent-form' +import PersistentForm from '../../../lib/persistent-form' import { getAmountErrorObject, getGasFeeErrorObject, @@ -112,6 +112,7 @@ export default class SendTransactionScreen extends PersistentForm { gasTotal: prevGasTotal, tokenBalance: prevTokenBalance, network: prevNetwork, + selectedToken: prevSelectedToken, } = prevProps const uninitialized = [prevBalance, prevGasTotal].every(n => n === null) @@ -161,6 +162,13 @@ export default class SendTransactionScreen extends PersistentForm { this.updateGas() } } + + const prevTokenAddress = prevSelectedToken && prevSelectedToken.address + const selectedTokenAddress = selectedToken && selectedToken.address + + if (selectedTokenAddress && prevTokenAddress !== selectedTokenAddress) { + this.updateSendToken() + } } componentDidMount () { @@ -171,18 +179,7 @@ export default class SendTransactionScreen extends PersistentForm { } componentWillMount () { - const { - from: { address }, - selectedToken, - tokenContract, - updateSendTokenBalance, - } = this.props - - updateSendTokenBalance({ - selectedToken, - tokenContract, - address, - }) + this.updateSendToken() // Show QR Scanner modal if ?scan=true if (window.location.search === '?scan=true') { @@ -199,6 +196,21 @@ export default class SendTransactionScreen extends PersistentForm { this.props.resetSendState() } + updateSendToken () { + const { + from: { address }, + selectedToken, + tokenContract, + updateSendTokenBalance, + } = this.props + + updateSendTokenBalance({ + selectedToken, + tokenContract, + address, + }) + } + render () { const { history, showHexData } = this.props diff --git a/ui/app/components/app/send/send.constants.js b/ui/app/pages/send/send.constants.js index 36549038e..d3fa38d10 100644 --- a/ui/app/components/app/send/send.constants.js +++ b/ui/app/pages/send/send.constants.js @@ -1,5 +1,5 @@ const ethUtil = require('ethereumjs-util') -const { conversionUtil, multiplyCurrencies } = require('../../../helpers/utils/conversion-util') +const { conversionUtil, multiplyCurrencies } = require('../../helpers/utils/conversion-util') const MIN_GAS_PRICE_DEC = '0' const MIN_GAS_PRICE_HEX = (parseInt(MIN_GAS_PRICE_DEC)).toString(16) diff --git a/ui/app/components/app/send/send.container.js b/ui/app/pages/send/send.container.js index e65463b93..69adbb765 100644 --- a/ui/app/components/app/send/send.container.js +++ b/ui/app/pages/send/send.container.js @@ -2,6 +2,10 @@ import { connect } from 'react-redux' import SendEther from './send.component' import { withRouter } from 'react-router-dom' import { compose } from 'recompose' +const { + getSelectedAddress, +} = require('../../selectors/selectors') + import { getAmountConversionRate, getBlockGasLimit, @@ -12,7 +16,6 @@ import { getGasTotal, getPrimaryCurrency, getRecentBlocks, - getSelectedAddress, getSelectedToken, getSelectedTokenContract, getSelectedTokenToFiatRate, @@ -31,21 +34,21 @@ import { setGasTotal, showQrScanner, qrCodeDetected, -} from '../../../store/actions' +} from '../../store/actions' import { resetSendState, updateSendErrors, -} from '../../../ducks/send/send.duck' +} from '../../ducks/send/send.duck' import { fetchBasicGasEstimates, -} from '../../../ducks/gas/gas.duck' +} from '../../ducks/gas/gas.duck' import { calcGasTotal, } from './send.utils.js' import { SEND_ROUTE, -} from '../../../helpers/constants/routes' +} from '../../helpers/constants/routes' module.exports = compose( withRouter, diff --git a/ui/app/components/app/send/send.scss b/ui/app/pages/send/send.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/app/send/send.scss +++ b/ui/app/pages/send/send.scss diff --git a/ui/app/components/app/send/send.selectors.js b/ui/app/pages/send/send.selectors.js index 2ec677ad1..d4035df28 100644 --- a/ui/app/components/app/send/send.selectors.js +++ b/ui/app/pages/send/send.selectors.js @@ -1,18 +1,19 @@ -const { valuesFor } = require('../../../helpers/utils/util') +const { valuesFor } = require('../../helpers/utils/util') const abi = require('human-standard-token-abi') const { multiplyCurrencies, -} = require('../../../helpers/utils/conversion-util') +} = require('../../helpers/utils/conversion-util') const { getMetaMaskAccounts, -} = require('../../../selectors/selectors') + getSelectedAddress, +} = require('../../selectors/selectors') const { estimateGasPriceFromRecentBlocks, calcGasTotal, } = require('./send.utils') import { getFastPriceEstimateInHexWEI, -} from '../../../selectors/custom-gas' +} from '../../selectors/custom-gas' const selectors = { accountsWithSendEtherInfoSelector, @@ -33,7 +34,6 @@ const selectors = { getPrimaryCurrency, getRecentBlocks, getSelectedAccount, - getSelectedAddress, getSelectedIdentity, getSelectedToken, getSelectedTokenContract, @@ -149,12 +149,6 @@ function getSelectedAccount (state) { return accounts[selectedAddress] } -function getSelectedAddress (state) { - const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0] - - return selectedAddress -} - function getSelectedIdentity (state) { const selectedAddress = getSelectedAddress(state) const identities = state.metamask.identities @@ -246,9 +240,7 @@ function getSendTo (state) { function getSendToAccounts (state) { const fromAccounts = accountsWithSendEtherInfoSelector(state) const addressBookAccounts = getAddressBook(state) - const allAccounts = [...fromAccounts, ...addressBookAccounts] - // TODO: figure out exactly what the below returns and put a descriptive variable name on it - return Object.entries(allAccounts).map(([key, account]) => account) + return [...fromAccounts, ...addressBookAccounts] } function getSendWarnings (state) { diff --git a/ui/app/components/app/send/send.utils.js b/ui/app/pages/send/send.utils.js index 7609d46ea..4acc174f9 100644 --- a/ui/app/components/app/send/send.utils.js +++ b/ui/app/pages/send/send.utils.js @@ -5,10 +5,10 @@ const { multiplyCurrencies, conversionGreaterThan, conversionLessThan, -} = require('../../../helpers/utils/conversion-util') +} = require('../../helpers/utils/conversion-util') const { calcTokenAmount, -} = require('../../../helpers/utils/token-util') +} = require('../../helpers/utils/token-util') const { BASE_TOKEN_GAS_COST, INSUFFICIENT_FUNDS_ERROR, diff --git a/ui/app/components/app/send/tests/send-component.test.js b/ui/app/pages/send/tests/send-component.test.js index 738c14839..81955cc1d 100644 --- a/ui/app/components/app/send/tests/send-component.test.js +++ b/ui/app/pages/send/tests/send-component.test.js @@ -3,7 +3,7 @@ import assert from 'assert' import proxyquire from 'proxyquire' import { shallow } from 'enzyme' import sinon from 'sinon' -import timeout from '../../../../../lib/test-timeout' +import timeout from '../../../../lib/test-timeout' import SendHeader from '../send-header/send-header.container' import SendContent from '../send-content/send-content.component' diff --git a/ui/app/components/app/send/tests/send-container.test.js b/ui/app/pages/send/tests/send-container.test.js index 9538b67b3..131c42f59 100644 --- a/ui/app/components/app/send/tests/send-container.test.js +++ b/ui/app/pages/send/tests/send-container.test.js @@ -24,7 +24,7 @@ proxyquire('../send.container.js', { }, }, 'react-router-dom': { withRouter: () => {} }, - 'recompose': { compose: (arg1, arg2) => () => arg2() }, + 'recompose': { compose: (_, arg2) => () => arg2() }, './send.selectors': { getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, getBlockGasLimit: (s) => `mockBlockGasLimit:${s}`, @@ -35,7 +35,6 @@ proxyquire('../send.container.js', { getGasTotal: (s) => `mockGasTotal:${s}`, getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`, getRecentBlocks: (s) => `mockRecentBlocks:${s}`, - getSelectedAddress: (s) => `mockSelectedAddress:${s}`, getSelectedToken: (s) => `mockSelectedToken:${s}`, getSelectedTokenContract: (s) => `mockTokenContract:${s}`, getSelectedTokenToFiatRate: (s) => `mockTokenToFiatRate:${s}`, @@ -47,11 +46,15 @@ proxyquire('../send.container.js', { getTokenBalance: (s) => `mockTokenBalance:${s}`, getQrCodeData: (s) => `mockQrCodeData:${s}`, }, - '../../../store/actions': actionSpies, - '../../../ducks/send/send.duck': duckActionSpies, + '../../selectors/selectors': { + getSelectedAddress: (s) => `mockSelectedAddress:${s}`, + }, + '../../store/actions': actionSpies, + '../../ducks/send/send.duck': duckActionSpies, './send.utils.js': { calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice, }, + }) describe('send container', () => { diff --git a/ui/app/components/app/send/tests/send-selectors-test-data.js b/ui/app/pages/send/tests/send-selectors-test-data.js index cff26a191..cff26a191 100644 --- a/ui/app/components/app/send/tests/send-selectors-test-data.js +++ b/ui/app/pages/send/tests/send-selectors-test-data.js diff --git a/ui/app/components/app/send/tests/send-selectors.test.js b/ui/app/pages/send/tests/send-selectors.test.js index cdc86fe59..ccc126795 100644 --- a/ui/app/components/app/send/tests/send-selectors.test.js +++ b/ui/app/pages/send/tests/send-selectors.test.js @@ -20,7 +20,6 @@ const { getPrimaryCurrency, getRecentBlocks, getSelectedAccount, - getSelectedAddress, getSelectedIdentity, getSelectedToken, getSelectedTokenContract, @@ -274,14 +273,6 @@ describe('send selectors', () => { }) }) - describe('getSelectedAddress()', () => { - it('should', () => { - assert.equal( - getSelectedAddress(mockState), - '0xd85a4b6a394794842887b8284293d69163007bbb' - ) - }) - }) describe('getSelectedIdentity()', () => { it('should return the identity object of the currently selected address', () => { diff --git a/ui/app/components/app/send/tests/send-utils.test.js b/ui/app/pages/send/tests/send-utils.test.js index fc4c6deed..bf9cba14a 100644 --- a/ui/app/components/app/send/tests/send-utils.test.js +++ b/ui/app/pages/send/tests/send-utils.test.js @@ -9,7 +9,7 @@ import { const { addCurrencies, subtractCurrencies, -} = require('../../../../helpers/utils/conversion-util') +} = require('../../../helpers/utils/conversion-util') const { INSUFFICIENT_FUNDS_ERROR, @@ -17,12 +17,12 @@ const { } = require('../send.constants') const stubs = { - addCurrencies: sinon.stub().callsFake((a, b, obj) => { + addCurrencies: sinon.stub().callsFake((a, b) => { 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)), + conversionUtil: sinon.stub().callsFake((val) => parseInt(val, 16)), conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value), multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`), calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d), @@ -32,7 +32,7 @@ const stubs = { } const sendUtils = proxyquire('../send.utils.js', { - '../../../helpers/utils/conversion-util': { + '../../helpers/utils/conversion-util': { addCurrencies: stubs.addCurrencies, conversionUtil: stubs.conversionUtil, conversionGTE: stubs.conversionGTE, @@ -40,7 +40,7 @@ const sendUtils = proxyquire('../send.utils.js', { conversionGreaterThan: stubs.conversionGreaterThan, conversionLessThan: stubs.conversionLessThan, }, - '../../../helpers/utils/token-util': { calcTokenAmount: stubs.calcTokenAmount }, + '../../helpers/utils/token-util': { calcTokenAmount: stubs.calcTokenAmount }, 'ethereumjs-abi': { rawEncode: stubs.rawEncode, }, diff --git a/ui/app/components/app/send/to-autocomplete.component.js b/ui/app/pages/send/to-autocomplete.component.js index 183967c58..183967c58 100644 --- a/ui/app/components/app/send/to-autocomplete.component.js +++ b/ui/app/pages/send/to-autocomplete.component.js diff --git a/ui/app/components/app/send/to-autocomplete/index.js b/ui/app/pages/send/to-autocomplete/index.js index 244d301d1..244d301d1 100644 --- a/ui/app/components/app/send/to-autocomplete/index.js +++ b/ui/app/pages/send/to-autocomplete/index.js diff --git a/ui/app/components/app/send/to-autocomplete/to-autocomplete.js b/ui/app/pages/send/to-autocomplete/to-autocomplete.js index d3db8cb59..11f86acf3 100644 --- a/ui/app/components/app/send/to-autocomplete/to-autocomplete.js +++ b/ui/app/pages/send/to-autocomplete/to-autocomplete.js @@ -1,11 +1,12 @@ 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 -const Tooltip = require('../../../ui/tooltip') -const checksumAddress = require('../../../../helpers/utils/util').checksumAddress +const Tooltip = require('../../../components/ui/tooltip') +const checksumAddress = require('../../../helpers/utils/util').checksumAddress ToAutoComplete.contextTypes = { t: PropTypes.func, @@ -84,7 +85,7 @@ ToAutoComplete.prototype.handleInputEvent = function (event = {}, cb) { cb && cb(event.target.value) } -ToAutoComplete.prototype.componentDidUpdate = function (nextProps, nextState) { +ToAutoComplete.prototype.componentDidUpdate = function (nextProps) { if (this.props.to !== nextProps.to) { this.handleInputEvent() } @@ -93,24 +94,34 @@ ToAutoComplete.prototype.componentDidUpdate = function (nextProps, nextState) { 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', diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js index d1cad1746..3d27fe349 100644 --- a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js @@ -24,6 +24,8 @@ export default class AdvancedTab extends PureComponent { setAdvancedInlineGasFeatureFlag: PropTypes.func, advancedInlineGas: PropTypes.bool, showFiatInTestnets: PropTypes.bool, + autoLogoutTimeLimit: PropTypes.number, + setAutoLogoutTimeLimit: PropTypes.func.isRequired, setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired, } @@ -49,7 +51,7 @@ export default class AdvancedTab extends PureComponent { <TextField type="text" id="new-rpc" - placeholder={t('rpcURL')} + placeholder={t('rpcUrl')} value={newRpc} onChange={e => this.setState({ newRpc: e.target.value })} onKeyPress={e => { @@ -189,7 +191,7 @@ export default class AdvancedTab extends PureComponent { <div className="settings-page__content-item"> <div className="settings-page__content-item-col"> <Button - type="primary" + type="secondary" large onClick={event => { event.preventDefault() @@ -219,7 +221,7 @@ export default class AdvancedTab extends PureComponent { <div className="settings-page__content-item"> <div className="settings-page__content-item-col"> <Button - type="primary" + type="secondary" large onClick={() => { window.logStateString((err, result) => { @@ -251,7 +253,7 @@ export default class AdvancedTab extends PureComponent { <div className="settings-page__content-item"> <div className="settings-page__content-item-col"> <Button - type="secondary" + type="warning" large className="settings-tab__button--orange" onClick={event => { @@ -355,6 +357,48 @@ export default class AdvancedTab extends PureComponent { ) } + renderAutoLogoutTimeLimit () { + const { t } = this.context + const { + autoLogoutTimeLimit, + setAutoLogoutTimeLimit, + } = this.props + + return ( + <div className="settings-page__content-row"> + <div className="settings-page__content-item"> + <span>{ t('autoLogoutTimeLimit') }</span> + <div className="settings-page__content-description"> + { t('autoLogoutTimeLimitDescription') } + </div> + </div> + <div className="settings-page__content-item"> + <div className="settings-page__content-item-col"> + <TextField + type="number" + id="autoTimeout" + placeholder="5" + value={this.state.autoLogoutTimeLimit} + defaultValue={autoLogoutTimeLimit} + onChange={e => this.setState({ autoLogoutTimeLimit: Math.max(Number(e.target.value), 0) })} + fullWidth + margin="dense" + min={0} + /> + <button + className="button btn-primary settings-tab__rpc-save-button" + onClick={() => { + setAutoLogoutTimeLimit(this.state.autoLogoutTimeLimit) + }} + > + { t('save') } + </button> + </div> + </div> + </div> + ) + } + renderContent () { const { warning } = this.props @@ -368,6 +412,7 @@ export default class AdvancedTab extends PureComponent { { this.renderAdvancedGasInputInline() } { this.renderHexDataOptIn() } { this.renderShowConversionInTestnets() } + { this.renderAutoLogoutTimeLimit() } </div> ) } diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js index 69d7e07e6..bcac55f5e 100644 --- a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js @@ -8,10 +8,11 @@ import { setFeatureFlag, showModal, setShowFiatConversionOnTestnetsPreference, + setAutoLogoutTimeLimit, } from '../../../store/actions' import {preferencesSelector} from '../../../selectors/selectors' -const mapStateToProps = state => { +export const mapStateToProps = state => { const { appState: { warning }, metamask } = state const { featureFlags: { @@ -19,17 +20,18 @@ const mapStateToProps = state => { advancedInlineGas, } = {}, } = metamask - const { showFiatInTestnets } = preferencesSelector(state) + const { showFiatInTestnets, autoLogoutTimeLimit } = preferencesSelector(state) return { warning, sendHexData, advancedInlineGas, showFiatInTestnets, + autoLogoutTimeLimit, } } -const mapDispatchToProps = dispatch => { +export const mapDispatchToProps = dispatch => { return { setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)), setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname)), @@ -39,6 +41,9 @@ const mapDispatchToProps = dispatch => { setShowFiatConversionOnTestnetsPreference: value => { return dispatch(setShowFiatConversionOnTestnetsPreference(value)) }, + setAutoLogoutTimeLimit: value => { + return dispatch(setAutoLogoutTimeLimit(value)) + }, } } diff --git a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js new file mode 100644 index 000000000..f81329533 --- /dev/null +++ b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js @@ -0,0 +1,44 @@ +import React from 'react' +import assert from 'assert' +import sinon from 'sinon' +import { shallow } from 'enzyme' +import AdvancedTab from '../advanced-tab.component' +import TextField from '../../../../components/ui/text-field' + +describe('AdvancedTab Component', () => { + it('should render correctly', () => { + const root = shallow( + <AdvancedTab />, + { + context: { + t: s => `_${s}`, + }, + } + ) + + assert.equal(root.find('.settings-page__content-row').length, 8) + }) + + it('should update autoLogoutTimeLimit', () => { + const setAutoLogoutTimeLimitSpy = sinon.spy() + const root = shallow( + <AdvancedTab + setAutoLogoutTimeLimit={setAutoLogoutTimeLimitSpy} + />, + { + context: { + t: s => `_${s}`, + }, + } + ) + + const autoTimeout = root.find('.settings-page__content-row').last() + const textField = autoTimeout.find(TextField) + + textField.props().onChange({ target: { value: 1440 } }) + assert.equal(root.state().autoLogoutTimeLimit, 1440) + + autoTimeout.find('button').simulate('click') + assert.equal(setAutoLogoutTimeLimitSpy.args[0][0], 1440) + }) +}) diff --git a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js new file mode 100644 index 000000000..62122073d --- /dev/null +++ b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-container.test.js @@ -0,0 +1,46 @@ +import assert from 'assert' +import { mapStateToProps, mapDispatchToProps } from '../advanced-tab.container' + +const defaultState = { + appState: { + warning: null, + }, + metamask: { + featureFlags: { + sendHexData: false, + advancedInlineGas: false, + }, + preferences: { + autoLogoutTimeLimit: 0, + showFiatInTestnets: false, + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, +} + +describe('AdvancedTab Container', () => { + it('should map state to props correctly', () => { + const props = mapStateToProps(defaultState) + const expected = { + warning: null, + sendHexData: false, + advancedInlineGas: false, + showFiatInTestnets: false, + autoLogoutTimeLimit: 0, + } + + assert.deepEqual(props, expected) + }) + + it('should map dispatch to props correctly', () => { + const props = mapDispatchToProps(() => 'mockDispatch') + + assert.ok(typeof props.setHexDataFeatureFlag === 'function') + assert.ok(typeof props.setRpcTarget === 'function') + assert.ok(typeof props.displayWarning === 'function') + assert.ok(typeof props.showResetAccountConfirmationModal === 'function') + assert.ok(typeof props.setAdvancedInlineGasFeatureFlag === 'function') + assert.ok(typeof props.setShowFiatConversionOnTestnetsPreference === 'function') + assert.ok(typeof props.setAutoLogoutTimeLimit === 'function') + }) +}) diff --git a/ui/app/pages/settings/index.scss b/ui/app/pages/settings/index.scss index 52208dc85..66959ba93 100644 --- a/ui/app/pages/settings/index.scss +++ b/ui/app/pages/settings/index.scss @@ -1,5 +1,7 @@ @import 'info-tab/index'; +@import 'networks-tab/index'; + @import 'settings-tab/index'; .settings-page { @@ -13,7 +15,6 @@ flex-flow: row nowrap; padding: 12px 24px; align-items: center; - border-bottom: 1px solid $alto; flex: 0 0 auto; &__title { @@ -22,6 +23,45 @@ } } + &__subheader { + padding: 16px 4px; + font-size: 20px; + border-bottom: 1px solid $alto; + margin-right: 24px; + + @media screen and (max-width: 575px) { + display: none; + } + } + + &__sub-header { + height: 72px; + border-bottom: 1px solid #D8D8D8; + display: flex; + justify-content: space-between; + align-items: center; + + @media screen and (max-width: 575px) { + height: 69px; + position: relative; + text-align: center; + } + } + + &__sub-header-text { + font-family: Roboto; + font-style: normal; + font-weight: normal; + font-size: 24px; + line-height: 24px; + color: black; + + @media screen and (max-width: 575px) { + font-size: 16px; + width: 100%; + } + } + &__back-button { display: none; @@ -49,8 +89,9 @@ &__content { display: flex; flex-flow: row nowrap; - height: auto; + height: 100%; overflow: auto; + border-top: 1px solid #D8D8D8; &__tabs { display: flex; @@ -58,9 +99,15 @@ flex: 1 1 auto; @media screen and (min-width: 576px) { - flex: 0 0 32%; + flex: 0 0 40%; max-width: 210px; - border-right: 1px solid $alto; + padding-top: 8px; + } + + .tab-bar__tab { + @media screen and (min-width: 576px) { + padding: 16px 24px 0; + } } } @@ -76,6 +123,10 @@ &__body { padding: 12px 24px; + + @media screen and (min-width: 576px) { + padding: 12px; + } } &__content-row { @@ -89,7 +140,6 @@ min-width: 0; display: flex; flex-direction: column; - padding: 0 5px; min-height: 71px; @media screen and (max-width: 575px) { diff --git a/ui/app/pages/settings/info-tab/index.scss b/ui/app/pages/settings/info-tab/index.scss index 43ad6f652..9cc7e21b2 100644 --- a/ui/app/pages/settings/info-tab/index.scss +++ b/ui/app/pages/settings/info-tab/index.scss @@ -30,7 +30,7 @@ } &__link-text { - color: $curious-blue; + @extend %link; } &__version-number { diff --git a/ui/app/pages/settings/networks-tab/index.js b/ui/app/pages/settings/networks-tab/index.js new file mode 100644 index 000000000..362004498 --- /dev/null +++ b/ui/app/pages/settings/networks-tab/index.js @@ -0,0 +1 @@ +export { default } from './networks-tab.container' diff --git a/ui/app/pages/settings/networks-tab/index.scss b/ui/app/pages/settings/networks-tab/index.scss new file mode 100644 index 000000000..b0020437d --- /dev/null +++ b/ui/app/pages/settings/networks-tab/index.scss @@ -0,0 +1,200 @@ +.networks-tab { + &__content { + margin-top: 24px; + display: flex; + height: 100%; + max-width: 739px; + justify-content: space-between; + + @media screen and (max-width: 575px) { + margin-top: 0px; + } + } + + &__body { + padding: 12px 24px; + height: 100%; + display: flex; + flex-direction: column; + + @media screen and (max-width: 575px) { + padding: 0; + } + } + + &__back-button { + display: none; + + @media screen and (max-width: 575px) { + display: block; + background-image: url('/images/caret-left-black.svg'); + width: 18px; + height: 18px; + opacity: .5; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + margin-right: 16px; + cursor: pointer; + position: absolute; + margin-left: 10px; + } + } + + &__network-form { + flex: 0.5 0 auto; + max-width: 343px; + max-height: 465px; + display: flex; + flex-direction: column; + justify-content: space-between; + + .page-container__footer { + border-top: none; + + @media screen and (max-width: 575px) { + width: 93%; + } + + header { + padding: 10px 0px; + } + } + + @media screen and (max-width: 575px) { + display: flex; + flex: auto; + max-width: 100%; + max-height: 100%; + align-items: center; + width: 100%; + margin-top: 10px; + } + } + + &__network-form-row { + @media screen and (max-width: 575px) { + display: flex; + flex-direction: column; + width: 93%; + } + } + + &__network-form-label { + font-family: Roboto; + font-style: normal; + font-weight: normal; + font-size: 14px; + line-height: 20px; + color: #000000; + } + + &__networks-list { + flex: 0.5 0 auto; + max-width: 343px; + + @media screen and (max-width: 575px) { + max-width: 100vw; + width: 100vw; + overflow-y: scroll; + } + } + + &__add-network-button-wrapper { + display: none; + + @media screen and (max-width: 575px) { + display: flex; + padding-top: 19px; + padding-bottom: 23px; + justify-content: center; + align-items: center; + border-top: 1px solid #D8D8D8; + + .button { + width: 178px; + } + } + } + + &__add-network-header-button-wrapper { + padding-top: 15px; + padding-bottom: 21px; + justify-content: center; + + .button { + width: 178px; + } + + @media screen and (max-width: 575px) { + display: none; + } + } + + &__networks-list--selection { + @media screen and (max-width: 575px) { + display: none; + } + } + + &__networks-list-item { + display: flex; + padding: 13px 0px 13px 17px; + position: relative; + + .menu-icon-circle { + &:hover { + cursor: pointer; + } + } + + @media screen and (max-width: 575px) { + padding: 20px 23px 21px 17px; + border-bottom: 1px solid #D8D8D8; + } + } + + &__networks-list-item:last-of-type { + @media screen and (max-width: 575px) { + border-bottom: none; + } + } + + &__networks-list-name { + margin-left: 11px; + font-family: Roboto; + font-style: normal; + font-weight: normal; + font-size: 16px; + line-height: 23px; + color: #6A737D; + + &:hover { + cursor: pointer; + } + } + + &__networks-list-arrow { + display: none; + + @media screen and (max-width: 575px) { + display: block; + background-image: url('/images/caret-right.svg'); + width: 24px; + height: 24px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + right: 10px; + cursor: pointer; + position: absolute; + width: 24px; + height: 24px; + } + } + + &__networks-list-name--selected { + font-weight: bold; + color: #000000; + } +}
\ No newline at end of file diff --git a/ui/app/pages/settings/networks-tab/network-form/index.js b/ui/app/pages/settings/networks-tab/network-form/index.js new file mode 100644 index 000000000..89d9de42b --- /dev/null +++ b/ui/app/pages/settings/networks-tab/network-form/index.js @@ -0,0 +1 @@ +export { default } from './network-form.component' diff --git a/ui/app/pages/settings/networks-tab/network-form/network-form.component.js b/ui/app/pages/settings/networks-tab/network-form/network-form.component.js new file mode 100644 index 000000000..5e455b65e --- /dev/null +++ b/ui/app/pages/settings/networks-tab/network-form/network-form.component.js @@ -0,0 +1,225 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import validUrl from 'valid-url' +import PageContainerFooter from '../../../../components/ui/page-container/page-container-footer' +import TextField from '../../../../components/ui/text-field' + +export default class NetworksTab extends PureComponent { + static contextTypes = { + t: PropTypes.func.isRequired, + metricsEvent: PropTypes.func.isRequired, + } + + static propTypes = { + editRpc: PropTypes.func.isRequired, + rpcUrl: PropTypes.string, + chainId: PropTypes.string, + ticker: PropTypes.string, + viewOnly: PropTypes.bool, + networkName: PropTypes.string, + onClear: PropTypes.func.isRequired, + setRpcTarget: PropTypes.func.isRequired, + networksTabIsInAddMode: PropTypes.bool, + blockExplorerUrl: PropTypes.string, + rpcPrefs: PropTypes.object, + } + + state = { + rpcUrl: this.props.rpcUrl, + chainId: this.props.chainId, + ticker: this.props.ticker, + networkName: this.props.networkName, + blockExplorerUrl: this.props.blockExplorerUrl, + errors: {}, + } + + componentDidUpdate (prevProps) { + const { rpcUrl: prevRpcUrl, networksTabIsInAddMode: prevAddMode } = prevProps + const { + rpcUrl, + chainId, + ticker, + networkName, + networksTabIsInAddMode, + blockExplorerUrl, + } = this.props + + if (!prevAddMode && networksTabIsInAddMode) { + this.setState({ + rpcUrl: '', + chainId: '', + ticker: '', + networkName: '', + blockExplorerUrl: '', + errors: {}, + }) + } else if (prevRpcUrl !== rpcUrl) { + this.setState({ rpcUrl, chainId, ticker, networkName, blockExplorerUrl, errors: {} }) + } + } + + componentWillUnmount () { + this.props.onClear() + this.setState({ + rpcUrl: '', + chainId: '', + ticker: '', + networkName: '', + blockExplorerUrl: '', + errors: {}, + }) + } + + stateIsUnchanged () { + const { + rpcUrl, + chainId, + ticker, + networkName, + blockExplorerUrl, + } = this.props + + const { + rpcUrl: stateRpcUrl, + chainId: stateChainId, + ticker: stateTicker, + networkName: stateNetworkName, + blockExplorerUrl: stateBlockExplorerUrl, + } = this.state + + return ( + stateRpcUrl === rpcUrl && + stateChainId === chainId && + stateTicker === ticker && + stateNetworkName === networkName && + stateBlockExplorerUrl === blockExplorerUrl + ) + } + + renderFormTextField (fieldKey, textFieldId, onChange, value, optionalTextFieldKey) { + const { errors } = this.state + const { viewOnly } = this.props + + return ( + <div className="networks-tab__network-form-row"> + <div className="networks-tab__network-form-label">{this.context.t(optionalTextFieldKey || fieldKey)}</div> + <TextField + type="text" + id={textFieldId} + onChange={onChange} + fullWidth + margin="dense" + value={value} + disabled={viewOnly} + error={errors[fieldKey]} + /> + </div> + ) + } + + setStateWithValue = (stateKey, validator) => { + return (e) => { + validator && validator(e.target.value, stateKey) + this.setState({ [stateKey]: e.target.value }) + } + } + + setErrorTo = (errorKey, errorVal) => { + this.setState({ + errors: { + ...this.state.errors, + [errorKey]: errorVal, + }, + }) + } + + validateChainId = (chainId) => { + this.setErrorTo('chainId', !!chainId && Number.isNaN(parseInt(chainId)) + ? `${this.context.t('invalidInput')} chainId` + : '' + ) + } + + validateUrl = (url, stateKey) => { + if (validUrl.isWebUri(url)) { + this.setErrorTo(stateKey, '') + } else { + const appendedRpc = `http://${url}` + const validWhenAppended = validUrl.isWebUri(appendedRpc) && !url.match(/^https?:\/\/$/) + + this.setErrorTo(stateKey, this.context.t(validWhenAppended ? 'uriErrorMsg' : 'invalidRPC')) + } + } + + render () { + const { setRpcTarget, viewOnly, rpcUrl: propsRpcUrl, editRpc, rpcPrefs = {} } = this.props + const { + networkName, + rpcUrl, + chainId, + ticker, + blockExplorerUrl, + errors, + } = this.state + + + return ( + <div className="networks-tab__network-form"> + {this.renderFormTextField( + 'networkName', + 'network-name', + this.setStateWithValue('networkName'), + networkName, + )} + {this.renderFormTextField( + 'rpcUrl', + 'rpc-url', + this.setStateWithValue('rpcUrl', this.validateUrl), + rpcUrl, + )} + {this.renderFormTextField( + 'chainId', + 'chainId', + this.setStateWithValue('chainId', this.validateChainId), + chainId, + 'optionalChainId', + )} + {this.renderFormTextField( + 'symbol', + 'network-ticker', + this.setStateWithValue('ticker'), + ticker, + 'optionalSymbol', + )} + {this.renderFormTextField( + 'blockExplorerUrl', + 'block-explorer-url', + this.setStateWithValue('blockExplorerUrl', this.validateUrl), + blockExplorerUrl, + 'optionalBlockExplorerUrl', + )} + <PageContainerFooter + cancelText={this.context.t('cancel')} + hideCancel={true} + onSubmit={() => { + if (propsRpcUrl && rpcUrl !== propsRpcUrl) { + editRpc(propsRpcUrl, rpcUrl, chainId, ticker, networkName, { + blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl, + ...rpcPrefs, + }) + } else { + setRpcTarget(rpcUrl, chainId, ticker, networkName, { + blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl, + ...rpcPrefs, + }) + } + }} + submitText={this.context.t('save')} + submitButtonType={'confirm'} + disabled={viewOnly || this.stateIsUnchanged() || Object.values(errors).some(x => x) || !rpcUrl} + /> + </div> + ) + } + +} diff --git a/ui/app/pages/settings/networks-tab/networks-tab.component.js b/ui/app/pages/settings/networks-tab/networks-tab.component.js new file mode 100644 index 000000000..2f921a892 --- /dev/null +++ b/ui/app/pages/settings/networks-tab/networks-tab.component.js @@ -0,0 +1,214 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { SETTINGS_ROUTE } from '../../../helpers/constants/routes' +import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' +import classnames from 'classnames' +import Button from '../../../components/ui/button' +import NetworkForm from './network-form' +import NetworkDropdownIcon from '../../../components/app/dropdowns/components/network-dropdown-icon' + +export default class NetworksTab extends PureComponent { + static contextTypes = { + t: PropTypes.func.isRequired, + metricsEvent: PropTypes.func.isRequired, + } + + static propTypes = { + editRpc: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, + location: PropTypes.object.isRequired, + networkIsSelected: PropTypes.bool, + networksTabIsInAddMode: PropTypes.bool, + networksToRender: PropTypes.array.isRequired, + selectedNetwork: PropTypes.object, + setNetworksTabAddMode: PropTypes.func.isRequired, + setRpcTarget: PropTypes.func.isRequired, + setSelectedSettingsRpcUrl: PropTypes.func.isRequired, + providerUrl: PropTypes.string, + providerType: PropTypes.string, + networkDefaultedToProvider: PropTypes.bool, + } + + componentWillMount () { + this.props.setSelectedSettingsRpcUrl(null) + } + + isCurrentPath (pathname) { + return this.props.location.pathname === pathname + } + + renderSubHeader () { + const { + networkIsSelected, + setSelectedSettingsRpcUrl, + setNetworksTabAddMode, + networksTabIsInAddMode, + networkDefaultedToProvider, + } = this.props + + return ( + <div className="settings-page__sub-header"> + <div + className="networks-tab__back-button" + onClick={(networkIsSelected && !networkDefaultedToProvider) || networksTabIsInAddMode + ? () => { + setNetworksTabAddMode(false) + setSelectedSettingsRpcUrl(null) + } + : () => this.props.history.push(SETTINGS_ROUTE) + } + /> + <span className="settings-page__sub-header-text">{ this.context.t('networks') }</span> + <div className="networks-tab__add-network-header-button-wrapper"> + <Button + type="primary" + onClick={event => { + event.preventDefault() + setSelectedSettingsRpcUrl(null) + setNetworksTabAddMode(true) + }} + > + { this.context.t('addNetwork') } + </Button> + </div> + </div> + ) + } + + renderNetworkListItem (network, selectRpcUrl) { + const { + setSelectedSettingsRpcUrl, + setNetworksTabAddMode, + networkIsSelected, + providerUrl, + providerType, + networksTabIsInAddMode, + } = this.props + const { + border, + iconColor, + label, + labelKey, + rpcUrl, + providerType: currentProviderType, + } = network + + const listItemNetworkIsSelected = selectRpcUrl && selectRpcUrl === rpcUrl + const listItemUrlIsProviderUrl = rpcUrl === providerUrl + const listItemTypeIsProviderNonRpcType = providerType !== 'rpc' && currentProviderType === providerType + const listItemNetworkIsCurrentProvider = !networkIsSelected && !networksTabIsInAddMode && (listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType) + const displayNetworkListItemAsSelected = listItemNetworkIsSelected || listItemNetworkIsCurrentProvider + + return ( + <div + key={'settings-network-list-item:' + rpcUrl} + className="networks-tab__networks-list-item" + onClick={ () => { + setNetworksTabAddMode(false) + setSelectedSettingsRpcUrl(rpcUrl) + }} + > + <NetworkDropdownIcon + backgroundColor={iconColor || 'white'} + innerBorder={border} + /> + <div className={ classnames('networks-tab__networks-list-name', { + 'networks-tab__networks-list-name--selected': displayNetworkListItemAsSelected, + }) }> + { label || this.context.t(labelKey) } + </div> + <div className="networks-tab__networks-list-arrow" /> + </div> + ) + } + + renderNetworksList () { + const { networksToRender, selectedNetwork, networkIsSelected, networksTabIsInAddMode, networkDefaultedToProvider } = this.props + + return ( + <div className={classnames('networks-tab__networks-list', { + 'networks-tab__networks-list--selection': (networkIsSelected && !networkDefaultedToProvider) || networksTabIsInAddMode, + })}> + { networksToRender.map(network => this.renderNetworkListItem(network, selectedNetwork.rpcUrl)) } + </div> + ) + } + + renderNetworksTabContent () { + const { + setRpcTarget, + setSelectedSettingsRpcUrl, + setNetworksTabAddMode, + selectedNetwork: { + labelKey, + label, + rpcUrl, + chainId, + ticker, + viewOnly, + rpcPrefs, + blockExplorerUrl, + }, + networksTabIsInAddMode, + editRpc, + networkDefaultedToProvider, + } = this.props + const envIsPopup = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP + + return ( + <div className="networks-tab__content"> + {this.renderNetworksList()} + {networksTabIsInAddMode || !envIsPopup || (envIsPopup && !networkDefaultedToProvider) + ? <NetworkForm + setRpcTarget={setRpcTarget} + editRpc={editRpc} + networkName={label || labelKey && this.context.t(labelKey) || ''} + rpcUrl={rpcUrl} + chainId={chainId} + ticker={ticker} + onClear={() => { + setNetworksTabAddMode(false) + setSelectedSettingsRpcUrl(null) + }} + viewOnly={viewOnly} + networksTabIsInAddMode={networksTabIsInAddMode} + rpcPrefs={rpcPrefs} + blockExplorerUrl={blockExplorerUrl} + /> + : null + } + </div> + ) + } + + renderContent () { + const { setNetworksTabAddMode, setSelectedSettingsRpcUrl, networkIsSelected, networksTabIsInAddMode } = this.props + + return ( + <div className="networks-tab__body"> + {this.renderSubHeader()} + {this.renderNetworksTabContent()} + {!networkIsSelected && !networksTabIsInAddMode + ? <div className="networks-tab__add-network-button-wrapper"> + <Button + type="primary" + onClick={event => { + event.preventDefault() + setSelectedSettingsRpcUrl(null) + setNetworksTabAddMode(true) + }} + > + { this.context.t('addNetwork') } + </Button> + </div> + : null + } + </div> + ) + } + + render () { + return this.renderContent() + } +} diff --git a/ui/app/pages/settings/networks-tab/networks-tab.constants.js b/ui/app/pages/settings/networks-tab/networks-tab.constants.js new file mode 100644 index 000000000..d3d1a01cc --- /dev/null +++ b/ui/app/pages/settings/networks-tab/networks-tab.constants.js @@ -0,0 +1,50 @@ +const defaultNetworksData = [ + { + labelKey: 'mainnet', + iconColor: '#29B6AF', + providerType: 'mainnet', + rpcUrl: 'https://api.infura.io/v1/jsonrpc/mainnet', + chainId: '1', + ticker: 'ETH', + blockExplorerUrl: 'https://etherscan.io', + }, + { + labelKey: 'ropsten', + iconColor: '#FF4A8D', + providerType: 'ropsten', + rpcUrl: 'https://api.infura.io/v1/jsonrpc/ropsten', + chainId: '3', + ticker: 'ETH', + blockExplorerUrl: 'https://ropsten.etherscan.io', + }, + { + labelKey: 'kovan', + iconColor: '#9064FF', + providerType: 'kovan', + rpcUrl: 'https://api.infura.io/v1/jsonrpc/kovan', + chainId: '4', + ticker: 'ETH', + blockExplorerUrl: 'https://etherscan.io', + }, + { + labelKey: 'rinkeby', + iconColor: '#F6C343', + providerType: 'rinkeby', + rpcUrl: 'https://api.infura.io/v1/jsonrpc/rinkeby', + chainId: '42', + ticker: 'ETH', + blockExplorerUrl: 'https://rinkeby.etherscan.io', + }, + { + labelKey: 'localhost', + iconColor: 'white', + border: '1px solid #6A737D', + providerType: 'localhost', + rpcUrl: 'http://localhost:8545/', + blockExplorerUrl: 'https://etherscan.io', + }, +] + +export { + defaultNetworksData, +} diff --git a/ui/app/pages/settings/networks-tab/networks-tab.container.js b/ui/app/pages/settings/networks-tab/networks-tab.container.js new file mode 100644 index 000000000..a5d71f714 --- /dev/null +++ b/ui/app/pages/settings/networks-tab/networks-tab.container.js @@ -0,0 +1,77 @@ +import NetworksTab from './networks-tab.component' +import { compose } from 'recompose' +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { + setSelectedSettingsRpcUrl, + updateAndSetCustomRpc, + displayWarning, + setNetworksTabAddMode, + editRpc, +} from '../../../store/actions' +import { defaultNetworksData } from './networks-tab.constants' +const defaultNetworks = defaultNetworksData.map(network => ({ ...network, viewOnly: true })) + +const mapStateToProps = state => { + const { + frequentRpcListDetail, + provider, + } = state.metamask + const { + networksTabSelectedRpcUrl, + networksTabIsInAddMode, + } = state.appState + + const frequentRpcNetworkListDetails = frequentRpcListDetail.map(rpc => { + return { + label: rpc.nickname, + iconColor: '#6A737D', + providerType: 'rpc', + rpcUrl: rpc.rpcUrl, + chainId: rpc.chainId, + ticker: rpc.ticker, + blockExplorerUrl: rpc.rpcPrefs && rpc.rpcPrefs.blockExplorerUrl || '', + } + }) + + const networksToRender = [ ...defaultNetworks, ...frequentRpcNetworkListDetails ] + let selectedNetwork = networksToRender.find(network => network.rpcUrl === networksTabSelectedRpcUrl) || {} + const networkIsSelected = Boolean(selectedNetwork.rpcUrl) + + let networkDefaultedToProvider = false + if (!networkIsSelected && !networksTabIsInAddMode) { + selectedNetwork = networksToRender.find(network => { + return network.rpcUrl === provider.rpcTarget || network.providerType !== 'rpc' && network.providerType === provider.type + }) || {} + networkDefaultedToProvider = true + } + + return { + selectedNetwork, + networksToRender, + networkIsSelected, + networksTabIsInAddMode, + providerType: provider.type, + providerUrl: provider.rpcTarget, + networkDefaultedToProvider, + } +} + +const mapDispatchToProps = dispatch => { + return { + setSelectedSettingsRpcUrl: newRpcUrl => dispatch(setSelectedSettingsRpcUrl(newRpcUrl)), + setRpcTarget: (newRpc, chainId, ticker, nickname, rpcPrefs) => { + dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname, rpcPrefs)) + }, + displayWarning: warning => dispatch(displayWarning(warning)), + setNetworksTabAddMode: isInAddMode => dispatch(setNetworksTabAddMode(isInAddMode)), + editRpc: (oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs) => { + dispatch(editRpc(oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs)) + }, + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(NetworksTab) diff --git a/ui/app/pages/settings/security-tab/security-tab.component.js b/ui/app/pages/settings/security-tab/security-tab.component.js index 233561115..01a28bac7 100644 --- a/ui/app/pages/settings/security-tab/security-tab.component.js +++ b/ui/app/pages/settings/security-tab/security-tab.component.js @@ -39,7 +39,7 @@ export default class SecurityTab extends PureComponent { <div className="settings-page__content-item"> <div className="settings-page__content-item-col"> <Button - type="primary" + type="secondary" large onClick={() => { window.logStateString((err, result) => { @@ -73,7 +73,7 @@ export default class SecurityTab extends PureComponent { <div className="settings-page__content-item"> <div className="settings-page__content-item-col"> <Button - type="secondary" + type="warning" large className="settings-tab__button--orange" onClick={event => { @@ -101,7 +101,7 @@ export default class SecurityTab extends PureComponent { <div className="settings-page__content-item"> <div className="settings-page__content-item-col"> <Button - type="secondary" + type="danger" large onClick={event => { event.preventDefault() diff --git a/ui/app/pages/settings/settings-tab/index.scss b/ui/app/pages/settings/settings-tab/index.scss index ef32b0e4c..c1750af2c 100644 --- a/ui/app/pages/settings/settings-tab/index.scss +++ b/ui/app/pages/settings/settings-tab/index.scss @@ -6,19 +6,15 @@ } &__advanced-link { - color: $curious-blue; + @extend %small-link; padding-left: 5px; } &__rpc-save-button { align-self: flex-end; padding: 5px; - text-transform: uppercase; - color: $dusty-gray; cursor: pointer; width: 25%; - min-width: 80px; - height: 33px; } &__button--red { @@ -35,20 +31,6 @@ } } - &__button--orange { - border-color: lighten($ecstasy, 20%); - color: $ecstasy; - - &:active { - background: lighten($ecstasy, 40%); - border-color: $ecstasy; - } - - &:hover { - border-color: $ecstasy; - } - } - &__radio-buttons { display: flex; align-items: center; diff --git a/ui/app/pages/settings/settings.component.js b/ui/app/pages/settings/settings.component.js index 061e65060..a2f137264 100644 --- a/ui/app/pages/settings/settings.component.js +++ b/ui/app/pages/settings/settings.component.js @@ -1,11 +1,12 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import { Switch, Route, matchPath } from 'react-router-dom' +import { Switch, Route, matchPath, withRouter } from 'react-router-dom' import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums' import { getEnvironmentType } from '../../../../app/scripts/lib/util' import TabBar from '../../components/app/tab-bar' import c from 'classnames' import SettingsTab from './settings-tab' +import NetworksTab from './networks-tab' import AdvancedTab from './advanced-tab' import InfoTab from './info-tab' import SecurityTab from './security-tab' @@ -16,6 +17,7 @@ import { GENERAL_ROUTE, ABOUT_US_ROUTE, SETTINGS_ROUTE, + NETWORKS_ROUTE, } from '../../helpers/constants/routes' const ROUTES_TO_I18N_KEYS = { @@ -25,7 +27,7 @@ const ROUTES_TO_I18N_KEYS = { [ABOUT_US_ROUTE]: 'about', } -export default class SettingsPage extends PureComponent { +class SettingsPage extends PureComponent { static propTypes = { location: PropTypes.object, history: PropTypes.object, @@ -55,7 +57,7 @@ export default class SettingsPage extends PureComponent { > <div className="settings-page__header"> { - !this.isCurrentPath(SETTINGS_ROUTE) && ( + !this.isCurrentPath(SETTINGS_ROUTE) && !this.isCurrentPath(NETWORKS_ROUTE) && ( <div className="settings-page__back-button" onClick={() => history.push(SETTINGS_ROUTE)} @@ -75,6 +77,7 @@ export default class SettingsPage extends PureComponent { { this.renderTabs() } </div> <div className="settings-page__content__modules"> + { this.renderSubHeader() } { this.renderContent() } </div> </div> @@ -82,6 +85,17 @@ export default class SettingsPage extends PureComponent { ) } + renderSubHeader () { + const { t } = this.context + const { location: { pathname } } = this.props + + return ( + <div className="settings-page__subheader"> + {t(ROUTES_TO_I18N_KEYS[pathname] || 'general')} + </div> + ) + } + renderTabs () { const { history, location } = this.props const { t } = this.context @@ -92,6 +106,7 @@ export default class SettingsPage extends PureComponent { { content: t('general'), description: t('generalSettingsDescription'), key: GENERAL_ROUTE }, { content: t('advanced'), description: t('advancedSettingsDescription'), key: ADVANCED_ROUTE }, { content: t('securityAndPrivacy'), description: t('securitySettingsDescription'), key: SECURITY_ROUTE }, + { content: t('networks'), description: t('networkSettingsDescription'), key: NETWORKS_ROUTE }, { content: t('about'), description: t('aboutSettingsDescription'), key: ABOUT_US_ROUTE }, ]} isActive={key => { @@ -125,6 +140,11 @@ export default class SettingsPage extends PureComponent { /> <Route exact + path={NETWORKS_ROUTE} + component={NetworksTab} + /> + <Route + exact path={SECURITY_ROUTE} component={SecurityTab} /> @@ -135,3 +155,5 @@ export default class SettingsPage extends PureComponent { ) } } + +export default withRouter(SettingsPage) diff --git a/ui/app/selectors/custom-gas.js b/ui/app/selectors/custom-gas.js index ecffb37ca..5ba786f0f 100644 --- a/ui/app/selectors/custom-gas.js +++ b/ui/app/selectors/custom-gas.js @@ -18,7 +18,7 @@ import { } from '../helpers/utils/formatters' import { calcGasTotal, -} from '../components/app/send/send.utils' +} from '../pages/send/send.utils' import { addHexPrefix } from 'ethereumjs-util' const selectors = { diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index 554232f7b..c7cb80024 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -48,6 +48,8 @@ const selectors = { getNumberOfAccounts, getNumberOfTokens, isEthereumNetwork, + getMetaMetricState, + getRpcPrefsForCurrentProvider, } module.exports = selectors @@ -91,7 +93,8 @@ function getAccountType (state) { } function getSelectedAsset (state) { - return getSelectedToken(state) || 'ETH' + const selectedToken = getSelectedToken(state) + return selectedToken && selectedToken.symbol || 'ETH' } function getCurrentNetworkId (state) { @@ -164,7 +167,7 @@ function getSelectedToken (state) { const tokens = state.metamask.tokens || [] const selectedTokenAddress = state.metamask.selectedTokenAddress const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0] - const sendToken = state.metamask.send.token + const sendToken = state.metamask.send && state.metamask.send.token return selectedToken || sendToken || null } @@ -300,9 +303,10 @@ function isEthereumNetwork (state) { MAINNET, RINKEBY, ROPSTEN, + GOERLI, } = NETWORK_TYPES - return [ KOVAN, MAINNET, RINKEBY, ROPSTEN].includes(networkType) + return [ KOVAN, MAINNET, RINKEBY, ROPSTEN, GOERLI].includes(networkType) } function preferencesSelector ({ metamask }) { @@ -312,3 +316,22 @@ function preferencesSelector ({ metamask }) { function getAdvancedInlineGasShown (state) { return Boolean(state.metamask.featureFlags.advancedInlineGas) } + +function getMetaMetricState (state) { + return { + network: getCurrentNetworkId(state), + activeCurrency: getSelectedAsset(state), + accountType: getAccountType(state), + metaMetricsId: state.metamask.metaMetricsId, + numberOfTokens: getNumberOfTokens(state), + numberOfAccounts: getNumberOfAccounts(state), + participateInMetaMetrics: state.metamask.participateInMetaMetrics, + } +} + +function getRpcPrefsForCurrentProvider (state) { + const { frequentRpcListDetail, provider } = state.metamask + const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget) + const { rpcPrefs = {} } = selectRpcInfo || {} + return rpcPrefs +} diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index 7d369fdb9..7f6cbea1f 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -5,7 +5,7 @@ const { getTokenAddressFromTokenObject } = require('../helpers/utils/util') const { calcTokenBalance, estimateGas, -} = require('../components/app/send/send.utils') +} = require('../pages/send/send.utils') const ethUtil = require('ethereumjs-util') const { fetchLocale } = require('../helpers/utils/i18n-helper') const log = require('loglevel') @@ -239,6 +239,7 @@ var actions = { updateAndSetCustomRpc: updateAndSetCustomRpc, setRpcTarget: setRpcTarget, delRpcTarget: delRpcTarget, + editRpc: editRpc, setProviderType: setProviderType, SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH', setHardwareWalletDefaultHdPath, @@ -316,6 +317,7 @@ var actions = { UPDATE_PREFERENCES: 'UPDATE_PREFERENCES', setUseNativeCurrencyAsPrimaryCurrencyPreference, setShowFiatConversionOnTestnetsPreference, + setAutoLogoutTimeLimit, // Migration of users to new UI setCompletedUiMigration, @@ -343,12 +345,21 @@ var actions = { createCancelTransaction, createSpeedUpTransaction, - approveProviderRequest, - rejectProviderRequest, + approveProviderRequestByOrigin, + rejectProviderRequestByOrigin, clearApprovedOrigins, setFirstTimeFlowType, SET_FIRST_TIME_FLOW_TYPE: 'SET_FIRST_TIME_FLOW_TYPE', + + SET_SELECTED_SETTINGS_RPC_URL: 'SET_SELECTED_SETTINGS_RPC_URL', + setSelectedSettingsRpcUrl, + SET_NETWORKS_TAB_ADD_MODE: 'SET_NETWORKS_TAB_ADD_MODE', + setNetworksTabAddMode, + + // AppStateController-related actions + SET_LAST_ACTIVE_TIME: 'SET_LAST_ACTIVE_TIME', + setLastActiveTime, } module.exports = actions @@ -761,7 +772,7 @@ function addNewAccount () { function checkHardwareStatus (deviceName, hdPath) { log.debug(`background.checkHardwareStatus`, deviceName, hdPath) - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { background.checkHardwareStatus(deviceName, hdPath, (err, unlocked) => { @@ -782,10 +793,10 @@ function checkHardwareStatus (deviceName, hdPath) { function forgetDevice (deviceName) { log.debug(`background.forgetDevice`, deviceName) - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.forgetDevice(deviceName, (err, response) => { + background.forgetDevice(deviceName, (err) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -803,7 +814,7 @@ function forgetDevice (deviceName) { function connectHardware (deviceName, page, hdPath) { log.debug(`background.connectHardware`, deviceName, page, hdPath) - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { background.connectHardware(deviceName, page, hdPath, (err, accounts) => { @@ -824,10 +835,10 @@ function connectHardware (deviceName, page, hdPath) { function unlockHardwareWalletAccount (index, deviceName, hdPath) { log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath) - return (dispatch, getState) => { + return (dispatch) => { dispatch(actions.showLoadingIndication()) return new Promise((resolve, reject) => { - background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err, accounts) => { + background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err) => { if (err) { log.error(err) dispatch(actions.displayWarning(err.message)) @@ -848,7 +859,7 @@ function showInfoPage () { } function showQrScanner (ROUTE) { - return (dispatch, getState) => { + return (dispatch) => { return WebcamUtils.checkStatus() .then(status => { if (!status.environmentReady) { @@ -987,7 +998,7 @@ function signTypedMsg (msgData) { function signTx (txData) { return (dispatch) => { - global.ethQuery.sendTransaction(txData, (err, data) => { + global.ethQuery.sendTransaction(txData, (err) => { if (err) { return dispatch(actions.displayWarning(err.message)) } @@ -1020,7 +1031,6 @@ function setGasTotal (gasTotal) { function updateGasData ({ gasPrice, blockGasLimit, - recentBlocks, selectedAddress, selectedToken, to, @@ -1402,7 +1412,7 @@ function cancelTx (txData) { * @return {function(*): Promise<void>} */ function cancelTxs (txDataList) { - return async (dispatch, getState) => { + return async (dispatch) => { window.onbeforeunload = null dispatch(actions.showLoadingIndication()) const txIds = txDataList.map(({id}) => id) @@ -1807,7 +1817,7 @@ function removeSuggestedTokens () { return (dispatch) => { dispatch(actions.showLoadingIndication()) window.onbeforeunload = null - return new Promise((resolve, reject) => { + return new Promise((resolve) => { background.removeSuggestedTokens((err, suggestedTokens) => { dispatch(actions.hideLoadingIndication()) if (err) { @@ -1826,7 +1836,7 @@ function removeSuggestedTokens () { } function addKnownMethodData (fourBytePrefix, methodData) { - return (dispatch) => { + return () => { background.addKnownMethodData(fourBytePrefix, methodData) } } @@ -1931,7 +1941,7 @@ function setProviderType (type) { return (dispatch, getState) => { const { type: currentProviderType } = getState().metamask.provider log.debug(`background.setProviderType`, type) - background.setProviderType(type, (err, result) => { + background.setProviderType(type, (err) => { if (err) { log.error(err) return dispatch(actions.displayWarning('Had a problem changing networks!')) @@ -1958,10 +1968,10 @@ function setPreviousProvider (type) { } } -function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname) { +function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname, rpcPrefs) { return (dispatch) => { log.debug(`background.updateAndSetCustomRpc: ${newRpc} ${chainId} ${ticker} ${nickname}`) - background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err, result) => { + background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, rpcPrefs, (err) => { if (err) { log.error(err) return dispatch(actions.displayWarning('Had a problem changing networks!')) @@ -1974,10 +1984,33 @@ function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname) { } } +function editRpc (oldRpc, newRpc, chainId, ticker = 'ETH', nickname, rpcPrefs) { + return (dispatch) => { + log.debug(`background.delRpcTarget: ${oldRpc}`) + background.delCustomRpc(oldRpc, (err) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem removing network!')) + } + dispatch(actions.setSelectedToken()) + background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, rpcPrefs, (err) => { + if (err) { + log.error(err) + return dispatch(actions.displayWarning('Had a problem changing networks!')) + } + dispatch({ + type: actions.SET_RPC_TARGET, + value: newRpc, + }) + }) + }) + } +} + function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname) { return (dispatch) => { log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`) - background.setCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err, result) => { + background.setCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err) => { if (err) { log.error(err) return dispatch(actions.displayWarning('Had a problem changing networks!')) @@ -1990,7 +2023,7 @@ function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname) { function delRpcTarget (oldRpc) { return (dispatch) => { log.debug(`background.delRpcTarget: ${oldRpc}`) - background.delCustomRpc(oldRpc, (err, result) => { + background.delCustomRpc(oldRpc, (err) => { if (err) { log.error(err) return dispatch(self.displayWarning('Had a problem removing network!')) @@ -2000,11 +2033,12 @@ function delRpcTarget (oldRpc) { } } + // Calls the addressBookController to add a new address. function addToAddressBook (recipient, nickname = '') { log.debug(`background.addToAddressBook`) return (dispatch) => { - background.setAddressBook(recipient, nickname, (err, result) => { + background.setAddressBook(recipient, nickname, (err) => { if (err) { log.error(err) return dispatch(self.displayWarning('Address book failed to update')) @@ -2273,7 +2307,7 @@ function pairUpdate (coin) { } } -function shapeShiftSubview (network) { +function shapeShiftSubview () { var pair = 'btc_eth' return (dispatch) => { dispatch(actions.showSubLoadingIndication()) @@ -2309,7 +2343,7 @@ function coinShiftRquest (data, marketData) { } function buyWithShapeShift (data) { - return dispatch => new Promise((resolve, reject) => { + return () => new Promise((resolve, reject) => { shapeShiftRequest('shift', { method: 'POST', data}, (response) => { if (response.error) { return reject(response.error) @@ -2356,7 +2390,7 @@ function shapeShiftRequest (query, options, cb) { !options ? options = {} : null options.method ? method = options.method : method = 'GET' - var requestListner = function (request) { + var requestListner = function () { try { queryResponse = JSON.parse(this.responseText) cb ? cb(queryResponse) : null @@ -2439,6 +2473,10 @@ function setShowFiatConversionOnTestnetsPreference (value) { return setPreference('showFiatInTestnets', value) } +function setAutoLogoutTimeLimit (value) { + return setPreference('autoLogoutTimeLimit', value) +} + function setCompletedOnboarding () { return async dispatch => { dispatch(actions.showLoadingIndication()) @@ -2680,20 +2718,20 @@ function setPendingTokens (pendingTokens) { } } -function approveProviderRequest (tabID) { - return (dispatch) => { - background.approveProviderRequest(tabID) +function approveProviderRequestByOrigin (origin) { + return () => { + background.approveProviderRequestByOrigin(origin) } } -function rejectProviderRequest (tabID) { - return (dispatch) => { - background.rejectProviderRequest(tabID) +function rejectProviderRequestByOrigin (origin) { + return () => { + background.rejectProviderRequestByOrigin(origin) } } function clearApprovedOrigins () { - return (dispatch) => { + return () => { background.clearApprovedOrigins() } } @@ -2712,3 +2750,27 @@ function setFirstTimeFlowType (type) { }) } } + +function setSelectedSettingsRpcUrl (newRpcUrl) { + return { + type: actions.SET_SELECTED_SETTINGS_RPC_URL, + value: newRpcUrl, + } +} + +function setNetworksTabAddMode (isInAddMode) { + return { + type: actions.SET_NETWORKS_TAB_ADD_MODE, + value: isInAddMode, + } +} + +function setLastActiveTime () { + return (dispatch) => { + background.setLastActiveTime((err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + }) + } +} |