From 8c4d58aa4508e3d54c3f69847347e78d09c63b97 Mon Sep 17 00:00:00 2001 From: Bruno Date: Sun, 10 Jun 2018 03:52:32 -0400 Subject: initial trezor support --- ui/app/actions.js | 42 ++++ ui/app/components/account-menu/index.js | 9 + .../pages/create-account/connect-hardware.js | 234 +++++++++++++++++++++ ui/app/components/pages/create-account/index.js | 25 ++- .../components/pages/create-account/new-account.js | 2 + ui/app/css/itcss/components/new-account.scss | 105 ++++++++- ui/app/routes.js | 2 + 7 files changed, 413 insertions(+), 6 deletions(-) create mode 100644 ui/app/components/pages/create-account/connect-hardware.js (limited to 'ui') diff --git a/ui/app/actions.js b/ui/app/actions.js index 1edf692b6..758696203 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -79,6 +79,8 @@ var actions = { addNewKeyring, importNewAccount, addNewAccount, + connectHardware, + unlockTrezorAccount, NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', navigateToNewAccountScreen, resetAccount, @@ -596,6 +598,46 @@ function addNewAccount () { } } +function connectHardware (deviceName, page) { + log.debug(`background.connectHardware`, deviceName, page) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.connectHardware(deviceName, page, (err, accounts) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + + forceUpdateMetamaskState(dispatch) + return resolve(accounts) + }) + }) + } +} + +function unlockTrezorAccount (index) { + log.debug(`background.unlockTrezorAccount`, index) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.unlockTrezorAccount(index, (err, accounts) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + return resolve() + }) + }) + } +} + function showInfoPage () { return { type: actions.SHOW_INFO_PAGE, diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index f34631ca8..629669dac 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -14,6 +14,7 @@ const { INFO_ROUTE, NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, DEFAULT_ROUTE, } = require('../../routes') @@ -106,6 +107,14 @@ AccountMenu.prototype.render = function () { icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }), text: this.context.t('importAccount'), }), + h(Item, { + onClick: () => { + toggleAccountMenu() + history.push(CONNECT_HARDWARE_ROUTE) + }, + icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }), + text: this.context.t('connectHardware'), + }), h(Divider), h(Item, { onClick: () => { diff --git a/ui/app/components/pages/create-account/connect-hardware.js b/ui/app/components/pages/create-account/connect-hardware.js new file mode 100644 index 000000000..37b7414b3 --- /dev/null +++ b/ui/app/components/pages/create-account/connect-hardware.js @@ -0,0 +1,234 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../../actions') +const genAccountLink = require('../../../../lib/account-link.js') +const log = require('loglevel') +const { DEFAULT_ROUTE } = require('../../../routes') + +class ConnectHardwareForm extends Component { + constructor (props, context) { + super(props) + this.state = { + error: null, + response: null, + btnText: 'Connect to Trezor', // Test + selectedAccount: '', + accounts: [], + } + } + + connectToTrezor () { + if (this.state.accounts.length) { + return null + } + this.setState({ btnText: 'Connecting...' }) + this.getPage() + } + + getPage (page = 1) { + this.props + .connectHardware('trezor', page) + .then(accounts => { + if (accounts.length) { + this.setState({ accounts: accounts }) + } + }) + .catch(e => { + this.setState({ btnText: 'Connect to Trezor' }) + }) + } + + unlockAccount () { + if (this.state.selectedAccount === '') { + return Promise.reject({ error: 'You need to select an account!' }) + } + log.debug('should unlock account ', this.state.selectedAccount) + return this.props.unlockTrezorAccount(this.state.selectedAccount) + } + + handleRadioChange = e => { + log.debug('Selected account with index ', e.target.value) + + this.setState({ + selectedAccount: e.target.value, + error: null, + }) + } + + renderAccounts () { + if (!this.state.accounts.length) { + return null + } + log.debug('ACCOUNTS : ', this.state.accounts) + log.debug('SELECTED?', this.state.selectedAccount) + + return h('div.hw-account-list', [ + h('div.hw-account-list__title_wrapper', [ + h('div.hw-account-list__title', {}, ['Select an Address']), + h('div.hw-account-list__device', {}, ['Trezor - ETH']), + ]), + this.state.accounts.map((a, i) => { + return h('div.hw-account-list__item', { key: a.address }, [ + h('span.hw-account-list__item__index', a.index + 1), + h('div.hw-account-list__item__radio', [ + h('input', { + type: 'radio', + name: 'selectedAccount', + id: `address-${i}`, + value: a.index, + onChange: this.handleRadioChange, + }), + h( + 'label.hw-account-list__item__label', + { + htmlFor: `address-${i}`, + }, + `${a.address.slice(0, 4)}...${a.address.slice(-4)}` + ), + ]), + h('span.hw-account-list__item__balance', `${a.balance} ETH`), + h( + 'a.hw-account-list__item__link', + { + href: genAccountLink(a.address, this.props.network), + target: '_blank', + title: this.context.t('etherscanView'), + }, + h('img', { src: 'images/popout.svg' }) + ), + ]) + }), + ]) + } + + renderPagination () { + if (!this.state.accounts.length) { + return null + } + return h('div.hw-list-pagination', [ + h( + 'button.btn-primary.hw-list-pagination__button', + { + onClick: () => this.getPage(-1), + }, + '< Prev' + ), + + h( + 'button.btn-primary.hw-list-pagination__button', + { + onClick: () => this.getPage(), + }, + 'Next >' + ), + ]) + } + + renderButtons () { + if (!this.state.accounts.length) { + return null + } + const { history } = this.props + + return h('div.new-account-create-form__buttons', {}, [ + h( + 'button.btn-default.btn--large.new-account-create-form__button', + { + onClick: () => history.push(DEFAULT_ROUTE), + }, + [this.context.t('cancel')] + ), + + h( + 'button.btn-primary.btn--large.new-account-create-form__button', + { + onClick: () => { + this.unlockAccount(this.state.selectedAccount) + .then(() => history.push(DEFAULT_ROUTE)) + .catch(e => { + this.setState({ error: e.error }) + }) + }, + }, + [this.context.t('unlock')] + ), + ]) + } + + renderError () { + return this.state.error + ? h('span.error', { style: { marginBottom: 40 } }, this.state.error) + : null + } + + renderConnectButton () { + return !this.state.accounts.length + ? h( + 'button.btn-primary.btn--large', + { onClick: () => this.connectToTrezor(), style: { margin: 12 } }, + this.state.btnText + ) + : null + } + + render () { + return h('div.new-account-create-form', [ + this.renderError(), + this.renderConnectButton(), + this.renderAccounts(), + this.renderPagination(), + this.renderButtons(), + ]) + } +} + +ConnectHardwareForm.propTypes = { + hideModal: PropTypes.func, + showImportPage: PropTypes.func, + showConnectPage: PropTypes.func, + connectHardware: PropTypes.func, + unlockTrezorAccount: PropTypes.func, + numberOfExistingAccounts: PropTypes.number, + history: PropTypes.object, + t: PropTypes.func, + network: PropTypes.string, +} + +const mapStateToProps = state => { + const { + metamask: { network, selectedAddress, identities = {} }, + } = state + const numberOfExistingAccounts = Object.keys(identities).length + + return { + network, + address: selectedAddress, + numberOfExistingAccounts, + } +} + +const mapDispatchToProps = dispatch => { + return { + toCoinbase: address => + dispatch(actions.buyEth({ network: '1', address, amount: 0 })), + hideModal: () => dispatch(actions.hideModal()), + connectHardware: (deviceName, page) => { + return dispatch(actions.connectHardware(deviceName, page)) + }, + unlockTrezorAccount: index => { + return dispatch(actions.unlockTrezorAccount(index)) + }, + showImportPage: () => dispatch(actions.showImportPage()), + showConnectPage: () => dispatch(actions.showConnectPage()), + } +} + +ConnectHardwareForm.contextTypes = { + t: PropTypes.func, +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)( + ConnectHardwareForm +) diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js index 6e3b93742..69a4db80a 100644 --- a/ui/app/components/pages/create-account/index.js +++ b/ui/app/components/pages/create-account/index.js @@ -8,7 +8,12 @@ const { getCurrentViewContext } = require('../../../selectors') const classnames = require('classnames') const NewAccountCreateForm = require('./new-account') const NewAccountImportForm = require('./import-account') -const { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE } = require('../../../routes') +const ConnectHardwareForm = require('./connect-hardware') +const { + NEW_ACCOUNT_ROUTE, + IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, +} = require('../../../routes') class CreateAccountPage extends Component { renderTabs () { @@ -36,6 +41,19 @@ class CreateAccountPage extends Component { }, [ this.context.t('import'), ]), + h( + 'div.new-account__tabs__tab', + { + className: classnames('new-account__tabs__tab', { + 'new-account__tabs__selected': matchPath(location.pathname, { + path: CONNECT_HARDWARE_ROUTE, + exact: true, + }), + }), + onClick: () => history.push(CONNECT_HARDWARE_ROUTE), + }, + this.context.t('connect') + ), ]) } @@ -57,6 +75,11 @@ class CreateAccountPage extends Component { path: IMPORT_ACCOUNT_ROUTE, component: NewAccountImportForm, }), + h(Route, { + exact: true, + path: CONNECT_HARDWARE_ROUTE, + component: ConnectHardwareForm, + }), ]), ]), ]) diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js index 9c94990e0..402b8f03b 100644 --- a/ui/app/components/pages/create-account/new-account.js +++ b/ui/app/components/pages/create-account/new-account.js @@ -62,6 +62,7 @@ class NewAccountCreateForm extends Component { NewAccountCreateForm.propTypes = { hideModal: PropTypes.func, showImportPage: PropTypes.func, + showConnectPage: PropTypes.func, createAccount: PropTypes.func, numberOfExistingAccounts: PropTypes.number, history: PropTypes.object, @@ -92,6 +93,7 @@ const mapDispatchToProps = dispatch => { }) }, showImportPage: () => dispatch(actions.showImportPage()), + showConnectPage: () => dispatch(actions.showConnectPage()), } } diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index 293579058..2ecc157f1 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -28,7 +28,6 @@ &__tab { height: 54px; - width: 75px; padding: 15px 10px; color: $dusty-gray; font-family: Roboto; @@ -38,10 +37,6 @@ cursor: pointer; } - &__tab:first-of-type { - margin-right: 20px; - } - &__tab:hover { color: $black; border-bottom: none; @@ -158,6 +153,106 @@ } } +.hw-account-list { + display: flex; + flex: 1; + flex-flow: column; + width: 100%; + + &__title_wrapper { + display: flex; + flex-direction: row; + flex: 1; + } + + &__title { + margin-bottom: 23px; + align-self: flex-start; + color: $scorpion; + font-family: Roboto; + font-size: 16px; + line-height: 21px; + font-weight: bold; + display: flex; + flex: 1; + } + + &__device { + margin-bottom: 23px; + align-self: flex-end; + color: $scorpion; + font-family: Roboto; + font-size: 16px; + line-height: 21px; + font-weight: normal; + display: flex; + } + + &__item { + font-size: 15px; + flex-direction: row; + display: flex; + padding: 10px; + } + + &__item:nth-of-type(even) { + background-color: #fbfbfb; + } + + &__item:nth-of-type(odd) { + background: rgba(0, 0, 0, 0.03); + } + + &__item:hover { + background-color: rgba(0, 0, 0, 0.06); + } + + &__item__index { + display: flex; + margin-right: 20px; + } + + &__item__radio { + display: flex; + flex: 1; + } + + &__item__label { + margin-left: 10px; + display: flex; + flex: 1; + } + + &__item__balance { + display: flex; + flex: 1; + justify-content: center; + } + + &__item__link { + display: flex; + } + + &__item__link img { + width: 15px; + height: 15px; + } +} + +.hw-list-pagination { + display: flex; + align-self: flex-end; + margin-top: 10px; + + &__button { + height: 25px; + flex: initial; + min-width: 90px; + font-size: 12px; + } +} + + .new-account-create-form { display: flex; flex-flow: column; diff --git a/ui/app/routes.js b/ui/app/routes.js index 0ff3f644d..04f71165f 100644 --- a/ui/app/routes.js +++ b/ui/app/routes.js @@ -9,6 +9,7 @@ const ADD_TOKEN_ROUTE = '/add-token' const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token' const NEW_ACCOUNT_ROUTE = '/new-account' const IMPORT_ACCOUNT_ROUTE = '/new-account/import' +const CONNECT_HARDWARE_ROUTE = '/new-account/connect' const SEND_ROUTE = '/send' const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction' const SIGNATURE_REQUEST_ROUTE = '/confirm-transaction/signature-request' @@ -35,6 +36,7 @@ module.exports = { CONFIRM_ADD_TOKEN_ROUTE, NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, SEND_ROUTE, CONFIRM_TRANSACTION_ROUTE, NOTICE_ROUTE, -- cgit From d1880073f678dbdc52e07e62ec66c39eea5062a6 Mon Sep 17 00:00:00 2001 From: Bruno Date: Sun, 10 Jun 2018 21:10:22 -0400 Subject: balances working --- .../pages/create-account/connect-hardware.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware.js b/ui/app/components/pages/create-account/connect-hardware.js index 37b7414b3..6f1e03550 100644 --- a/ui/app/components/pages/create-account/connect-hardware.js +++ b/ui/app/components/pages/create-account/connect-hardware.js @@ -6,6 +6,7 @@ const actions = require('../../../actions') const genAccountLink = require('../../../../lib/account-link.js') const log = require('loglevel') const { DEFAULT_ROUTE } = require('../../../routes') +const { formatBalance } = require('../../../util') class ConnectHardwareForm extends Component { constructor (props, context) { @@ -57,10 +58,22 @@ class ConnectHardwareForm extends Component { }) } + getBalance (address) { + // Get the balance + log.debug('getBalance : ', address) + const { accounts } = this.props + const balanceValue = accounts && accounts[address] ? accounts[address].balance : '' + log.debug('balanceValue : ', balanceValue) + const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' + log.debug('formattedBalance : ', formattedBalance) + return formattedBalance + } + renderAccounts () { if (!this.state.accounts.length) { return null } + log.debug('ACCOUNTS : ', this.state.accounts) log.debug('SELECTED?', this.state.selectedAccount) @@ -70,6 +83,7 @@ class ConnectHardwareForm extends Component { h('div.hw-account-list__device', {}, ['Trezor - ETH']), ]), this.state.accounts.map((a, i) => { + return h('div.hw-account-list__item', { key: a.address }, [ h('span.hw-account-list__item__index', a.index + 1), h('div.hw-account-list__item__radio', [ @@ -88,7 +102,7 @@ class ConnectHardwareForm extends Component { `${a.address.slice(0, 4)}...${a.address.slice(-4)}` ), ]), - h('span.hw-account-list__item__balance', `${a.balance} ETH`), + h('span.hw-account-list__item__balance', `${this.getBalance(a.address)}`), h( 'a.hw-account-list__item__link', { @@ -194,16 +208,18 @@ ConnectHardwareForm.propTypes = { history: PropTypes.object, t: PropTypes.func, network: PropTypes.string, + accounts: PropTypes.object, } const mapStateToProps = state => { const { - metamask: { network, selectedAddress, identities = {} }, + metamask: { network, selectedAddress, identities = {}, accounts = [] }, } = state const numberOfExistingAccounts = Object.keys(identities).length return { network, + accounts, address: selectedAddress, numberOfExistingAccounts, } -- cgit From 8763ea898e7838d08315063b0e2181405a2ae3d5 Mon Sep 17 00:00:00 2001 From: Bruno Date: Wed, 13 Jun 2018 01:32:13 -0400 Subject: move TrezorKeyring to its own package --- ui/app/components/pages/create-account/connect-hardware.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware.js b/ui/app/components/pages/create-account/connect-hardware.js index 6f1e03550..152a4f275 100644 --- a/ui/app/components/pages/create-account/connect-hardware.js +++ b/ui/app/components/pages/create-account/connect-hardware.js @@ -25,10 +25,10 @@ class ConnectHardwareForm extends Component { return null } this.setState({ btnText: 'Connecting...' }) - this.getPage() + this.getPage(1) } - getPage (page = 1) { + getPage (page) { this.props .connectHardware('trezor', page) .then(accounts => { @@ -133,7 +133,7 @@ class ConnectHardwareForm extends Component { h( 'button.btn-primary.hw-list-pagination__button', { - onClick: () => this.getPage(), + onClick: () => this.getPage(1), }, 'Next >' ), -- cgit From 86a8c98148447916915da99962d82ec5c5dd6cb7 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 2 Jul 2018 15:14:57 -0400 Subject: always open connect hardware in full screen mode --- ui/app/components/account-menu/index.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'ui') diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 629669dac..be6963ac4 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -9,6 +9,10 @@ const actions = require('../../actions') const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') const Identicon = require('../identicon') const { formatBalance } = require('../../util') +const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') +const { getEnvironmentType } = require('../../../../app/scripts/lib/util') + + const { SETTINGS_ROUTE, INFO_ROUTE, @@ -110,7 +114,11 @@ AccountMenu.prototype.render = function () { h(Item, { onClick: () => { toggleAccountMenu() - history.push(CONNECT_HARDWARE_ROUTE) + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE) + } else { + history.push(CONNECT_HARDWARE_ROUTE) + } }, icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }), text: this.context.t('connectHardware'), -- cgit From f19ffaf08d49f33c395a25faf3eeb6b08d5285a4 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 2 Jul 2018 15:16:05 -0400 Subject: move hardcoded strings to localization file --- .../pages/create-account/connect-hardware.js | 24 +++++++--------------- 1 file changed, 7 insertions(+), 17 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware.js b/ui/app/components/pages/create-account/connect-hardware.js index 152a4f275..d022d7487 100644 --- a/ui/app/components/pages/create-account/connect-hardware.js +++ b/ui/app/components/pages/create-account/connect-hardware.js @@ -4,7 +4,6 @@ const h = require('react-hyperscript') const connect = require('react-redux').connect const actions = require('../../../actions') const genAccountLink = require('../../../../lib/account-link.js') -const log = require('loglevel') const { DEFAULT_ROUTE } = require('../../../routes') const { formatBalance } = require('../../../util') @@ -14,7 +13,7 @@ class ConnectHardwareForm extends Component { this.state = { error: null, response: null, - btnText: 'Connect to Trezor', // Test + btnText: context.t('connectToTrezor'), selectedAccount: '', accounts: [], } @@ -24,7 +23,7 @@ class ConnectHardwareForm extends Component { if (this.state.accounts.length) { return null } - this.setState({ btnText: 'Connecting...' }) + this.setState({ btnText: this.context.t('connecting')}) this.getPage(1) } @@ -37,21 +36,18 @@ class ConnectHardwareForm extends Component { } }) .catch(e => { - this.setState({ btnText: 'Connect to Trezor' }) + this.setState({ btnText: this.context.t('connectToTrezor') }) }) } unlockAccount () { if (this.state.selectedAccount === '') { - return Promise.reject({ error: 'You need to select an account!' }) + return Promise.reject({ error: this.context.t('accountSelectionRequired') }) } - log.debug('should unlock account ', this.state.selectedAccount) return this.props.unlockTrezorAccount(this.state.selectedAccount) } handleRadioChange = e => { - log.debug('Selected account with index ', e.target.value) - this.setState({ selectedAccount: e.target.value, error: null, @@ -60,12 +56,9 @@ class ConnectHardwareForm extends Component { getBalance (address) { // Get the balance - log.debug('getBalance : ', address) const { accounts } = this.props const balanceValue = accounts && accounts[address] ? accounts[address].balance : '' - log.debug('balanceValue : ', balanceValue) const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' - log.debug('formattedBalance : ', formattedBalance) return formattedBalance } @@ -74,12 +67,9 @@ class ConnectHardwareForm extends Component { return null } - log.debug('ACCOUNTS : ', this.state.accounts) - log.debug('SELECTED?', this.state.selectedAccount) - return h('div.hw-account-list', [ h('div.hw-account-list__title_wrapper', [ - h('div.hw-account-list__title', {}, ['Select an Address']), + h('div.hw-account-list__title', {}, [this.context.t('selectAnAddress')]), h('div.hw-account-list__device', {}, ['Trezor - ETH']), ]), this.state.accounts.map((a, i) => { @@ -127,7 +117,7 @@ class ConnectHardwareForm extends Component { { onClick: () => this.getPage(-1), }, - '< Prev' + `< ${this.context.t('prev')}` ), h( @@ -135,7 +125,7 @@ class ConnectHardwareForm extends Component { { onClick: () => this.getPage(1), }, - 'Next >' + `${this.context.t('next')} >` ), ]) } -- cgit From 9d3f2435e58e2454506ea1a5f7b85452a10edffa Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 3 Jul 2018 15:46:15 -0400 Subject: lint fix --- ui/app/components/pages/create-account/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js index ad2081315..d3de1ea01 100644 --- a/ui/app/components/pages/create-account/index.js +++ b/ui/app/components/pages/create-account/index.js @@ -12,7 +12,7 @@ const ConnectHardwareForm = require('./connect-hardware') const { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, - CONNECT_HARDWARE_ROUTE, + CONNECT_HARDWARE_ROUTE, } = require('../../../routes') class CreateAccountPage extends Component { -- cgit From 313090efcc5200c56373cf312052217f1a1340ef Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Thu, 5 Jul 2018 16:11:41 -0400 Subject: added message for non-chrome browsers --- .../pages/create-account/connect-hardware.js | 27 +++++++++++++++++++++- ui/app/css/itcss/components/new-account.scss | 13 +++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware.js b/ui/app/components/pages/create-account/connect-hardware.js index d022d7487..c51b4c773 100644 --- a/ui/app/components/pages/create-account/connect-hardware.js +++ b/ui/app/components/pages/create-account/connect-hardware.js @@ -177,10 +177,35 @@ class ConnectHardwareForm extends Component { : null } + renderUnsupportedBrowser () { + return ( + [h('div.hw-unsupported-browser', [ + h('h3.hw-unsupported-browser__title', {}, 'Bummer! Your Browser is not supported...'), + h('p.hw-unsupported-browser__msg', {}, 'You need to use Metamask on Google Chrome in order to connect to your TREZOR device.'), + ]), + h( + 'button.btn-primary.btn--large', + { onClick: () => global.platform.openWindow({ + url: 'https://google.com/chrome', + }), style: { margin: 12 } }, + 'Download Google Chrome' + )] + ) + } + + renderConnectScreen () { + const isChrome = window.navigator.userAgent.search('Chrome') !== -1 + if (isChrome) { + return this.renderConnectButton() + } else { + return this.renderUnsupportedBrowser() + } + } + render () { return h('div.new-account-create-form', [ this.renderError(), - this.renderConnectButton(), + this.renderConnectScreen(), this.renderAccounts(), this.renderPagination(), this.renderButtons(), diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index 2ecc157f1..551025df3 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -153,6 +153,19 @@ } } +.hw-unsupported-browser { + &__title { + padding-top: 10px; + } + + &__msg { + font-size: 14px; + color: #9b9b9b; + margin-top: 15px; + margin-bottom: 15px; + } +} + .hw-account-list { display: flex; flex: 1; -- cgit From ba5cde0995f956fb22825d604fe7d664677abaaa Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Thu, 5 Jul 2018 17:04:36 -0400 Subject: move strings to localization file --- .../pages/create-account/connect-hardware.js | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware.js b/ui/app/components/pages/create-account/connect-hardware.js index c51b4c773..1e4ee38e3 100644 --- a/ui/app/components/pages/create-account/connect-hardware.js +++ b/ui/app/components/pages/create-account/connect-hardware.js @@ -179,17 +179,18 @@ class ConnectHardwareForm extends Component { renderUnsupportedBrowser () { return ( - [h('div.hw-unsupported-browser', [ - h('h3.hw-unsupported-browser__title', {}, 'Bummer! Your Browser is not supported...'), - h('p.hw-unsupported-browser__msg', {}, 'You need to use Metamask on Google Chrome in order to connect to your TREZOR device.'), - ]), - h( - 'button.btn-primary.btn--large', - { onClick: () => global.platform.openWindow({ - url: 'https://google.com/chrome', - }), style: { margin: 12 } }, - 'Download Google Chrome' - )] + [ + h('div.hw-unsupported-browser', [ + h('h3.hw-unsupported-browser__title', {}, this.context.t('browserNotSupported')), + h('p.hw-unsupported-browser__msg', {}, this.context.t('chromeRequiredForTrezor')), + ]), + h( + 'button.btn-primary.btn--large', + { onClick: () => global.platform.openWindow({ + url: 'https://google.com/chrome', + }), style: { margin: 12 } }, + this.context.t('downloadGoogleChrome') + )] ) } -- cgit From 6c2730f24300449010bd3552d4d746bcb5dd176a Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Thu, 5 Jul 2018 17:45:28 -0400 Subject: Refactor UI --- .../pages/create-account/connect-hardware.js | 266 --------------------- .../connect-hardware/account-list.js | 137 +++++++++++ .../connect-hardware/connect-screen.js | 60 +++++ .../pages/create-account/connect-hardware/index.js | 131 ++++++++++ 4 files changed, 328 insertions(+), 266 deletions(-) delete mode 100644 ui/app/components/pages/create-account/connect-hardware.js create mode 100644 ui/app/components/pages/create-account/connect-hardware/account-list.js create mode 100644 ui/app/components/pages/create-account/connect-hardware/connect-screen.js create mode 100644 ui/app/components/pages/create-account/connect-hardware/index.js (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware.js b/ui/app/components/pages/create-account/connect-hardware.js deleted file mode 100644 index 1e4ee38e3..000000000 --- a/ui/app/components/pages/create-account/connect-hardware.js +++ /dev/null @@ -1,266 +0,0 @@ -const { Component } = require('react') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../../actions') -const genAccountLink = require('../../../../lib/account-link.js') -const { DEFAULT_ROUTE } = require('../../../routes') -const { formatBalance } = require('../../../util') - -class ConnectHardwareForm extends Component { - constructor (props, context) { - super(props) - this.state = { - error: null, - response: null, - btnText: context.t('connectToTrezor'), - selectedAccount: '', - accounts: [], - } - } - - connectToTrezor () { - if (this.state.accounts.length) { - return null - } - this.setState({ btnText: this.context.t('connecting')}) - this.getPage(1) - } - - getPage (page) { - this.props - .connectHardware('trezor', page) - .then(accounts => { - if (accounts.length) { - this.setState({ accounts: accounts }) - } - }) - .catch(e => { - this.setState({ btnText: this.context.t('connectToTrezor') }) - }) - } - - unlockAccount () { - if (this.state.selectedAccount === '') { - return Promise.reject({ error: this.context.t('accountSelectionRequired') }) - } - return this.props.unlockTrezorAccount(this.state.selectedAccount) - } - - handleRadioChange = e => { - this.setState({ - selectedAccount: e.target.value, - error: null, - }) - } - - getBalance (address) { - // Get the balance - const { accounts } = this.props - const balanceValue = accounts && accounts[address] ? accounts[address].balance : '' - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' - return formattedBalance - } - - renderAccounts () { - if (!this.state.accounts.length) { - return null - } - - return h('div.hw-account-list', [ - h('div.hw-account-list__title_wrapper', [ - h('div.hw-account-list__title', {}, [this.context.t('selectAnAddress')]), - h('div.hw-account-list__device', {}, ['Trezor - ETH']), - ]), - this.state.accounts.map((a, i) => { - - return h('div.hw-account-list__item', { key: a.address }, [ - h('span.hw-account-list__item__index', a.index + 1), - h('div.hw-account-list__item__radio', [ - h('input', { - type: 'radio', - name: 'selectedAccount', - id: `address-${i}`, - value: a.index, - onChange: this.handleRadioChange, - }), - h( - 'label.hw-account-list__item__label', - { - htmlFor: `address-${i}`, - }, - `${a.address.slice(0, 4)}...${a.address.slice(-4)}` - ), - ]), - h('span.hw-account-list__item__balance', `${this.getBalance(a.address)}`), - h( - 'a.hw-account-list__item__link', - { - href: genAccountLink(a.address, this.props.network), - target: '_blank', - title: this.context.t('etherscanView'), - }, - h('img', { src: 'images/popout.svg' }) - ), - ]) - }), - ]) - } - - renderPagination () { - if (!this.state.accounts.length) { - return null - } - return h('div.hw-list-pagination', [ - h( - 'button.btn-primary.hw-list-pagination__button', - { - onClick: () => this.getPage(-1), - }, - `< ${this.context.t('prev')}` - ), - - h( - 'button.btn-primary.hw-list-pagination__button', - { - onClick: () => this.getPage(1), - }, - `${this.context.t('next')} >` - ), - ]) - } - - renderButtons () { - if (!this.state.accounts.length) { - return null - } - const { history } = this.props - - return h('div.new-account-create-form__buttons', {}, [ - h( - 'button.btn-default.btn--large.new-account-create-form__button', - { - onClick: () => history.push(DEFAULT_ROUTE), - }, - [this.context.t('cancel')] - ), - - h( - 'button.btn-primary.btn--large.new-account-create-form__button', - { - onClick: () => { - this.unlockAccount(this.state.selectedAccount) - .then(() => history.push(DEFAULT_ROUTE)) - .catch(e => { - this.setState({ error: e.error }) - }) - }, - }, - [this.context.t('unlock')] - ), - ]) - } - - renderError () { - return this.state.error - ? h('span.error', { style: { marginBottom: 40 } }, this.state.error) - : null - } - - renderConnectButton () { - return !this.state.accounts.length - ? h( - 'button.btn-primary.btn--large', - { onClick: () => this.connectToTrezor(), style: { margin: 12 } }, - this.state.btnText - ) - : null - } - - renderUnsupportedBrowser () { - return ( - [ - h('div.hw-unsupported-browser', [ - h('h3.hw-unsupported-browser__title', {}, this.context.t('browserNotSupported')), - h('p.hw-unsupported-browser__msg', {}, this.context.t('chromeRequiredForTrezor')), - ]), - h( - 'button.btn-primary.btn--large', - { onClick: () => global.platform.openWindow({ - url: 'https://google.com/chrome', - }), style: { margin: 12 } }, - this.context.t('downloadGoogleChrome') - )] - ) - } - - renderConnectScreen () { - const isChrome = window.navigator.userAgent.search('Chrome') !== -1 - if (isChrome) { - return this.renderConnectButton() - } else { - return this.renderUnsupportedBrowser() - } - } - - render () { - return h('div.new-account-create-form', [ - this.renderError(), - this.renderConnectScreen(), - this.renderAccounts(), - this.renderPagination(), - this.renderButtons(), - ]) - } -} - -ConnectHardwareForm.propTypes = { - hideModal: PropTypes.func, - showImportPage: PropTypes.func, - showConnectPage: PropTypes.func, - connectHardware: PropTypes.func, - unlockTrezorAccount: PropTypes.func, - numberOfExistingAccounts: PropTypes.number, - history: PropTypes.object, - t: PropTypes.func, - network: PropTypes.string, - accounts: PropTypes.object, -} - -const mapStateToProps = state => { - const { - metamask: { network, selectedAddress, identities = {}, accounts = [] }, - } = state - const numberOfExistingAccounts = Object.keys(identities).length - - return { - network, - accounts, - address: selectedAddress, - numberOfExistingAccounts, - } -} - -const mapDispatchToProps = dispatch => { - return { - toCoinbase: address => - dispatch(actions.buyEth({ network: '1', address, amount: 0 })), - hideModal: () => dispatch(actions.hideModal()), - connectHardware: (deviceName, page) => { - return dispatch(actions.connectHardware(deviceName, page)) - }, - unlockTrezorAccount: index => { - return dispatch(actions.unlockTrezorAccount(index)) - }, - showImportPage: () => dispatch(actions.showImportPage()), - showConnectPage: () => dispatch(actions.showConnectPage()), - } -} - -ConnectHardwareForm.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps, mapDispatchToProps)( - ConnectHardwareForm -) diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js new file mode 100644 index 000000000..598865ad8 --- /dev/null +++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js @@ -0,0 +1,137 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const genAccountLink = require('../../../../lib/account-link.js') +const { DEFAULT_ROUTE } = require('../../../routes') +const { formatBalance } = require('../../../util') + +export default class AccountList extends Component { + constructor (props, context) { + super(props) + } + + getBalance (address) { + // Get the balance + const { accounts } = this.props + const balanceValue = accounts && accounts[address] ? accounts[address].balance : '' + const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' + return formattedBalance + } + + renderAccounts () { + if (!this.props.accounts.length) { + return null + } + + return h('div.hw-account-list', [ + h('div.hw-account-list__title_wrapper', [ + h('div.hw-account-list__title', {}, [this.context.t('selectAnAddress')]), + h('div.hw-account-list__device', {}, ['Trezor - ETH']), + ]), + this.props.accounts.map((a, i) => { + + return h('div.hw-account-list__item', { key: a.address }, [ + h('span.hw-account-list__item__index', a.index + 1), + h('div.hw-account-list__item__radio', [ + h('input', { + type: 'radio', + name: 'selectedAccount', + id: `address-${i}`, + value: a.index, + onChange: (e) => this.props.onAccountChange(e.target.value), + }), + h( + 'label.hw-account-list__item__label', + { + htmlFor: `address-${i}`, + }, + `${a.address.slice(0, 4)}...${a.address.slice(-4)}` + ), + ]), + h('span.hw-account-list__item__balance', `${this.getBalance(a.address)}`), + h( + 'a.hw-account-list__item__link', + { + href: genAccountLink(a.address, this.props.network), + target: '_blank', + title: this.context.t('etherscanView'), + }, + h('img', { src: 'images/popout.svg' }) + ), + ]) + }), + ]) + } + + renderPagination () { + if (!this.state.accounts.length) { + return null + } + return h('div.hw-list-pagination', [ + h( + 'button.btn-primary.hw-list-pagination__button', + { + onClick: () => this.props.getPage(-1), + }, + `< ${this.context.t('prev')}` + ), + + h( + 'button.btn-primary.hw-list-pagination__button', + { + onClick: () => this.props.getPage(1), + }, + `${this.context.t('next')} >` + ), + ]) + } + + renderButtons () { + if (!this.state.accounts.length) { + return null + } + const { history } = this.props + + return h('div.new-account-create-form__buttons', {}, [ + h( + 'button.btn-default.btn--large.new-account-create-form__button', + { + onClick: () => history.push(DEFAULT_ROUTE), + }, + [this.context.t('cancel')] + ), + + h( + 'button.btn-primary.btn--large.new-account-create-form__button', + { + onClick: () => { + this.unlockAccount(this.state.selectedAccount) + .then(() => history.push(DEFAULT_ROUTE)) + .catch(e => { + this.setState({ error: e.error }) + }) + }, + }, + [this.context.t('unlock')] + ), + ]) + } + + render () { + return null + } + +} + + +AccountList.propTypes = { + accounts: PropTypes.object.isRequired, + onAccountChange: PropTypes.func.isRequired, + getPage: PropTypes.func.isRequired, + network: PropTypes.string, + history: PropTypes.object, +} + +AccountList.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js new file mode 100644 index 000000000..1b064a15c --- /dev/null +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -0,0 +1,60 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') + +export default class ConnectScreen extends Component { + constructor (props, context) { + super(props) + } + + connectToTrezor = () => { + if (this.props.connectToTrezor) { + this.props.connectToTrezor() + } + } + + renderUnsupportedBrowser () { + return ( + [ + h('div.hw-unsupported-browser', [ + h('h3.hw-unsupported-browser__title', {}, this.context.t('browserNotSupported')), + h('p.hw-unsupported-browser__msg', {}, this.context.t('chromeRequiredForTrezor')), + ]), + h( + 'button.btn-primary.btn--large', + { onClick: () => global.platform.openWindow({ + url: 'https://google.com/chrome', + }), style: { margin: 12 } }, + this.context.t('downloadGoogleChrome') + )] + ) + } + + renderConnectButton () { + return !this.state.accounts.length + ? h( + 'button.btn-primary.btn--large', + { onClick: this.connectToTrezor, style: { margin: 12 } }, + this.props.btnText + ) + : null + } + + render () { + const isChrome = window.navigator.userAgent.search('Chrome') !== -1 + if (isChrome) { + return this.renderConnectButton() + } else { + return this.renderUnsupportedBrowser() + } + } +} + +ConnectScreen.propTypes = { + connectToTrezor: PropTypes.func.isRequired, + btnText: PropTypes.string, +} + +ConnectScreen.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js new file mode 100644 index 000000000..04e69162f --- /dev/null +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -0,0 +1,131 @@ +const { Component } = require('react') +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../../actions') +const ConnectScreen = require('./connect-screen') +const AccountList = require('./account-list') + +class ConnectHardwareForm extends Component { + constructor (props, context) { + super(props) + this.state = { + error: null, + response: null, + btnText: context.t('connectToTrezor'), + selectedAccount: '', + accounts: [], + } + } + + connectToTrezor = () => { + if (this.state.accounts.length) { + return null + } + this.setState({ btnText: this.context.t('connecting')}) + this.getPage(1) + } + + onAccountChange = (account) => { + this.setState({selectedAccount: account, error: null}) + } + + getPage = (page) => { + this.props + .connectHardware('trezor', page) + .then(accounts => { + if (accounts.length) { + this.setState({ accounts: accounts }) + } + }) + .catch(e => { + this.setState({ btnText: this.context.t('connectToTrezor') }) + }) + } + + unlockAccount () { + if (this.state.selectedAccount === '') { + return Promise.reject({ error: this.context.t('accountSelectionRequired') }) + } + return this.props.unlockTrezorAccount(this.state.selectedAccount) + } + + + renderError () { + return this.state.error + ? h('span.error', { style: { marginBottom: 40 } }, this.state.error) + : null + } + + renderContent () { + if (!this.state.accounts.length) { + return h(ConnectScreen, { + connectToTrezor: this.connectToTrezor, + btnText: this.state.btnText, + }) + } + + return h(AccountList, { + accounts: this.state.accounts, + onAccountChange: this.onAccountChange, + network: this.props.network, + getPage: this.getPage, + history: this.props.history, + }) + } + + render () { + return h('div.new-account-create-form', [ + this.renderError(), + this.renderContent(), + ]) + } +} + +ConnectHardwareForm.propTypes = { + hideModal: PropTypes.func, + showImportPage: PropTypes.func, + showConnectPage: PropTypes.func, + connectHardware: PropTypes.func, + unlockTrezorAccount: PropTypes.func, + numberOfExistingAccounts: PropTypes.number, + history: PropTypes.object, + t: PropTypes.func, + network: PropTypes.string, + accounts: PropTypes.object, +} + +const mapStateToProps = state => { + const { + metamask: { network, selectedAddress, identities = {}, accounts = [] }, + } = state + const numberOfExistingAccounts = Object.keys(identities).length + + return { + network, + accounts, + address: selectedAddress, + numberOfExistingAccounts, + } +} + +const mapDispatchToProps = dispatch => { + return { + connectHardware: (deviceName, page) => { + return dispatch(actions.connectHardware(deviceName, page)) + }, + unlockTrezorAccount: index => { + return dispatch(actions.unlockTrezorAccount(index)) + }, + showImportPage: () => dispatch(actions.showImportPage()), + showConnectPage: () => dispatch(actions.showConnectPage()), + } +} + +ConnectHardwareForm.contextTypes = { + t: PropTypes.func, +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)( + ConnectHardwareForm +) -- cgit From 6b2511f94f436a30c6c683f9da2c3142d9a6461c Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Thu, 5 Jul 2018 20:59:31 -0400 Subject: UI refactor --- .../connect-hardware/account-list.js | 111 ++++++++++----------- .../connect-hardware/connect-screen.js | 51 +++++----- .../pages/create-account/connect-hardware/index.js | 41 ++++++-- 3 files changed, 106 insertions(+), 97 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js index 598865ad8..77e0af3ac 100644 --- a/ui/app/components/pages/create-account/connect-hardware/account-list.js +++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js @@ -1,11 +1,10 @@ const { Component } = require('react') const PropTypes = require('prop-types') const h = require('react-hyperscript') -const genAccountLink = require('../../../../lib/account-link.js') -const { DEFAULT_ROUTE } = require('../../../routes') -const { formatBalance } = require('../../../util') +const genAccountLink = require('../../../../../lib/account-link.js') +const { formatBalance } = require('../../../../util') -export default class AccountList extends Component { +class AccountList extends Component { constructor (props, context) { super(props) } @@ -19,54 +18,48 @@ export default class AccountList extends Component { } renderAccounts () { - if (!this.props.accounts.length) { - return null - } - return h('div.hw-account-list', [ - h('div.hw-account-list__title_wrapper', [ - h('div.hw-account-list__title', {}, [this.context.t('selectAnAddress')]), - h('div.hw-account-list__device', {}, ['Trezor - ETH']), - ]), - this.props.accounts.map((a, i) => { + h('div.hw-account-list__title_wrapper', [ + h('div.hw-account-list__title', {}, [this.context.t('selectAnAddress')]), + h('div.hw-account-list__device', {}, ['Trezor - ETH']), + ]), + this.props.accounts.map((a, i) => { - return h('div.hw-account-list__item', { key: a.address }, [ - h('span.hw-account-list__item__index', a.index + 1), - h('div.hw-account-list__item__radio', [ - h('input', { - type: 'radio', - name: 'selectedAccount', - id: `address-${i}`, - value: a.index, - onChange: (e) => this.props.onAccountChange(e.target.value), - }), + return h('div.hw-account-list__item', { key: a.address }, [ + h('span.hw-account-list__item__index', a.index + 1), + h('div.hw-account-list__item__radio', [ + h('input', { + type: 'radio', + name: 'selectedAccount', + id: `address-${i}`, + value: a.index, + onChange: (e) => this.props.onAccountChange(e.target.value), + checked: this.props.selectedAccount === a.index.toString(), + }), + h( + 'label.hw-account-list__item__label', + { + htmlFor: `address-${i}`, + }, + `${a.address.slice(0, 4)}...${a.address.slice(-4)}` + ), + ]), + h('span.hw-account-list__item__balance', `${this.getBalance(a.address)}`), h( - 'label.hw-account-list__item__label', - { - htmlFor: `address-${i}`, - }, - `${a.address.slice(0, 4)}...${a.address.slice(-4)}` + 'a.hw-account-list__item__link', + { + href: genAccountLink(a.address, this.props.network), + target: '_blank', + title: this.context.t('etherscanView'), + }, + h('img', { src: 'images/popout.svg' }) ), - ]), - h('span.hw-account-list__item__balance', `${this.getBalance(a.address)}`), - h( - 'a.hw-account-list__item__link', - { - href: genAccountLink(a.address, this.props.network), - target: '_blank', - title: this.context.t('etherscanView'), - }, - h('img', { src: 'images/popout.svg' }) - ), - ]) - }), + ]) + }), ]) } renderPagination () { - if (!this.state.accounts.length) { - return null - } return h('div.hw-list-pagination', [ h( 'button.btn-primary.hw-list-pagination__button', @@ -87,30 +80,19 @@ export default class AccountList extends Component { } renderButtons () { - if (!this.state.accounts.length) { - return null - } - const { history } = this.props - return h('div.new-account-create-form__buttons', {}, [ h( 'button.btn-default.btn--large.new-account-create-form__button', { - onClick: () => history.push(DEFAULT_ROUTE), + onClick: this.props.onCancel.bind(this), }, [this.context.t('cancel')] ), h( - 'button.btn-primary.btn--large.new-account-create-form__button', + `button.btn-primary.btn--large.new-account-create-form__button ${this.props.selectedAccount === null ? '.btn-primary--disabled' : ''}`, { - onClick: () => { - this.unlockAccount(this.state.selectedAccount) - .then(() => history.push(DEFAULT_ROUTE)) - .catch(e => { - this.setState({ error: e.error }) - }) - }, + onClick: this.props.onUnlockAccount.bind(this), }, [this.context.t('unlock')] ), @@ -118,20 +100,29 @@ export default class AccountList extends Component { } render () { - return null + return h('div', {}, [ + this.renderAccounts(), + this.renderPagination(), + this.renderButtons(), + ]) } } AccountList.propTypes = { - accounts: PropTypes.object.isRequired, + accounts: PropTypes.array.isRequired, onAccountChange: PropTypes.func.isRequired, getPage: PropTypes.func.isRequired, network: PropTypes.string, + selectedAccount: PropTypes.string, history: PropTypes.object, + onUnlockAccount: PropTypes.func, + onCancel: PropTypes.func, } AccountList.contextTypes = { t: PropTypes.func, } + +module.exports = AccountList diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js index 1b064a15c..ec6a11b40 100644 --- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -2,51 +2,43 @@ const { Component } = require('react') const PropTypes = require('prop-types') const h = require('react-hyperscript') -export default class ConnectScreen extends Component { +class ConnectScreen extends Component { constructor (props, context) { super(props) } - connectToTrezor = () => { - if (this.props.connectToTrezor) { - this.props.connectToTrezor() - } - } - renderUnsupportedBrowser () { return ( - [ - h('div.hw-unsupported-browser', [ - h('h3.hw-unsupported-browser__title', {}, this.context.t('browserNotSupported')), - h('p.hw-unsupported-browser__msg', {}, this.context.t('chromeRequiredForTrezor')), - ]), - h( - 'button.btn-primary.btn--large', - { onClick: () => global.platform.openWindow({ - url: 'https://google.com/chrome', - }), style: { margin: 12 } }, - this.context.t('downloadGoogleChrome') - )] + h('div', {}, [ + h('div.hw-unsupported-browser', [ + h('h3.hw-unsupported-browser__title', {}, this.context.t('browserNotSupported')), + h('p.hw-unsupported-browser__msg', {}, this.context.t('chromeRequiredForTrezor')), + ]), + h( + 'button.btn-primary.btn--large', + { onClick: () => global.platform.openWindow({ + url: 'https://google.com/chrome', + }), style: { margin: 12 } }, + this.context.t('downloadGoogleChrome') + ), + ]) ) } renderConnectButton () { - return !this.state.accounts.length - ? h( - 'button.btn-primary.btn--large', - { onClick: this.connectToTrezor, style: { margin: 12 } }, - this.props.btnText - ) - : null + return h( + 'button.btn-primary.btn--large', + { onClick: this.props.connectToTrezor.bind(this), style: { margin: 12 } }, + this.props.btnText + ) } render () { const isChrome = window.navigator.userAgent.search('Chrome') !== -1 if (isChrome) { return this.renderConnectButton() - } else { - return this.renderUnsupportedBrowser() } + return this.renderUnsupportedBrowser() } } @@ -58,3 +50,6 @@ ConnectScreen.propTypes = { ConnectScreen.contextTypes = { t: PropTypes.func, } + +module.exports = ConnectScreen + diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index 04e69162f..22c54d28c 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -2,9 +2,10 @@ const { Component } = require('react') const PropTypes = require('prop-types') const h = require('react-hyperscript') const connect = require('react-redux').connect -const actions = require('../../../actions') +const actions = require('../../../../actions') const ConnectScreen = require('./connect-screen') const AccountList = require('./account-list') +const { DEFAULT_ROUTE } = require('../../../../routes') class ConnectHardwareForm extends Component { constructor (props, context) { @@ -13,7 +14,7 @@ class ConnectHardwareForm extends Component { error: null, response: null, btnText: context.t('connectToTrezor'), - selectedAccount: '', + selectedAccount: null, accounts: [], } } @@ -23,11 +24,11 @@ class ConnectHardwareForm extends Component { return null } this.setState({ btnText: this.context.t('connecting')}) - this.getPage(1) + this.getPage(0) } onAccountChange = (account) => { - this.setState({selectedAccount: account, error: null}) + this.setState({selectedAccount: account.toString(), error: null}) } getPage = (page) => { @@ -35,7 +36,16 @@ class ConnectHardwareForm extends Component { .connectHardware('trezor', page) .then(accounts => { if (accounts.length) { - this.setState({ accounts: accounts }) + const newState = { accounts: accounts } + // Default to the first account + if (this.state.selectedAccount === null) { + const firstAccount = accounts[0] + newState.selectedAccount = firstAccount.index.toString() + // If the page doesn't contain the selected account, let's deselect it + } else if (!accounts.filter(a => a.index.toString() === '').lenght) { + newState.selectedAccount = null + } + this.setState(newState) } }) .catch(e => { @@ -43,13 +53,23 @@ class ConnectHardwareForm extends Component { }) } - unlockAccount () { - if (this.state.selectedAccount === '') { - return Promise.reject({ error: this.context.t('accountSelectionRequired') }) + onUnlockAccount = () => { + + if (this.state.selectedAccount === null) { + this.setState({ error: this.context.t('accountSelectionRequired') }) } - return this.props.unlockTrezorAccount(this.state.selectedAccount) + + this.props.unlockTrezorAccount(this.state.selectedAccount) + .then(_ => { + this.props.history.push(DEFAULT_ROUTE) + }).catch(e => { + this.setState({ error: e.toString() }) + }) } + onCancel = () => { + this.props.history.push(DEFAULT_ROUTE) + } renderError () { return this.state.error @@ -67,10 +87,13 @@ class ConnectHardwareForm extends Component { return h(AccountList, { accounts: this.state.accounts, + selectedAccount: this.state.selectedAccount, onAccountChange: this.onAccountChange, network: this.props.network, getPage: this.getPage, history: this.props.history, + onUnlockAccount: this.onUnlockAccount, + onCancel: this.onCancel, }) } -- cgit From dddbb4250b30b7263eb97ddc2e23791166bcc98e Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Fri, 6 Jul 2018 20:04:20 -0400 Subject: update connect harwdware screen --- .../connect-hardware/connect-screen.js | 28 +++++++++++++++------- ui/app/css/itcss/components/new-account.scss | 6 ++++- 2 files changed, 25 insertions(+), 9 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js index ec6a11b40..cf6353bf8 100644 --- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -10,9 +10,9 @@ class ConnectScreen extends Component { renderUnsupportedBrowser () { return ( h('div', {}, [ - h('div.hw-unsupported-browser', [ - h('h3.hw-unsupported-browser__title', {}, this.context.t('browserNotSupported')), - h('p.hw-unsupported-browser__msg', {}, this.context.t('chromeRequiredForTrezor')), + h('div.hw-connect', [ + h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')), + h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')), ]), h( 'button.btn-primary.btn--large', @@ -25,18 +25,30 @@ class ConnectScreen extends Component { ) } - renderConnectButton () { - return h( + renderConnectScreen () { + return ( + h('div', {}, [ + h('div.hw-connect', [ + h('h3.hw-connect__title', {}, this.context.t('trezorHardwareWallet')), + h('p.hw-connect__msg', {}, this.context.t('connectToTrezorHelp')), + h('p.hw-connect__msg', {}, [ + this.context.t('connectToTrezorTrouble'), + h('a.___info-link', { url: 'https://support.metamask.io/', target: '_blank'}, this.context.t('learnMore')), + ]), + ]), + h( 'button.btn-primary.btn--large', - { onClick: this.props.connectToTrezor.bind(this), style: { margin: 12 } }, + { onClick: this.props.connectToTrezor.bind(this) }, this.props.btnText - ) + ), + ]) + ) } render () { const isChrome = window.navigator.userAgent.search('Chrome') !== -1 if (isChrome) { - return this.renderConnectButton() + return this.renderConnectScreen() } return this.renderUnsupportedBrowser() } diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index 551025df3..3d59081d8 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -153,7 +153,7 @@ } } -.hw-unsupported-browser { +.hw-connect { &__title { padding-top: 10px; } @@ -164,6 +164,10 @@ margin-top: 15px; margin-bottom: 15px; } + + &__info-link { + color: #2f9ae0; + } } .hw-account-list { -- cgit From 512760154528c47213cc8ff75475c21e3e674a23 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Fri, 6 Jul 2018 20:37:08 -0400 Subject: copy updated --- .../pages/create-account/connect-hardware/connect-screen.js | 5 ++++- ui/app/css/itcss/components/new-account.scss | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js index cf6353bf8..dd9fdfba2 100644 --- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -33,7 +33,10 @@ class ConnectScreen extends Component { h('p.hw-connect__msg', {}, this.context.t('connectToTrezorHelp')), h('p.hw-connect__msg', {}, [ this.context.t('connectToTrezorTrouble'), - h('a.___info-link', { url: 'https://support.metamask.io/', target: '_blank'}, this.context.t('learnMore')), + h('a.hw-connect__link', { + href: 'https://support.metamask.io/', + target: '_blank', + }, ` ${this.context.t('learnMore')}`), ]), ]), h( diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index 3d59081d8..7dfa839ab 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -165,7 +165,7 @@ margin-bottom: 15px; } - &__info-link { + &__link { color: #2f9ae0; } } -- cgit From 7cca7ace2ea4cd4b9d3a242067c9a7c344406aba Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 9 Jul 2018 17:24:52 -0400 Subject: fix all the account related bugs --- .../connect-hardware/account-list.js | 11 +------- .../pages/create-account/connect-hardware/index.js | 29 +++++++++++++++++++--- 2 files changed, 27 insertions(+), 13 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js index 77e0af3ac..170d8f0b3 100644 --- a/ui/app/components/pages/create-account/connect-hardware/account-list.js +++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js @@ -2,21 +2,12 @@ const { Component } = require('react') const PropTypes = require('prop-types') const h = require('react-hyperscript') const genAccountLink = require('../../../../../lib/account-link.js') -const { formatBalance } = require('../../../../util') class AccountList extends Component { constructor (props, context) { super(props) } - getBalance (address) { - // Get the balance - const { accounts } = this.props - const balanceValue = accounts && accounts[address] ? accounts[address].balance : '' - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' - return formattedBalance - } - renderAccounts () { return h('div.hw-account-list', [ h('div.hw-account-list__title_wrapper', [ @@ -44,7 +35,7 @@ class AccountList extends Component { `${a.address.slice(0, 4)}...${a.address.slice(-4)}` ), ]), - h('span.hw-account-list__item__balance', `${this.getBalance(a.address)}`), + h('span.hw-account-list__item__balance', `${a.balance}`), h( 'a.hw-account-list__item__link', { diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index 22c54d28c..1aaa0be64 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -6,6 +6,7 @@ const actions = require('../../../../actions') const ConnectScreen = require('./connect-screen') const AccountList = require('./account-list') const { DEFAULT_ROUTE } = require('../../../../routes') +const { formatBalance } = require('../../../../util') class ConnectHardwareForm extends Component { constructor (props, context) { @@ -31,20 +32,42 @@ class ConnectHardwareForm extends Component { this.setState({selectedAccount: account.toString(), error: null}) } + getBalance (address) { + // Get the balance + const { accounts } = this.props + const balanceValue = accounts && accounts[address.toLowerCase()] ? accounts[address.toLowerCase()].balance : '' + const formattedBalance = balanceValue !== null ? formatBalance(balanceValue, 6) : '...' + console.log('[TREZOR]: got balance', address, accounts, balanceValue, formattedBalance) + return formattedBalance + } + getPage = (page) => { this.props .connectHardware('trezor', page) .then(accounts => { + console.log('[TREZOR]: GOT PAGE!', accounts) if (accounts.length) { - const newState = { accounts: accounts } + const newState = {} // Default to the first account if (this.state.selectedAccount === null) { const firstAccount = accounts[0] - newState.selectedAccount = firstAccount.index.toString() + newState.selectedAccount = firstAccount.index.toString() === '0' ? firstAccount.index.toString() : null + console.log('[TREZOR]: just defaulted to account', newState.selectedAccount) // If the page doesn't contain the selected account, let's deselect it - } else if (!accounts.filter(a => a.index.toString() === '').lenght) { + } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) { newState.selectedAccount = null + console.log('[TREZOR]: just removed default account', newState.selectedAccount) } + + console.log('[TREZOR]: mapping balances') + + // Map accounts with balances + newState.accounts = accounts.map(account => { + account.balance = this.getBalance(account.address) + return account + }) + + console.log('[TREZOR]: ABOUT TO RENDER ACCOUNTS: ', page, newState.accounts) this.setState(newState) } }) -- cgit From 2de3039b6b21ca05ef185c078b67815448864c72 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Mon, 9 Jul 2018 17:55:37 -0400 Subject: fix account duplication --- ui/app/components/pages/create-account/connect-hardware/index.js | 6 ------ 1 file changed, 6 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index 1aaa0be64..126102235 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -37,7 +37,6 @@ class ConnectHardwareForm extends Component { const { accounts } = this.props const balanceValue = accounts && accounts[address.toLowerCase()] ? accounts[address.toLowerCase()].balance : '' const formattedBalance = balanceValue !== null ? formatBalance(balanceValue, 6) : '...' - console.log('[TREZOR]: got balance', address, accounts, balanceValue, formattedBalance) return formattedBalance } @@ -45,21 +44,17 @@ class ConnectHardwareForm extends Component { this.props .connectHardware('trezor', page) .then(accounts => { - console.log('[TREZOR]: GOT PAGE!', accounts) if (accounts.length) { const newState = {} // Default to the first account if (this.state.selectedAccount === null) { const firstAccount = accounts[0] newState.selectedAccount = firstAccount.index.toString() === '0' ? firstAccount.index.toString() : null - console.log('[TREZOR]: just defaulted to account', newState.selectedAccount) // If the page doesn't contain the selected account, let's deselect it } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) { newState.selectedAccount = null - console.log('[TREZOR]: just removed default account', newState.selectedAccount) } - console.log('[TREZOR]: mapping balances') // Map accounts with balances newState.accounts = accounts.map(account => { @@ -67,7 +62,6 @@ class ConnectHardwareForm extends Component { return account }) - console.log('[TREZOR]: ABOUT TO RENDER ACCOUNTS: ', page, newState.accounts) this.setState(newState) } }) -- cgit From 9b81180ab10cf8ca59666104e862c0331e953591 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 10 Jul 2018 00:20:00 -0400 Subject: added ui to remove accounts --- ui/app/components/account-menu/index.js | 34 +++++++++++++++++++++++---- ui/app/css/itcss/components/account-menu.scss | 17 ++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) (limited to 'ui') diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index be6963ac4..9530d6aeb 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -187,16 +187,42 @@ AccountMenu.prototype.renderAccounts = function () { h('div.account-menu__balance', formattedBalance), ]), - this.indicateIfLoose(keyring), + this.renderKeyringType(keyring), + this.renderForgetAccount(keyring, identity.address), ], ) }) } -AccountMenu.prototype.indicateIfLoose = function (keyring) { +AccountMenu.prototype.renderForgetAccount = function (keyring, address) { + // Any account that's not form the HD wallet can be forgotten + const type = keyring.type + const isForgetable = type !== 'HD Key Tree' + return isForgetable ? h('a.forget-account-icon', { onClick: (e) => this.forgetAccount(e, address) }, '') : null +} + +AccountMenu.prototype.forgetAccount = function (e, address) { + e.preventDefault() + e.stopPropagation() + console.log('should forget address: ', address) +} + +AccountMenu.prototype.renderKeyringType = function (keyring) { try { // Sometimes keyrings aren't loaded yet: const type = keyring.type - const isLoose = type !== 'HD Key Tree' - return isLoose ? h('.keyring-label.allcaps', this.context.t('imported')) : null + let label + switch (type) { + case 'Trezor Hardware': + label = this.context.t('hardware') + break + case 'Simple Key Pair': + label = this.context.t('imported') + break + default: + label = '' + } + + return label !== '' ? h('.keyring-label.allcaps', label) : null + } catch (e) { return } } diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss index 96fba890c..20fc68424 100644 --- a/ui/app/css/itcss/components/account-menu.scss +++ b/ui/app/css/itcss/components/account-menu.scss @@ -72,6 +72,7 @@ background-color: $dusty-gray; color: $black; font-weight: normal; + letter-spacing: .5px; } } @@ -84,6 +85,22 @@ @media screen and (max-width: 575px) { padding: 12px 14px; } + + .forget-account-icon { + width: 25px; + margin-left: 10px; + } + + &:hover { + .forget-account-icon::after { + content: '\00D7'; + font-size: 25px; + color: $white; + cursor: pointer; + position: absolute; + margin-top: -5px; + } + } } &__account-info { -- cgit From b9c2994d24e688305d63aaefd7fac88d88773ad9 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 10 Jul 2018 19:19:29 -0400 Subject: finish warning modal UI --- ui/app/components/account-menu/index.js | 5 ++ .../confirm-remove-account.component.js | 60 ++++++++++++++++++++++ .../confirm-remove-account.container.js | 13 +++++ .../modals/confirm-remove-account/index.js | 2 + ui/app/components/modals/index.scss | 11 ++++ ui/app/components/modals/modal.js | 19 ++++++- ui/app/css/itcss/components/account-menu.scss | 3 +- ui/app/css/itcss/components/new-account.scss | 1 + 8 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js create mode 100644 ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js create mode 100644 ui/app/components/modals/confirm-remove-account/index.js (limited to 'ui') diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 9530d6aeb..b561ea186 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -68,6 +68,9 @@ function mapDispatchToProps (dispatch) { dispatch(actions.hideSidebar()) dispatch(actions.toggleAccountMenu()) }, + showForgetAccountConfirmationModal: (address) => { + return dispatch(actions.showModal({ name: 'CONFIRM_FORGET_ACCOUNT', address })) + }, } } @@ -204,7 +207,9 @@ AccountMenu.prototype.renderForgetAccount = function (keyring, address) { AccountMenu.prototype.forgetAccount = function (e, address) { e.preventDefault() e.stopPropagation() + const { showForgetAccountConfirmationModal } = this.props console.log('should forget address: ', address) + showForgetAccountConfirmationModal(address) } AccountMenu.prototype.renderKeyringType = function (keyring) { diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js new file mode 100644 index 000000000..93be2a4e7 --- /dev/null +++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Button from '../../button' +import { addressSummary } from '../../../util' + +class ConfirmRemoveAccount extends Component { + static propTypes = { + hideModal: PropTypes.func.isRequired, + removeAccount: PropTypes.func.isRequired, + address: PropTypes.string.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + handleRemove () { + this.props.removeAccount(this.props.address) + .then(() => this.props.hideModal()) + } + + render () { + const { t } = this.context + + return ( +
+
+
+ { `${t('removeAccount')}` } +
+
+ {addressSummary(this.props.address)} +
+
+ { t('removeAccountDescription') } + { t('learnMore') } +
+
+
+ + +
+
+ ) + } +} + +export default ConfirmRemoveAccount diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js new file mode 100644 index 000000000..9a612f2f6 --- /dev/null +++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import ConfirmRemoveAccount from './confirm-remove-account.component' + +const { hideModal, removeAccount } = require('../../../actions') + +const mapDispatchToProps = dispatch => { + return { + hideModal: () => dispatch(hideModal()), + removeAccount: (address) => dispatch(removeAccount(address)), + } +} + +export default connect(null, mapDispatchToProps)(ConfirmRemoveAccount) diff --git a/ui/app/components/modals/confirm-remove-account/index.js b/ui/app/components/modals/confirm-remove-account/index.js new file mode 100644 index 000000000..9763fbe05 --- /dev/null +++ b/ui/app/components/modals/confirm-remove-account/index.js @@ -0,0 +1,2 @@ +import ConfirmRemoveAccount from './confirm-remove-account.container' +module.exports = ConfirmRemoveAccount diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss index ad6fe16d3..591e35148 100644 --- a/ui/app/components/modals/index.scss +++ b/ui/app/components/modals/index.scss @@ -17,6 +17,17 @@ text-align: center; font-size: .875rem; } + + &__address { + text-align: center; + font-size: 1rem; + margin-top: 20px; + margin-bottom: 20px; + } + + &__link { + color: #2f9ae0; + } &__content { overflow-y: auto; diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 85e85597a..758cfa4a2 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -1,4 +1,5 @@ -const Component = require('react').Component +const React = require('react') +const Component = React.Component const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect @@ -20,6 +21,7 @@ const HideTokenConfirmationModal = require('./hide-token-confirmation-modal') const CustomizeGasModal = require('../customize-gas-modal') const NotifcationModal = require('./notification-modal') const ConfirmResetAccount = require('./confirm-reset-account') +const ConfirmRemoveAccount = require('./confirm-remove-account') const TransactionConfirmed = require('./transaction-confirmed') const WelcomeBeta = require('./welcome-beta') const Notification = require('./notification') @@ -241,6 +243,19 @@ const MODALS = { }, }, + CONFIRM_FORGET_ACCOUNT: { + contents: h(ConfirmRemoveAccount), + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, + NEW_ACCOUNT: { contents: [ h(NewAccountModal, {}, []), @@ -370,7 +385,7 @@ Modal.prototype.render = function () { backdropStyle: BACKDROPSTYLE, closeOnClick: !disableBackdropClick, }, - children, + React.cloneElement(children, {...this.props.modalState.props}, null), ) } diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss index 20fc68424..ba5d176ef 100644 --- a/ui/app/css/itcss/components/account-menu.scss +++ b/ui/app/css/itcss/components/account-menu.scss @@ -88,7 +88,8 @@ .forget-account-icon { width: 25px; - margin-left: 10px; + padding-left: 10px; + height: 25px; } &:hover { diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index 7dfa839ab..66eb47378 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -156,6 +156,7 @@ .hw-connect { &__title { padding-top: 10px; + font-weight: 500; } &__msg { -- cgit From 523cf9ad33d88719520ae5e7293329d133b64d4d Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Wed, 11 Jul 2018 00:20:40 -0400 Subject: account removal is working --- ui/app/actions.js | 21 +++++++++++++++++++++ ui/app/components/account-menu/index.js | 20 ++++++++++---------- .../confirm-remove-account.container.js | 8 +++++++- ui/app/components/modals/modal.js | 5 ++--- 4 files changed, 40 insertions(+), 14 deletions(-) (limited to 'ui') diff --git a/ui/app/actions.js b/ui/app/actions.js index f04de8fe8..3bdd548d3 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -83,6 +83,7 @@ var actions = { NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', navigateToNewAccountScreen, resetAccount, + removeAccount, showNewVaultSeed: showNewVaultSeed, showInfoPage: showInfoPage, CLOSE_WELCOME_SCREEN: 'CLOSE_WELCOME_SCREEN', @@ -535,6 +536,26 @@ function resetAccount () { } } +function removeAccount (address) { + return dispatch => { + dispatch(actions.showLoadingIndication()) + + return new Promise((resolve, reject) => { + background.removeAccount(address, (err, account) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + log.info('Account removed: ' + account) + dispatch(actions.showAccountsPage()) + resolve() + }) + }) + } +} + function addNewKeyring (type, opts) { return (dispatch) => { dispatch(actions.showLoadingIndication()) diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index b561ea186..73450c1bd 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -68,7 +68,7 @@ function mapDispatchToProps (dispatch) { dispatch(actions.hideSidebar()) dispatch(actions.toggleAccountMenu()) }, - showForgetAccountConfirmationModal: (address) => { + showRemoveAccountConfirmationModal: (address) => { return dispatch(actions.showModal({ name: 'CONFIRM_FORGET_ACCOUNT', address })) }, } @@ -156,7 +156,8 @@ AccountMenu.prototype.renderAccounts = function () { } = this.props const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), []) - return accountOrder.map((address) => { + return accountOrder.filter(address => !!identities[address]).map((address) => { + const identity = identities[address] const isSelected = identity.address === selectedAddress @@ -191,25 +192,24 @@ AccountMenu.prototype.renderAccounts = function () { ]), this.renderKeyringType(keyring), - this.renderForgetAccount(keyring, identity.address), + this.renderRemoveAccount(keyring, identity.address), ], ) }) } -AccountMenu.prototype.renderForgetAccount = function (keyring, address) { +AccountMenu.prototype.renderRemoveAccount = function (keyring, address) { // Any account that's not form the HD wallet can be forgotten const type = keyring.type - const isForgetable = type !== 'HD Key Tree' - return isForgetable ? h('a.forget-account-icon', { onClick: (e) => this.forgetAccount(e, address) }, '') : null + const isRemovable = type !== 'HD Key Tree' + return isRemovable ? h('a.forget-account-icon', { onClick: (e) => this.removeAccount(e, address) }, '') : null } -AccountMenu.prototype.forgetAccount = function (e, address) { +AccountMenu.prototype.removeAccount = function (e, address) { e.preventDefault() e.stopPropagation() - const { showForgetAccountConfirmationModal } = this.props - console.log('should forget address: ', address) - showForgetAccountConfirmationModal(address) + const { showRemoveAccountConfirmationModal } = this.props + showRemoveAccountConfirmationModal(address) } AccountMenu.prototype.renderKeyringType = function (keyring) { diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js index 9a612f2f6..fcb149b3f 100644 --- a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js +++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js @@ -3,6 +3,12 @@ import ConfirmRemoveAccount from './confirm-remove-account.component' const { hideModal, removeAccount } = require('../../../actions') +const mapStateToProps = state => { + return { + address: state.appState.modal.modalState.props.address, + } +} + const mapDispatchToProps = dispatch => { return { hideModal: () => dispatch(hideModal()), @@ -10,4 +16,4 @@ const mapDispatchToProps = dispatch => { } } -export default connect(null, mapDispatchToProps)(ConfirmRemoveAccount) +export default connect(mapStateToProps, mapDispatchToProps)(ConfirmRemoveAccount) diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 758cfa4a2..9ace56661 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -1,5 +1,4 @@ -const React = require('react') -const Component = React.Component +const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect @@ -385,7 +384,7 @@ Modal.prototype.render = function () { backdropStyle: BACKDROPSTYLE, closeOnClick: !disableBackdropClick, }, - React.cloneElement(children, {...this.props.modalState.props}, null), + children, ) } -- cgit From 5a2a34591f8ab2aec3a056d5bb9e38ba5236cd07 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Wed, 11 Jul 2018 01:35:37 -0400 Subject: clean up --- ui/app/components/account-menu/index.js | 6 +++--- ui/app/components/modals/modal.js | 2 +- ui/app/css/itcss/components/account-menu.scss | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'ui') diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index 73450c1bd..c15ecbc9c 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -69,7 +69,7 @@ function mapDispatchToProps (dispatch) { dispatch(actions.toggleAccountMenu()) }, showRemoveAccountConfirmationModal: (address) => { - return dispatch(actions.showModal({ name: 'CONFIRM_FORGET_ACCOUNT', address })) + return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', address })) }, } } @@ -199,10 +199,10 @@ AccountMenu.prototype.renderAccounts = function () { } AccountMenu.prototype.renderRemoveAccount = function (keyring, address) { - // Any account that's not form the HD wallet can be forgotten + // Any account that's not from the HD wallet Keyring can be removed const type = keyring.type const isRemovable = type !== 'HD Key Tree' - return isRemovable ? h('a.forget-account-icon', { onClick: (e) => this.removeAccount(e, address) }, '') : null + return isRemovable ? h('a.remove-account-icon', { onClick: (e) => this.removeAccount(e, address) }, '') : null } AccountMenu.prototype.removeAccount = function (e, address) { diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 9ace56661..e40944165 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -242,7 +242,7 @@ const MODALS = { }, }, - CONFIRM_FORGET_ACCOUNT: { + CONFIRM_REMOVE_ACCOUNT: { contents: h(ConfirmRemoveAccount), mobileModalStyle: { ...modalContainerMobileStyle, diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss index ba5d176ef..9bce812e6 100644 --- a/ui/app/css/itcss/components/account-menu.scss +++ b/ui/app/css/itcss/components/account-menu.scss @@ -86,14 +86,14 @@ padding: 12px 14px; } - .forget-account-icon { + .remove-account-icon { width: 25px; padding-left: 10px; height: 25px; } &:hover { - .forget-account-icon::after { + .remove-account-icon::after { content: '\00D7'; font-size: 25px; color: $white; -- cgit From 80e875308b4447ed38d7e0f677570d73956dd9de Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Wed, 11 Jul 2018 21:21:36 -0400 Subject: forget device and autiload account features added --- ui/app/actions.js | 44 ++++++++++++++++++++++ .../connect-hardware/account-list.js | 10 +++++ .../pages/create-account/connect-hardware/index.js | 31 ++++++++++++++- ui/app/css/itcss/components/new-account.scss | 11 ++++++ 4 files changed, 95 insertions(+), 1 deletion(-) (limited to 'ui') diff --git a/ui/app/actions.js b/ui/app/actions.js index 3bdd548d3..9330a864b 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -79,6 +79,8 @@ var actions = { importNewAccount, addNewAccount, connectHardware, + checkHardwareStatus, + forgetDevice, unlockTrezorAccount, NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', navigateToNewAccountScreen, @@ -622,6 +624,48 @@ function addNewAccount () { } } +function checkHardwareStatus (deviceName) { + log.debug(`background.checkHardwareStatus`, deviceName) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.checkHardwareStatus(deviceName, (err, unlocked) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + + forceUpdateMetamaskState(dispatch) + return resolve(unlocked) + }) + }) + } +} + +function forgetDevice (deviceName) { + log.debug(`background.forgetDevice`, deviceName) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.forgetDevice(deviceName, (err, response) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + + forceUpdateMetamaskState(dispatch) + return resolve() + }) + }) + } +} + function connectHardware (deviceName, page) { log.debug(`background.connectHardware`, deviceName, page) return (dispatch, getState) => { diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js index 170d8f0b3..3bd6a00a7 100644 --- a/ui/app/components/pages/create-account/connect-hardware/account-list.js +++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js @@ -90,11 +90,20 @@ class AccountList extends Component { ]) } + renderForgetDevice () { + return h('div.hw-forget-device-container', {}, [ + h('a', { + onClick: this.props.onForgetDevice.bind(this), + }, this.context.t('forgetDevice')), + ]) + } + render () { return h('div', {}, [ this.renderAccounts(), this.renderPagination(), this.renderButtons(), + this.renderForgetDevice(), ]) } @@ -104,6 +113,7 @@ class AccountList extends Component { AccountList.propTypes = { accounts: PropTypes.array.isRequired, onAccountChange: PropTypes.func.isRequired, + onForgetDevice: PropTypes.func.isRequired, getPage: PropTypes.func.isRequired, network: PropTypes.string, selectedAccount: PropTypes.string, diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index 126102235..9aef36cfb 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -13,13 +13,19 @@ class ConnectHardwareForm extends Component { super(props) this.state = { error: null, - response: null, btnText: context.t('connectToTrezor'), selectedAccount: null, accounts: [], } } + async componentDidMount () { + const unlocked = await this.props.checkHardwareStatus('trezor') + if (unlocked) { + this.getPage(0) + } + } + connectToTrezor = () => { if (this.state.accounts.length) { return null @@ -70,6 +76,20 @@ class ConnectHardwareForm extends Component { }) } + onForgetDevice = () => { + this.props.forgetDevice('trezor') + .then(_ => { + this.setState({ + error: null, + btnText: this.context.t('connectToTrezor'), + selectedAccount: null, + accounts: [], + }) + }).catch(e => { + this.setState({ error: e.toString() }) + }) + } + onUnlockAccount = () => { if (this.state.selectedAccount === null) { @@ -110,6 +130,7 @@ class ConnectHardwareForm extends Component { getPage: this.getPage, history: this.props.history, onUnlockAccount: this.onUnlockAccount, + onForgetDevice: this.onForgetDevice, onCancel: this.onCancel, }) } @@ -127,6 +148,8 @@ ConnectHardwareForm.propTypes = { showImportPage: PropTypes.func, showConnectPage: PropTypes.func, connectHardware: PropTypes.func, + checkHardwareStatus: PropTypes.func, + forgetDevice: PropTypes.func, unlockTrezorAccount: PropTypes.func, numberOfExistingAccounts: PropTypes.number, history: PropTypes.object, @@ -154,6 +177,12 @@ const mapDispatchToProps = dispatch => { connectHardware: (deviceName, page) => { return dispatch(actions.connectHardware(deviceName, page)) }, + checkHardwareStatus: (deviceName) => { + return dispatch(actions.checkHardwareStatus(deviceName)) + }, + forgetDevice: (deviceName) => { + return dispatch(actions.forgetDevice(deviceName)) + }, unlockTrezorAccount: index => { return dispatch(actions.unlockTrezorAccount(index)) }, diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index 66eb47378..e11c10dfe 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -270,6 +270,17 @@ } } +.hw-forget-device-container { + display: flex; + flex-flow: column; + align-items: center; + padding: 30px 30px 0; + + a { + color: #2f9ae0; + cursor: pointer; + } +} .new-account-create-form { display: flex; -- cgit From 2a0a7853249284cb27831890f3b62847ea27eb83 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Thu, 12 Jul 2018 00:23:08 -0400 Subject: added tooltip --- ui/app/components/account-menu/index.js | 13 ++++++++++++- .../confirm-remove-account.component.js | 2 +- ui/app/css/itcss/components/account-menu.scss | 6 +++--- 3 files changed, 16 insertions(+), 5 deletions(-) (limited to 'ui') diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index c15ecbc9c..c5b577cfd 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -11,6 +11,7 @@ const Identicon = require('../identicon') const { formatBalance } = require('../../util') const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') const { getEnvironmentType } = require('../../../../app/scripts/lib/util') +const Tooltip = require('../tooltip') const { @@ -202,7 +203,17 @@ AccountMenu.prototype.renderRemoveAccount = function (keyring, address) { // Any account that's not from the HD wallet Keyring can be removed const type = keyring.type const isRemovable = type !== 'HD Key Tree' - return isRemovable ? h('a.remove-account-icon', { onClick: (e) => this.removeAccount(e, address) }, '') : null + if (isRemovable) { + return h(Tooltip, { + title: this.context.t('removeAccount'), + position: 'bottom', + }, [ + h('a.remove-account-icon', { + onClick: (e) => this.removeAccount(e, address), + }, ''), + ]) + } + return null } AccountMenu.prototype.removeAccount = function (e, address) { diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js index 93be2a4e7..d6c0c796d 100644 --- a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js +++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js @@ -26,7 +26,7 @@ class ConfirmRemoveAccount extends Component {
- { `${t('removeAccount')}` } + { `${t('removeAccount')}` }?
{addressSummary(this.props.address)} diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss index 9bce812e6..b14753e23 100644 --- a/ui/app/css/itcss/components/account-menu.scss +++ b/ui/app/css/itcss/components/account-menu.scss @@ -87,9 +87,9 @@ } .remove-account-icon { - width: 25px; - padding-left: 10px; - height: 25px; + width: 15px; + margin-left: 10px; + height: 15px; } &:hover { -- cgit From 4b528405eac7cea54c743307e6f577abd6ce9507 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Thu, 12 Jul 2018 13:19:51 -0400 Subject: catching up with develop --- ui/app/components/account-menu/index.js | 14 +++---- .../confirm-remove-account.component.js | 44 +++++++++++++++++++--- .../confirm-remove-account.container.js | 3 +- 3 files changed, 48 insertions(+), 13 deletions(-) (limited to 'ui') diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index c5b577cfd..fc48b60f3 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -69,8 +69,8 @@ function mapDispatchToProps (dispatch) { dispatch(actions.hideSidebar()) dispatch(actions.toggleAccountMenu()) }, - showRemoveAccountConfirmationModal: (address) => { - return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', address })) + showRemoveAccountConfirmationModal: (identity) => { + return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity })) }, } } @@ -193,13 +193,13 @@ AccountMenu.prototype.renderAccounts = function () { ]), this.renderKeyringType(keyring), - this.renderRemoveAccount(keyring, identity.address), + this.renderRemoveAccount(keyring, identity), ], ) }) } -AccountMenu.prototype.renderRemoveAccount = function (keyring, address) { +AccountMenu.prototype.renderRemoveAccount = function (keyring, identity) { // Any account that's not from the HD wallet Keyring can be removed const type = keyring.type const isRemovable = type !== 'HD Key Tree' @@ -209,18 +209,18 @@ AccountMenu.prototype.renderRemoveAccount = function (keyring, address) { position: 'bottom', }, [ h('a.remove-account-icon', { - onClick: (e) => this.removeAccount(e, address), + onClick: (e) => this.removeAccount(e, identity), }, ''), ]) } return null } -AccountMenu.prototype.removeAccount = function (e, address) { +AccountMenu.prototype.removeAccount = function (e, identity) { e.preventDefault() e.stopPropagation() const { showRemoveAccountConfirmationModal } = this.props - showRemoveAccountConfirmationModal(address) + showRemoveAccountConfirmationModal(identity) } AccountMenu.prototype.renderKeyringType = function (keyring) { diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js index d6c0c796d..b9dc6364f 100644 --- a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js +++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js @@ -2,12 +2,15 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import Button from '../../button' import { addressSummary } from '../../../util' +import Identicon from '../../identicon' +import genAccountLink from '../../../../lib/account-link' class ConfirmRemoveAccount extends Component { static propTypes = { hideModal: PropTypes.func.isRequired, removeAccount: PropTypes.func.isRequired, - address: PropTypes.string.isRequired, + identity: PropTypes.object.isRequired, + network: PropTypes.string.isRequired, } static contextTypes = { @@ -15,10 +18,43 @@ class ConfirmRemoveAccount extends Component { } handleRemove () { - this.props.removeAccount(this.props.address) + this.props.removeAccount(this.props.identity.address) .then(() => this.props.hideModal()) } + renderSelectedAccount () { + const { identity } = this.props + return ( +
+
+ +
+
+ Name + {identity.name} +
+
+ Public Address + { addressSummary(identity.address) } +
+
+ + + +
+
+ ) + } + render () { const { t } = this.context @@ -28,9 +64,7 @@ class ConfirmRemoveAccount extends Component {
{ `${t('removeAccount')}` }?
-
- {addressSummary(this.props.address)} -
+ { this.renderSelectedAccount() }
{ t('removeAccountDescription') } { t('learnMore') } diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js index fcb149b3f..4b194c995 100644 --- a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js +++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js @@ -5,7 +5,8 @@ const { hideModal, removeAccount } = require('../../../actions') const mapStateToProps = state => { return { - address: state.appState.modal.modalState.props.address, + identity: state.appState.modal.modalState.props.identity, + network: state.metamask.network, } } -- cgit From 5710e648bd77aa7be6e9a4ba1d7d3fe4ea20c010 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Thu, 12 Jul 2018 18:19:33 -0400 Subject: remove account modal updated --- .../confirm-remove-account.component.js | 19 ++++---- ui/app/components/modals/index.scss | 51 +++++++++++++++++++--- 2 files changed, 55 insertions(+), 15 deletions(-) (limited to 'ui') diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js index b9dc6364f..5a9f0f289 100644 --- a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js +++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js @@ -25,25 +25,24 @@ class ConfirmRemoveAccount extends Component { renderSelectedAccount () { const { identity } = this.props return ( -
-
+
+
-
- Name +
+ Name {identity.name}
-
- Public Address - { addressSummary(identity.address) } +
+ Public Address + { addressSummary(identity.address, 4, 4) }
-
+
Date: Fri, 13 Jul 2018 00:09:42 -0400 Subject: fix account balance bug --- ui/app/components/modals/index.scss | 2 +- .../pages/create-account/connect-hardware/index.js | 24 ++++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) (limited to 'ui') diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss index 1cefcb49c..e198cca44 100644 --- a/ui/app/components/modals/index.scss +++ b/ui/app/components/modals/index.scss @@ -53,7 +53,7 @@ } &__link { - margin-top: 16px; + margin-top: 14px; img { width: 15px; diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index 9aef36cfb..fa9cf4894 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -19,6 +19,18 @@ class ConnectHardwareForm extends Component { } } + componentWillReceiveProps (nextProps) { + const { accounts } = nextProps + const newAccounts = this.state.accounts.map(a => { + const normalizedAddress = a.address.toLowerCase() + const balanceValue = accounts[normalizedAddress] && accounts[normalizedAddress].balance || null + a.balance = balanceValue ? formatBalance(balanceValue, 6) : '...' + return a + }) + this.setState({accounts: newAccounts}) + } + + async componentDidMount () { const unlocked = await this.props.checkHardwareStatus('trezor') if (unlocked) { @@ -38,14 +50,6 @@ class ConnectHardwareForm extends Component { this.setState({selectedAccount: account.toString(), error: null}) } - getBalance (address) { - // Get the balance - const { accounts } = this.props - const balanceValue = accounts && accounts[address.toLowerCase()] ? accounts[address.toLowerCase()].balance : '' - const formattedBalance = balanceValue !== null ? formatBalance(balanceValue, 6) : '...' - return formattedBalance - } - getPage = (page) => { this.props .connectHardware('trezor', page) @@ -64,7 +68,9 @@ class ConnectHardwareForm extends Component { // Map accounts with balances newState.accounts = accounts.map(account => { - account.balance = this.getBalance(account.address) + const normalizedAddress = account.address.toLowerCase() + const balanceValue = this.props.accounts[normalizedAddress] && this.props.accounts[normalizedAddress].balance || null + account.balance = balanceValue ? formatBalance(balanceValue, 6) : '...' return account }) -- cgit From 82a93bb2872809df19744afdd95c68485c0dcd25 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Fri, 13 Jul 2018 02:03:54 -0400 Subject: detect ability to open popup instead of browser --- .../create-account/connect-hardware/connect-screen.js | 14 ++++++++------ .../pages/create-account/connect-hardware/index.js | 5 +++++ 2 files changed, 13 insertions(+), 6 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js index dd9fdfba2..0dd8e285b 100644 --- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -16,9 +16,11 @@ class ConnectScreen extends Component { ]), h( 'button.btn-primary.btn--large', - { onClick: () => global.platform.openWindow({ - url: 'https://google.com/chrome', - }), style: { margin: 12 } }, + { + onClick: () => global.platform.openWindow({ + url: 'https://google.com/chrome', + }), + }, this.context.t('downloadGoogleChrome') ), ]) @@ -49,8 +51,7 @@ class ConnectScreen extends Component { } render () { - const isChrome = window.navigator.userAgent.search('Chrome') !== -1 - if (isChrome) { + if (this.props.browserSupported) { return this.renderConnectScreen() } return this.renderUnsupportedBrowser() @@ -59,7 +60,8 @@ class ConnectScreen extends Component { ConnectScreen.propTypes = { connectToTrezor: PropTypes.func.isRequired, - btnText: PropTypes.string, + btnText: PropTypes.string.isRequired, + browserSupported: PropTypes.bool.isRequired, } ConnectScreen.contextTypes = { diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index fa9cf4894..689e88f2e 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -16,6 +16,7 @@ class ConnectHardwareForm extends Component { btnText: context.t('connectToTrezor'), selectedAccount: null, accounts: [], + browserSupported: true, } } @@ -78,6 +79,9 @@ class ConnectHardwareForm extends Component { } }) .catch(e => { + if (e === 'Window blocked') { + this.setState({ browserSupported: false }) + } this.setState({ btnText: this.context.t('connectToTrezor') }) }) } @@ -125,6 +129,7 @@ class ConnectHardwareForm extends Component { return h(ConnectScreen, { connectToTrezor: this.connectToTrezor, btnText: this.state.btnText, + browserSupported: this.state.browserSupported, }) } -- cgit From 53995463883c062157a3d725e7cb8fe54486badb Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Fri, 13 Jul 2018 13:49:20 -0400 Subject: added affiliate link to trezor --- .../create-account/connect-hardware/connect-screen.js | 6 ++++++ ui/app/css/itcss/components/new-account.scss | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js index 0dd8e285b..8d9980b10 100644 --- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -46,6 +46,12 @@ class ConnectScreen extends Component { { onClick: this.props.connectToTrezor.bind(this) }, this.props.btnText ), + h('div.hw-connect__get-trezor', {}, [ + h('a', { + href: 'https://shop.trezor.io/?a=metamask', + target: '_blank', + }, this.context.t('getYourTrezor')), + ]), ]) ) } diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index e11c10dfe..4552f0bf2 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -169,6 +169,21 @@ &__link { color: #2f9ae0; } + + &__get-trezor { + display: flex; + flex-flow: column; + align-items: center; + padding: 30px 30px 0; + width: 305px; + + a { + font-size: 14px; + text-align: center; + color: #2f9ae0; + cursor: pointer; + } + } } .hw-account-list { @@ -278,6 +293,7 @@ a { color: #2f9ae0; + font-size: 14px; cursor: pointer; } } -- cgit From 55382e9842c4f4136c88e441298193cc7abd8ba9 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Fri, 13 Jul 2018 15:19:21 -0400 Subject: fix account selection --- ui/app/components/pages/create-account/connect-hardware/index.js | 8 ++++++-- ui/app/css/itcss/components/new-account.scss | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index 689e88f2e..dc9907f31 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -59,8 +59,11 @@ class ConnectHardwareForm extends Component { const newState = {} // Default to the first account if (this.state.selectedAccount === null) { - const firstAccount = accounts[0] - newState.selectedAccount = firstAccount.index.toString() === '0' ? firstAccount.index.toString() : null + accounts.forEach((a, i) => { + if (a.address.toLowerCase() === this.props.address) { + newState.selectedAccount = a.index.toString() + } + }) // If the page doesn't contain the selected account, let's deselect it } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) { newState.selectedAccount = null @@ -167,6 +170,7 @@ ConnectHardwareForm.propTypes = { t: PropTypes.func, network: PropTypes.string, accounts: PropTypes.object, + address: PropTypes.string, } const mapStateToProps = state => { diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index 4552f0bf2..a44fab3be 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -242,7 +242,7 @@ &__item__index { display: flex; - margin-right: 20px; + width: 28px; } &__item__radio { @@ -337,4 +337,8 @@ width: 150px; min-width: initial; } + + &__button.btn-primary--disabled { + cursor: 'not-allowed'; + } } -- cgit From b3d78ed8a1fbea059344b04416fb21bdb1b73f86 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 20 Jun 2018 13:18:23 -0230 Subject: Remove send_ directory, revert to just having send Revert accidentally changed constants. Require defaults in ens-input, gas-fee-display and confirm screens. --- ui/app/actions.js | 2 +- ui/app/app.js | 2 +- ui/app/components/customize-gas-modal/index.js | 6 +- .../components/dropdowns/account-dropdown-mini.js | 2 +- ui/app/components/ens-input.js | 2 +- .../pending-tx/confirm-deploy-contract.js | 2 +- ui/app/components/pending-tx/confirm-send-ether.js | 6 +- ui/app/components/pending-tx/confirm-send-token.js | 6 +- ui/app/components/send/README.md | 0 .../account-list-item/account-list-item-README.md | 0 .../account-list-item.component.js | 73 +++ .../account-list-item.container.js | 15 + .../send/account-list-item/account-list-item.scss | 0 ui/app/components/send/account-list-item/index.js | 1 + .../tests/account-list-item-component.test.js | 138 +++++ .../tests/account-list-item-container.test.js | 32 + ui/app/components/send/currency-display.js | 169 ----- .../send/currency-display/currency-display.js | 169 +++++ ui/app/components/send/currency-display/index.js | 1 + ui/app/components/send/index.js | 1 + ui/app/components/send/send-content/index.js | 1 + .../send/send-content/send-amount-row/README.md | 0 .../amount-max-button.component.js | 54 ++ .../amount-max-button.container.js | 40 ++ .../amount-max-button.selectors.js | 9 + .../amount-max-button/amount-max-button.utils.js | 22 + .../send-amount-row/amount-max-button/index.js | 1 + .../tests/amount-max-button-component.test.js | 90 +++ .../tests/amount-max-button-container.test.js | 91 +++ .../tests/amount-max-button-selectors.test.js | 22 + .../tests/amount-max-button-utils.test.js | 27 + .../send/send-content/send-amount-row/index.js | 1 + .../send-amount-row/send-amount-row.component.js | 123 ++++ .../send-amount-row/send-amount-row.container.js | 54 ++ .../send-amount-row/send-amount-row.scss | 0 .../send-amount-row/send-amount-row.selectors.js | 9 + .../tests/send-amount-row-component.test.js | 202 ++++++ .../tests/send-amount-row-container.test.js | 125 ++++ .../tests/send-amount-row-selectors.test.js | 34 + .../send/send-content/send-content-README.md | 0 .../send/send-content/send-content.component.js | 28 + .../components/send/send-content/send-content.scss | 0 .../send/send-content/send-dropdown-list/index.js | 1 + .../send-dropdown-list.component.js | 52 ++ .../tests/send-dropdown-list-component.test.js | 105 ++++ .../from-dropdown/from-dropdown-README.md | 0 .../from-dropdown/from-dropdown.component.js | 46 ++ .../send-from-row/from-dropdown/from-dropdown.scss | 0 .../send-from-row/from-dropdown/index.js | 1 + .../tests/from-dropdown-component.test.js | 88 +++ .../send/send-content/send-from-row/index.js | 1 + .../send-from-row/send-from-row-README.md | 0 .../send-from-row/send-from-row.component.js | 63 ++ .../send-from-row/send-from-row.container.js | 46 ++ .../send-from-row/send-from-row.selectors.js | 9 + .../tests/send-from-row-component.test.js | 121 ++++ .../tests/send-from-row-container.test.js | 110 ++++ .../tests/send-from-row-selectors.test.js | 20 + .../send/send-content/send-gas-row/README.md | 0 .../gas-fee-display/gas-fee-display.component.js | 61 ++ .../send-gas-row/gas-fee-display/index.js | 1 + .../test/gas-fee-display.component.test.js | 55 ++ .../send/send-content/send-gas-row/index.js | 1 + .../send-gas-row/send-gas-row.component.js | 48 ++ .../send-gas-row/send-gas-row.container.js | 27 + .../send-content/send-gas-row/send-gas-row.scss | 0 .../send-gas-row/send-gas-row.selectors.js | 14 + .../tests/send-gas-row-component.test.js | 70 +++ .../tests/send-gas-row-container.test.js | 70 +++ .../tests/send-gas-row-selectors.test.js | 49 ++ .../send/send-content/send-row-wrapper/index.js | 1 + .../send-row-error-message/index.js | 1 + .../send-row-error-message-README.md | 0 .../send-row-error-message.component.js | 27 + .../send-row-error-message.container.js | 12 + .../send-row-error-message.scss | 0 .../tests/send-row-error-message-component.test.js | 28 + .../tests/send-row-error-message-container.test.js | 28 + .../send-row-wrapper/send-row-wrapper-README.md | 0 .../send-row-wrapper/send-row-wrapper.component.js | 43 ++ .../send-row-wrapper/send-row-wrapper.scss | 0 .../tests/send-row-wrapper-component.test.js | 79 +++ .../send/send-content/send-to-row/index.js | 1 + .../send-content/send-to-row/send-to-row-README.md | 0 .../send-to-row/send-to-row.component.js | 69 +++ .../send-to-row/send-to-row.container.js | 42 ++ .../send-to-row/send-to-row.selectors.js | 14 + .../send-content/send-to-row/send-to-row.utils.js | 19 + .../tests/send-to-row-component.test.js | 149 +++++ .../tests/send-to-row-container.test.js | 113 ++++ .../tests/send-to-row-selectors.test.js | 47 ++ .../send-to-row/tests/send-to-row-utils.test.js | 51 ++ .../tests/send-content-component.test.js | 38 ++ ui/app/components/send/send-footer/README.md | 0 ui/app/components/send/send-footer/index.js | 1 + .../send/send-footer/send-footer.component.js | 101 +++ .../send/send-footer/send-footer.container.js | 100 +++ .../components/send/send-footer/send-footer.scss | 0 .../send/send-footer/send-footer.selectors.js | 11 + .../send/send-footer/send-footer.utils.js | 81 +++ .../tests/send-footer-component.test.js | 230 +++++++ .../tests/send-footer-container.test.js | 191 ++++++ .../tests/send-footer-selectors.test.js | 24 + .../send-footer/tests/send-footer-utils.test.js | 210 +++++++ ui/app/components/send/send-header/README.md | 0 ui/app/components/send/send-header/index.js | 1 + .../send/send-header/send-header.component.js | 34 + .../send/send-header/send-header.container.js | 19 + .../send/send-header/send-header.selectors.js | 37 ++ .../tests/send-header-component.test.js | 70 +++ .../tests/send-header-container.test.js | 59 ++ .../tests/send-header-selectors.test.js | 47 ++ ui/app/components/send/send.component.js | 179 ++++++ ui/app/components/send/send.constants.js | 57 ++ ui/app/components/send/send.container.js | 93 +++ ui/app/components/send/send.scss | 0 ui/app/components/send/send.selectors.js | 279 +++++++++ ui/app/components/send/send.utils.js | 312 ++++++++++ .../components/send/tests/send-component.test.js | 332 ++++++++++ .../components/send/tests/send-container.test.js | 169 +++++ .../send/tests/send-selectors-test-data.js | 230 +++++++ .../components/send/tests/send-selectors.test.js | 685 +++++++++++++++++++++ ui/app/components/send/tests/send-utils.test.js | 486 +++++++++++++++ ui/app/components/send/to-autocomplete/index.js | 1 + .../send/to-autocomplete/to-autocomplete.js | 120 ++++ ui/app/components/send_/README.md | 0 .../account-list-item/account-list-item-README.md | 0 .../account-list-item.component.js | 73 --- .../account-list-item.container.js | 15 - .../send_/account-list-item/account-list-item.scss | 0 ui/app/components/send_/account-list-item/index.js | 1 - .../tests/account-list-item-component.test.js | 138 ----- .../tests/account-list-item-container.test.js | 32 - ui/app/components/send_/index.js | 1 - ui/app/components/send_/send-content/index.js | 1 - .../send_/send-content/send-amount-row/README.md | 0 .../amount-max-button.component.js | 54 -- .../amount-max-button.container.js | 40 -- .../amount-max-button.selectors.js | 9 - .../amount-max-button/amount-max-button.utils.js | 22 - .../send-amount-row/amount-max-button/index.js | 1 - .../tests/amount-max-button-component.test.js | 90 --- .../tests/amount-max-button-container.test.js | 91 --- .../tests/amount-max-button-selectors.test.js | 22 - .../tests/amount-max-button-utils.test.js | 27 - .../send_/send-content/send-amount-row/index.js | 1 - .../send-amount-row/send-amount-row.component.js | 123 ---- .../send-amount-row/send-amount-row.container.js | 54 -- .../send-amount-row/send-amount-row.scss | 0 .../send-amount-row/send-amount-row.selectors.js | 9 - .../tests/send-amount-row-component.test.js | 202 ------ .../tests/send-amount-row-container.test.js | 125 ---- .../tests/send-amount-row-selectors.test.js | 34 - .../send_/send-content/send-content-README.md | 0 .../send_/send-content/send-content.component.js | 28 - .../send_/send-content/send-content.scss | 0 .../send_/send-content/send-dropdown-list/index.js | 1 - .../send-dropdown-list.component.js | 52 -- .../tests/send-dropdown-list-component.test.js | 105 ---- .../from-dropdown/from-dropdown-README.md | 0 .../from-dropdown/from-dropdown.component.js | 46 -- .../send-from-row/from-dropdown/from-dropdown.scss | 0 .../send-from-row/from-dropdown/index.js | 1 - .../tests/from-dropdown-component.test.js | 88 --- .../send_/send-content/send-from-row/index.js | 1 - .../send-from-row/send-from-row-README.md | 0 .../send-from-row/send-from-row.component.js | 63 -- .../send-from-row/send-from-row.container.js | 46 -- .../send-from-row/send-from-row.selectors.js | 9 - .../tests/send-from-row-component.test.js | 121 ---- .../tests/send-from-row-container.test.js | 110 ---- .../tests/send-from-row-selectors.test.js | 20 - .../send_/send-content/send-gas-row/README.md | 0 .../gas-fee-display/gas-fee-display.component.js | 61 -- .../send-gas-row/gas-fee-display/index.js | 1 - .../test/gas-fee-display.component.test.js | 55 -- .../send_/send-content/send-gas-row/index.js | 1 - .../send-gas-row/send-gas-row.component.js | 48 -- .../send-gas-row/send-gas-row.container.js | 27 - .../send-content/send-gas-row/send-gas-row.scss | 0 .../send-gas-row/send-gas-row.selectors.js | 14 - .../tests/send-gas-row-component.test.js | 70 --- .../tests/send-gas-row-container.test.js | 70 --- .../tests/send-gas-row-selectors.test.js | 49 -- .../send_/send-content/send-row-wrapper/index.js | 1 - .../send-row-error-message/index.js | 1 - .../send-row-error-message-README.md | 0 .../send-row-error-message.component.js | 27 - .../send-row-error-message.container.js | 12 - .../send-row-error-message.scss | 0 .../tests/send-row-error-message-component.test.js | 28 - .../tests/send-row-error-message-container.test.js | 28 - .../send-row-wrapper/send-row-wrapper-README.md | 0 .../send-row-wrapper/send-row-wrapper.component.js | 43 -- .../send-row-wrapper/send-row-wrapper.scss | 0 .../tests/send-row-wrapper-component.test.js | 79 --- .../send_/send-content/send-to-row/index.js | 1 - .../send-content/send-to-row/send-to-row-README.md | 0 .../send-to-row/send-to-row.component.js | 69 --- .../send-to-row/send-to-row.container.js | 42 -- .../send-to-row/send-to-row.selectors.js | 14 - .../send-content/send-to-row/send-to-row.utils.js | 19 - .../tests/send-to-row-component.test.js | 149 ----- .../tests/send-to-row-container.test.js | 113 ---- .../tests/send-to-row-selectors.test.js | 47 -- .../send-to-row/tests/send-to-row-utils.test.js | 51 -- .../tests/send-content-component.test.js | 38 -- ui/app/components/send_/send-footer/README.md | 0 ui/app/components/send_/send-footer/index.js | 1 - .../send_/send-footer/send-footer.component.js | 101 --- .../send_/send-footer/send-footer.container.js | 100 --- .../components/send_/send-footer/send-footer.scss | 0 .../send_/send-footer/send-footer.selectors.js | 11 - .../send_/send-footer/send-footer.utils.js | 81 --- .../tests/send-footer-component.test.js | 230 ------- .../tests/send-footer-container.test.js | 191 ------ .../tests/send-footer-selectors.test.js | 24 - .../send-footer/tests/send-footer-utils.test.js | 210 ------- ui/app/components/send_/send-header/README.md | 0 ui/app/components/send_/send-header/index.js | 1 - .../send_/send-header/send-header.component.js | 34 - .../send_/send-header/send-header.container.js | 19 - .../send_/send-header/send-header.selectors.js | 37 -- .../tests/send-header-component.test.js | 70 --- .../tests/send-header-container.test.js | 59 -- .../tests/send-header-selectors.test.js | 47 -- ui/app/components/send_/send.component.js | 179 ------ ui/app/components/send_/send.constants.js | 57 -- ui/app/components/send_/send.container.js | 93 --- ui/app/components/send_/send.scss | 0 ui/app/components/send_/send.selectors.js | 279 --------- ui/app/components/send_/send.utils.js | 312 ---------- .../components/send_/tests/send-component.test.js | 332 ---------- .../components/send_/tests/send-container.test.js | 169 ----- .../send_/tests/send-selectors-test-data.js | 230 ------- .../components/send_/tests/send-selectors.test.js | 685 --------------------- ui/app/components/send_/tests/send-utils.test.js | 486 --------------- 237 files changed, 7357 insertions(+), 7235 deletions(-) create mode 100644 ui/app/components/send/README.md create mode 100644 ui/app/components/send/account-list-item/account-list-item-README.md create mode 100644 ui/app/components/send/account-list-item/account-list-item.component.js create mode 100644 ui/app/components/send/account-list-item/account-list-item.container.js create mode 100644 ui/app/components/send/account-list-item/account-list-item.scss create mode 100644 ui/app/components/send/account-list-item/index.js create mode 100644 ui/app/components/send/account-list-item/tests/account-list-item-component.test.js create mode 100644 ui/app/components/send/account-list-item/tests/account-list-item-container.test.js delete mode 100644 ui/app/components/send/currency-display.js create mode 100644 ui/app/components/send/currency-display/currency-display.js create mode 100644 ui/app/components/send/currency-display/index.js create mode 100644 ui/app/components/send/index.js create mode 100644 ui/app/components/send/send-content/index.js create mode 100644 ui/app/components/send/send-content/send-amount-row/README.md create mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js create mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js create mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js create mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js create mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js create mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js create mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js create mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js create mode 100644 ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js create mode 100644 ui/app/components/send/send-content/send-amount-row/index.js create mode 100644 ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js create mode 100644 ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js create mode 100644 ui/app/components/send/send-content/send-amount-row/send-amount-row.scss create mode 100644 ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js create mode 100644 ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js create mode 100644 ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js create mode 100644 ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js create mode 100644 ui/app/components/send/send-content/send-content-README.md create mode 100644 ui/app/components/send/send-content/send-content.component.js create mode 100644 ui/app/components/send/send-content/send-content.scss create mode 100644 ui/app/components/send/send-content/send-dropdown-list/index.js create mode 100644 ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js create mode 100644 ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js create mode 100644 ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md create mode 100644 ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js create mode 100644 ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss create mode 100644 ui/app/components/send/send-content/send-from-row/from-dropdown/index.js create mode 100644 ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js create mode 100644 ui/app/components/send/send-content/send-from-row/index.js create mode 100644 ui/app/components/send/send-content/send-from-row/send-from-row-README.md create mode 100644 ui/app/components/send/send-content/send-from-row/send-from-row.component.js create mode 100644 ui/app/components/send/send-content/send-from-row/send-from-row.container.js create mode 100644 ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js create mode 100644 ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js create mode 100644 ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js create mode 100644 ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js create mode 100644 ui/app/components/send/send-content/send-gas-row/README.md create mode 100644 ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js create mode 100644 ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js create mode 100644 ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js create mode 100644 ui/app/components/send/send-content/send-gas-row/index.js create mode 100644 ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js create mode 100644 ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js create mode 100644 ui/app/components/send/send-content/send-gas-row/send-gas-row.scss create mode 100644 ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js create mode 100644 ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js create mode 100644 ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js create mode 100644 ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js create mode 100644 ui/app/components/send/send-content/send-row-wrapper/index.js create mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js create mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md create mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js create mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js create mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss create mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js create mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js create mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md create mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js create mode 100644 ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss create mode 100644 ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js create mode 100644 ui/app/components/send/send-content/send-to-row/index.js create mode 100644 ui/app/components/send/send-content/send-to-row/send-to-row-README.md create mode 100644 ui/app/components/send/send-content/send-to-row/send-to-row.component.js create mode 100644 ui/app/components/send/send-content/send-to-row/send-to-row.container.js create mode 100644 ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js create mode 100644 ui/app/components/send/send-content/send-to-row/send-to-row.utils.js create mode 100644 ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js create mode 100644 ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js create mode 100644 ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js create mode 100644 ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js create mode 100644 ui/app/components/send/send-content/tests/send-content-component.test.js create mode 100644 ui/app/components/send/send-footer/README.md create mode 100644 ui/app/components/send/send-footer/index.js create mode 100644 ui/app/components/send/send-footer/send-footer.component.js create mode 100644 ui/app/components/send/send-footer/send-footer.container.js create mode 100644 ui/app/components/send/send-footer/send-footer.scss create mode 100644 ui/app/components/send/send-footer/send-footer.selectors.js create mode 100644 ui/app/components/send/send-footer/send-footer.utils.js create mode 100644 ui/app/components/send/send-footer/tests/send-footer-component.test.js create mode 100644 ui/app/components/send/send-footer/tests/send-footer-container.test.js create mode 100644 ui/app/components/send/send-footer/tests/send-footer-selectors.test.js create mode 100644 ui/app/components/send/send-footer/tests/send-footer-utils.test.js create mode 100644 ui/app/components/send/send-header/README.md create mode 100644 ui/app/components/send/send-header/index.js create mode 100644 ui/app/components/send/send-header/send-header.component.js create mode 100644 ui/app/components/send/send-header/send-header.container.js create mode 100644 ui/app/components/send/send-header/send-header.selectors.js create mode 100644 ui/app/components/send/send-header/tests/send-header-component.test.js create mode 100644 ui/app/components/send/send-header/tests/send-header-container.test.js create mode 100644 ui/app/components/send/send-header/tests/send-header-selectors.test.js create mode 100644 ui/app/components/send/send.component.js create mode 100644 ui/app/components/send/send.constants.js create mode 100644 ui/app/components/send/send.container.js create mode 100644 ui/app/components/send/send.scss create mode 100644 ui/app/components/send/send.selectors.js create mode 100644 ui/app/components/send/send.utils.js create mode 100644 ui/app/components/send/tests/send-component.test.js create mode 100644 ui/app/components/send/tests/send-container.test.js create mode 100644 ui/app/components/send/tests/send-selectors-test-data.js create mode 100644 ui/app/components/send/tests/send-selectors.test.js create mode 100644 ui/app/components/send/tests/send-utils.test.js create mode 100644 ui/app/components/send/to-autocomplete/index.js create mode 100644 ui/app/components/send/to-autocomplete/to-autocomplete.js delete mode 100644 ui/app/components/send_/README.md delete mode 100644 ui/app/components/send_/account-list-item/account-list-item-README.md delete mode 100644 ui/app/components/send_/account-list-item/account-list-item.component.js delete mode 100644 ui/app/components/send_/account-list-item/account-list-item.container.js delete mode 100644 ui/app/components/send_/account-list-item/account-list-item.scss delete mode 100644 ui/app/components/send_/account-list-item/index.js delete mode 100644 ui/app/components/send_/account-list-item/tests/account-list-item-component.test.js delete mode 100644 ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js delete mode 100644 ui/app/components/send_/index.js delete mode 100644 ui/app/components/send_/send-content/index.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/README.md delete mode 100644 ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/index.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss delete mode 100644 ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js delete mode 100644 ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js delete mode 100644 ui/app/components/send_/send-content/send-content-README.md delete mode 100644 ui/app/components/send_/send-content/send-content.component.js delete mode 100644 ui/app/components/send_/send-content/send-content.scss delete mode 100644 ui/app/components/send_/send-content/send-dropdown-list/index.js delete mode 100644 ui/app/components/send_/send-content/send-dropdown-list/send-dropdown-list.component.js delete mode 100644 ui/app/components/send_/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js delete mode 100644 ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md delete mode 100644 ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js delete mode 100644 ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss delete mode 100644 ui/app/components/send_/send-content/send-from-row/from-dropdown/index.js delete mode 100644 ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js delete mode 100644 ui/app/components/send_/send-content/send-from-row/index.js delete mode 100644 ui/app/components/send_/send-content/send-from-row/send-from-row-README.md delete mode 100644 ui/app/components/send_/send-content/send-from-row/send-from-row.component.js delete mode 100644 ui/app/components/send_/send-content/send-from-row/send-from-row.container.js delete mode 100644 ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js delete mode 100644 ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js delete mode 100644 ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js delete mode 100644 ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js delete mode 100644 ui/app/components/send_/send-content/send-gas-row/README.md delete mode 100644 ui/app/components/send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js delete mode 100644 ui/app/components/send_/send-content/send-gas-row/gas-fee-display/index.js delete mode 100644 ui/app/components/send_/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js delete mode 100644 ui/app/components/send_/send-content/send-gas-row/index.js delete mode 100644 ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js delete mode 100644 ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js delete mode 100644 ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss delete mode 100644 ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js delete mode 100644 ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js delete mode 100644 ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js delete mode 100644 ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/index.js delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/index.js delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss delete mode 100644 ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js delete mode 100644 ui/app/components/send_/send-content/send-to-row/index.js delete mode 100644 ui/app/components/send_/send-content/send-to-row/send-to-row-README.md delete mode 100644 ui/app/components/send_/send-content/send-to-row/send-to-row.component.js delete mode 100644 ui/app/components/send_/send-content/send-to-row/send-to-row.container.js delete mode 100644 ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js delete mode 100644 ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js delete mode 100644 ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js delete mode 100644 ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js delete mode 100644 ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js delete mode 100644 ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js delete mode 100644 ui/app/components/send_/send-content/tests/send-content-component.test.js delete mode 100644 ui/app/components/send_/send-footer/README.md delete mode 100644 ui/app/components/send_/send-footer/index.js delete mode 100644 ui/app/components/send_/send-footer/send-footer.component.js delete mode 100644 ui/app/components/send_/send-footer/send-footer.container.js delete mode 100644 ui/app/components/send_/send-footer/send-footer.scss delete mode 100644 ui/app/components/send_/send-footer/send-footer.selectors.js delete mode 100644 ui/app/components/send_/send-footer/send-footer.utils.js delete mode 100644 ui/app/components/send_/send-footer/tests/send-footer-component.test.js delete mode 100644 ui/app/components/send_/send-footer/tests/send-footer-container.test.js delete mode 100644 ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js delete mode 100644 ui/app/components/send_/send-footer/tests/send-footer-utils.test.js delete mode 100644 ui/app/components/send_/send-header/README.md delete mode 100644 ui/app/components/send_/send-header/index.js delete mode 100644 ui/app/components/send_/send-header/send-header.component.js delete mode 100644 ui/app/components/send_/send-header/send-header.container.js delete mode 100644 ui/app/components/send_/send-header/send-header.selectors.js delete mode 100644 ui/app/components/send_/send-header/tests/send-header-component.test.js delete mode 100644 ui/app/components/send_/send-header/tests/send-header-container.test.js delete mode 100644 ui/app/components/send_/send-header/tests/send-header-selectors.test.js delete mode 100644 ui/app/components/send_/send.component.js delete mode 100644 ui/app/components/send_/send.constants.js delete mode 100644 ui/app/components/send_/send.container.js delete mode 100644 ui/app/components/send_/send.scss delete mode 100644 ui/app/components/send_/send.selectors.js delete mode 100644 ui/app/components/send_/send.utils.js delete mode 100644 ui/app/components/send_/tests/send-component.test.js delete mode 100644 ui/app/components/send_/tests/send-container.test.js delete mode 100644 ui/app/components/send_/tests/send-selectors-test-data.js delete mode 100644 ui/app/components/send_/tests/send-selectors.test.js delete mode 100644 ui/app/components/send_/tests/send-utils.test.js (limited to 'ui') diff --git a/ui/app/actions.js b/ui/app/actions.js index 1fb49c920..0aeb802f8 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -6,7 +6,7 @@ const { calcGasTotal, calcTokenBalance, estimateGas, -} = require('./components/send_/send.utils') +} = require('./components/send/send.utils') const ethUtil = require('ethereumjs-util') const { fetchLocale } = require('../i18n-helper') const log = require('loglevel') diff --git a/ui/app/app.js b/ui/app/app.js index 74d360d3c..a00692df0 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -11,7 +11,7 @@ const log = require('loglevel') // init const InitializeScreen = require('../../mascara/src/app/first-time').default // accounts -const SendTransactionScreen = require('./components/send_/send.container') +const SendTransactionScreen = require('./components/send/send.container') const ConfirmTransaction = require('./components/pages/confirm-transaction') // slideout menu diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index cefa428b9..c255fd64d 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -16,11 +16,11 @@ const { MIN_GAS_PRICE_DEC, MIN_GAS_LIMIT_DEC, MIN_GAS_PRICE_GWEI, -} = require('../send_/send.constants') +} = require('../send/send.constants') const { isBalanceSufficient, -} = require('../send_/send.utils') +} = require('../send/send.utils') const { conversionUtil, @@ -45,7 +45,7 @@ const { const { getGasPrice, getGasLimit, -} = require('../send_/send.selectors') +} = require('../send/send.selectors') function mapStateToProps (state) { const selectedToken = getSelectedToken(state) diff --git a/ui/app/components/dropdowns/account-dropdown-mini.js b/ui/app/components/dropdowns/account-dropdown-mini.js index a7a908d3b..261eb0aa2 100644 --- a/ui/app/components/dropdowns/account-dropdown-mini.js +++ b/ui/app/components/dropdowns/account-dropdown-mini.js @@ -1,7 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const AccountListItem = require('../send_/account-list-item/account-list-item.component').default +const AccountListItem = require('../send/account-list-item/account-list-item.component').default module.exports = AccountDropdownMini diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index adbf2dba8..b9f99b3d1 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/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.component').default +const ToAutoComplete = require('./send/to-autocomplete').default const log = require('loglevel') const { isValidENSAddress } = require('../util') diff --git a/ui/app/components/pending-tx/confirm-deploy-contract.js b/ui/app/components/pending-tx/confirm-deploy-contract.js index af3a14f57..a059d1a32 100644 --- a/ui/app/components/pending-tx/confirm-deploy-contract.js +++ b/ui/app/components/pending-tx/confirm-deploy-contract.js @@ -11,7 +11,7 @@ const { conversionUtil } = require('../../conversion-util') const SenderToRecipient = require('../sender-to-recipient') const NetworkDisplay = require('../network-display') -const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants') +const { MIN_GAS_PRICE_HEX } = require('../send/send.constants') class ConfirmDeployContract extends Component { constructor (props) { diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index 22b2670d8..67f54aa26 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -19,14 +19,14 @@ const { const { calcGasTotal, isBalanceSufficient, -} = require('../send_/send.utils') -const GasFeeDisplay = require('../send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component').default +} = require('../send/send.utils') +const GasFeeDisplay = require('../send/send-content/send-gas-row/gas-fee-display/').default const SenderToRecipient = require('../sender-to-recipient') const NetworkDisplay = require('../network-display') const currencyFormatter = require('currency-formatter') const currencies = require('currency-formatter/currencies') -const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants') +const { MIN_GAS_PRICE_HEX } = require('../send/send.constants') const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') const { ENVIRONMENT_TYPE_POPUP, diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 535347cee..818853882 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -11,7 +11,7 @@ abiDecoder.addABI(tokenAbi) const actions = require('../../actions') const clone = require('clone') const Identicon = require('../identicon') -const GasFeeDisplay = require('../send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js').default +const GasFeeDisplay = require('../send/send-content/send-gas-row/gas-fee-display/').default const NetworkDisplay = require('../network-display') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN @@ -23,7 +23,7 @@ const { const { calcGasTotal, isBalanceSufficient, -} = require('../send_/send.utils') +} = require('../send/send.utils') const { calcTokenAmount, } = require('../../token-util') @@ -31,7 +31,7 @@ const classnames = require('classnames') const currencyFormatter = require('currency-formatter') const currencies = require('currency-formatter/currencies') -const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants') +const { MIN_GAS_PRICE_HEX } = require('../send/send.constants') const { getTokenExchangeRate, diff --git a/ui/app/components/send/README.md b/ui/app/components/send/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/account-list-item/account-list-item-README.md b/ui/app/components/send/account-list-item/account-list-item-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/account-list-item/account-list-item.component.js b/ui/app/components/send/account-list-item/account-list-item.component.js new file mode 100644 index 000000000..9f4a96e61 --- /dev/null +++ b/ui/app/components/send/account-list-item/account-list-item.component.js @@ -0,0 +1,73 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { checksumAddress } from '../../../util' +import Identicon from '../../identicon' +import CurrencyDisplay from '../currency-display' + +export default class AccountListItem extends Component { + + static propTypes = { + account: PropTypes.object, + className: PropTypes.string, + conversionRate: PropTypes.number, + currentCurrency: PropTypes.string, + displayAddress: PropTypes.bool, + displayBalance: PropTypes.bool, + handleClick: PropTypes.func, + icon: PropTypes.node, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + render () { + const { + account, + className, + conversionRate, + currentCurrency, + displayAddress = false, + displayBalance = true, + handleClick, + icon = null, + } = this.props + + const { name, address, balance } = account || {} + + return (
handleClick({ name, address, balance })} + > + +
+ + +
{ name || address }
+ + {icon &&
{ icon }
} + +
+ + {displayAddress && name &&
+ { checksumAddress(address) } +
} + + {displayBalance && } + +
) + } +} diff --git a/ui/app/components/send/account-list-item/account-list-item.container.js b/ui/app/components/send/account-list-item/account-list-item.container.js new file mode 100644 index 000000000..4b4519288 --- /dev/null +++ b/ui/app/components/send/account-list-item/account-list-item.container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux' +import { + getConversionRate, + getCurrentCurrency, +} from '../send.selectors.js' +import AccountListItem from './account-list-item.component' + +export default connect(mapStateToProps)(AccountListItem) + +function mapStateToProps (state) { + return { + conversionRate: getConversionRate(state), + currentCurrency: getCurrentCurrency(state), + } +} diff --git a/ui/app/components/send/account-list-item/account-list-item.scss b/ui/app/components/send/account-list-item/account-list-item.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/account-list-item/index.js b/ui/app/components/send/account-list-item/index.js new file mode 100644 index 000000000..907485cf7 --- /dev/null +++ b/ui/app/components/send/account-list-item/index.js @@ -0,0 +1 @@ +export { default } from './account-list-item.container' diff --git a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js new file mode 100644 index 000000000..ef152d2e7 --- /dev/null +++ b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js @@ -0,0 +1,138 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import proxyquire from 'proxyquire' +import Identicon from '../../../identicon' +import CurrencyDisplay from '../../currency-display' + +const utilsMethodStubs = { + checksumAddress: sinon.stub().returns('mockCheckSumAddress'), +} + +const AccountListItem = proxyquire('../account-list-item.component.js', { + '../../../util': utilsMethodStubs, +}).default + + +const propsMethodSpies = { + handleClick: sinon.spy(), +} + +describe('AccountListItem Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(} + />, { context: { t: str => str + '_t' } }) + }) + + afterEach(() => { + propsMethodSpies.handleClick.resetHistory() + }) + + describe('render', () => { + it('should render a div with the passed className', () => { + assert.equal(wrapper.find('.mockClassName').length, 1) + assert(wrapper.find('.mockClassName').is('div')) + assert(wrapper.find('.mockClassName').hasClass('account-list-item')) + }) + + it('should call handleClick with the expected props when the root div is clicked', () => { + const { onClick } = wrapper.find('.mockClassName').props() + assert.equal(propsMethodSpies.handleClick.callCount, 0) + onClick() + assert.equal(propsMethodSpies.handleClick.callCount, 1) + assert.deepEqual( + propsMethodSpies.handleClick.getCall(0).args, + [{ address: 'mockAddress', name: 'mockName', balance: 'mockBalance' }] + ) + }) + + it('should have a top row div', () => { + assert.equal(wrapper.find('.mockClassName > .account-list-item__top-row').length, 1) + assert(wrapper.find('.mockClassName > .account-list-item__top-row').is('div')) + }) + + it('should have an identicon, name and icon in the top row', () => { + const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') + assert.equal(topRow.find(Identicon).length, 1) + assert.equal(topRow.find('.account-list-item__account-name').length, 1) + assert.equal(topRow.find('.account-list-item__icon').length, 1) + }) + + it('should show the account name if it exists', () => { + const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') + assert.equal(topRow.find('.account-list-item__account-name').text(), 'mockName') + }) + + it('should show the account address if there is no name', () => { + wrapper.setProps({ account: { address: 'addressButNoName' } }) + const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') + assert.equal(topRow.find('.account-list-item__account-name').text(), 'addressButNoName') + }) + + it('should render the passed icon', () => { + const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') + assert(topRow.find('.account-list-item__icon').childAt(0).is('i')) + assert(topRow.find('.account-list-item__icon').childAt(0).hasClass('mockIcon')) + }) + + it('should not render an icon if none is passed', () => { + wrapper.setProps({ icon: null }) + const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') + assert.equal(topRow.find('.account-list-item__icon').length, 0) + }) + + it('should render the account address as a checksumAddress if displayAddress is true and name is provided', () => { + wrapper.setProps({ displayAddress: true }) + assert.equal(wrapper.find('.account-list-item__account-address').length, 1) + assert.equal(wrapper.find('.account-list-item__account-address').text(), 'mockCheckSumAddress') + assert.deepEqual( + utilsMethodStubs.checksumAddress.getCall(0).args, + ['mockAddress'] + ) + }) + + it('should not render the account address as a checksumAddress if displayAddress is false', () => { + wrapper.setProps({ displayAddress: false }) + assert.equal(wrapper.find('.account-list-item__account-address').length, 0) + }) + + it('should not render the account address as a checksumAddress if name is not provided', () => { + wrapper.setProps({ account: { address: 'someAddressButNoName' } }) + assert.equal(wrapper.find('.account-list-item__account-address').length, 0) + }) + + it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => { + wrapper.setProps({ displayBalance: true }) + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + assert.deepEqual( + wrapper.find(CurrencyDisplay).props(), + { + className: 'account-list-item__account-balances', + conversionRate: 4, + convertedBalanceClassName: 'account-list-item__account-secondary-balance', + convertedCurrency: 'mockCurrentyCurrency', + primaryBalanceClassName: 'account-list-item__account-primary-balance', + primaryCurrency: 'ETH', + readOnly: true, + value: 'mockBalance', + } + ) + }) + + it('should not render a CurrencyDisplay if displayBalance is false', () => { + wrapper.setProps({ displayBalance: false }) + assert.equal(wrapper.find(CurrencyDisplay).length, 0) + }) + }) +}) diff --git a/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js new file mode 100644 index 000000000..af0859117 --- /dev/null +++ b/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js @@ -0,0 +1,32 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../account-list-item.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + return () => ({}) + }, + }, + '../send.selectors.js': { + getConversionRate: (s) => `mockConversionRate:${s}`, + getCurrentCurrency: (s) => `mockCurrentCurrency:${s}`, + }, +}) + +describe('account-list-item container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + conversionRate: 'mockConversionRate:mockState', + currentCurrency: 'mockCurrentCurrency:mockState', + }) + }) + + }) + +}) diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js deleted file mode 100644 index 1cf55ce1a..000000000 --- a/ui/app/components/send/currency-display.js +++ /dev/null @@ -1,169 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') -const { removeLeadingZeroes } = require('../send_/send.utils') -const currencyFormatter = require('currency-formatter') -const currencies = require('currency-formatter/currencies') -const ethUtil = require('ethereumjs-util') - -module.exports = CurrencyDisplay - -inherits(CurrencyDisplay, Component) -function CurrencyDisplay () { - Component.call(this) -} - -function toHexWei (value) { - return conversionUtil(value, { - fromNumericBase: 'dec', - toNumericBase: 'hex', - toDenomination: 'WEI', - }) -} - -CurrencyDisplay.prototype.componentWillMount = function () { - this.setState({ - valueToRender: this.getValueToRender(this.props), - }) -} - -CurrencyDisplay.prototype.componentWillReceiveProps = function (nextProps) { - const currentValueToRender = this.getValueToRender(this.props) - const newValueToRender = this.getValueToRender(nextProps) - if (currentValueToRender !== newValueToRender) { - this.setState({ - valueToRender: newValueToRender, - }) - } -} - -CurrencyDisplay.prototype.getAmount = function (value) { - const { selectedToken } = this.props - const { decimals } = selectedToken || {} - const multiplier = Math.pow(10, Number(decimals || 0)) - - const sendAmount = multiplyCurrencies(value || '0', multiplier, {toNumericBase: 'hex'}) - - return selectedToken - ? sendAmount - : toHexWei(value) -} - -CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value, readOnly }) { - if (value === '0x0') return readOnly ? '0' : '' - const { decimals, symbol } = selectedToken || {} - const multiplier = Math.pow(10, Number(decimals || 0)) - - return selectedToken - ? conversionUtil(ethUtil.addHexPrefix(value), { - fromNumericBase: 'hex', - toNumericBase: 'dec', - toCurrency: symbol, - conversionRate: multiplier, - invertConversionRate: true, - }) - : conversionUtil(ethUtil.addHexPrefix(value), { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromDenomination: 'WEI', - numberOfDecimals: 9, - conversionRate, - }) -} - -CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { - const { primaryCurrency, convertedCurrency, conversionRate } = this.props - - let convertedValue = conversionUtil(nonFormattedValue, { - fromNumericBase: 'dec', - fromCurrency: primaryCurrency, - toCurrency: convertedCurrency, - numberOfDecimals: 2, - conversionRate, - }) - convertedValue = Number(convertedValue).toFixed(2) - - const upperCaseCurrencyCode = convertedCurrency.toUpperCase() - - return currencies.find(currency => currency.code === upperCaseCurrencyCode) - ? currencyFormatter.format(Number(convertedValue), { - code: upperCaseCurrencyCode, - }) - : convertedValue -} - -CurrencyDisplay.prototype.handleChange = function (newVal) { - this.setState({ valueToRender: removeLeadingZeroes(newVal) }) - this.props.onChange(this.getAmount(newVal)) -} - -CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) { - const valueString = String(valueToRender) - const valueLength = valueString.length || 1 - const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 - return (valueLength + decimalPointDeficit + 0.75) + 'ch' -} - -CurrencyDisplay.prototype.render = function () { - const { - className = 'currency-display', - primaryBalanceClassName = 'currency-display__input', - convertedBalanceClassName = 'currency-display__converted-value', - primaryCurrency, - convertedCurrency, - readOnly = false, - inError = false, - onBlur, - step, - } = this.props - const { valueToRender } = this.state - - const convertedValueToRender = this.getConvertedValueToRender(valueToRender) - - return h('div', { - className, - style: { - borderColor: inError ? 'red' : null, - }, - onClick: () => { - this.currencyInput && this.currencyInput.focus() - }, - }, [ - - h('div.currency-display__primary-row', [ - - h('div.currency-display__input-wrapper', [ - - h('input', { - className: primaryBalanceClassName, - value: `${valueToRender}`, - placeholder: '0', - type: 'number', - readOnly, - ...(!readOnly ? { - onChange: e => this.handleChange(e.target.value), - onBlur: () => onBlur(this.getAmount(valueToRender)), - } : {}), - ref: input => { this.currencyInput = input }, - style: { - width: this.getInputWidth(valueToRender, readOnly), - }, - min: 0, - step, - }), - - h('span.currency-display__currency-symbol', primaryCurrency), - - ]), - - ]), - - h('div', { - className: convertedBalanceClassName, - }, `${convertedValueToRender} ${convertedCurrency.toUpperCase()}`), - - ]) - -} - diff --git a/ui/app/components/send/currency-display/currency-display.js b/ui/app/components/send/currency-display/currency-display.js new file mode 100644 index 000000000..1b9f7738c --- /dev/null +++ b/ui/app/components/send/currency-display/currency-display.js @@ -0,0 +1,169 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const { conversionUtil, multiplyCurrencies } = require('../../../conversion-util') +const { removeLeadingZeroes } = require('../send.utils') +const currencyFormatter = require('currency-formatter') +const currencies = require('currency-formatter/currencies') +const ethUtil = require('ethereumjs-util') + +module.exports = CurrencyDisplay + +inherits(CurrencyDisplay, Component) +function CurrencyDisplay () { + Component.call(this) +} + +function toHexWei (value) { + return conversionUtil(value, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + toDenomination: 'WEI', + }) +} + +CurrencyDisplay.prototype.componentWillMount = function () { + this.setState({ + valueToRender: this.getValueToRender(this.props), + }) +} + +CurrencyDisplay.prototype.componentWillReceiveProps = function (nextProps) { + const currentValueToRender = this.getValueToRender(this.props) + const newValueToRender = this.getValueToRender(nextProps) + if (currentValueToRender !== newValueToRender) { + this.setState({ + valueToRender: newValueToRender, + }) + } +} + +CurrencyDisplay.prototype.getAmount = function (value) { + const { selectedToken } = this.props + const { decimals } = selectedToken || {} + const multiplier = Math.pow(10, Number(decimals || 0)) + + const sendAmount = multiplyCurrencies(value || '0', multiplier, {toNumericBase: 'hex'}) + + return selectedToken + ? sendAmount + : toHexWei(value) +} + +CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value, readOnly }) { + if (value === '0x0') return readOnly ? '0' : '' + const { decimals, symbol } = selectedToken || {} + const multiplier = Math.pow(10, Number(decimals || 0)) + + return selectedToken + ? conversionUtil(ethUtil.addHexPrefix(value), { + fromNumericBase: 'hex', + toNumericBase: 'dec', + toCurrency: symbol, + conversionRate: multiplier, + invertConversionRate: true, + }) + : conversionUtil(ethUtil.addHexPrefix(value), { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'WEI', + numberOfDecimals: 9, + conversionRate, + }) +} + +CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { + const { primaryCurrency, convertedCurrency, conversionRate } = this.props + + let convertedValue = conversionUtil(nonFormattedValue, { + fromNumericBase: 'dec', + fromCurrency: primaryCurrency, + toCurrency: convertedCurrency, + numberOfDecimals: 2, + conversionRate, + }) + convertedValue = Number(convertedValue).toFixed(2) + + const upperCaseCurrencyCode = convertedCurrency.toUpperCase() + + return currencies.find(currency => currency.code === upperCaseCurrencyCode) + ? currencyFormatter.format(Number(convertedValue), { + code: upperCaseCurrencyCode, + }) + : convertedValue +} + +CurrencyDisplay.prototype.handleChange = function (newVal) { + this.setState({ valueToRender: removeLeadingZeroes(newVal) }) + this.props.onChange(this.getAmount(newVal)) +} + +CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) { + const valueString = String(valueToRender) + const valueLength = valueString.length || 1 + const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 + return (valueLength + decimalPointDeficit + 0.75) + 'ch' +} + +CurrencyDisplay.prototype.render = function () { + const { + className = 'currency-display', + primaryBalanceClassName = 'currency-display__input', + convertedBalanceClassName = 'currency-display__converted-value', + primaryCurrency, + convertedCurrency, + readOnly = false, + inError = false, + onBlur, + step, + } = this.props + const { valueToRender } = this.state + + const convertedValueToRender = this.getConvertedValueToRender(valueToRender) + + return h('div', { + className, + style: { + borderColor: inError ? 'red' : null, + }, + onClick: () => { + this.currencyInput && this.currencyInput.focus() + }, + }, [ + + h('div.currency-display__primary-row', [ + + h('div.currency-display__input-wrapper', [ + + h('input', { + className: primaryBalanceClassName, + value: `${valueToRender}`, + placeholder: '0', + type: 'number', + readOnly, + ...(!readOnly ? { + onChange: e => this.handleChange(e.target.value), + onBlur: () => onBlur(this.getAmount(valueToRender)), + } : {}), + ref: input => { this.currencyInput = input }, + style: { + width: this.getInputWidth(valueToRender, readOnly), + }, + min: 0, + step, + }), + + h('span.currency-display__currency-symbol', primaryCurrency), + + ]), + + ]), + + h('div', { + className: convertedBalanceClassName, + }, `${convertedValueToRender} ${convertedCurrency.toUpperCase()}`), + + ]) + +} + diff --git a/ui/app/components/send/currency-display/index.js b/ui/app/components/send/currency-display/index.js new file mode 100644 index 000000000..0185a19e9 --- /dev/null +++ b/ui/app/components/send/currency-display/index.js @@ -0,0 +1 @@ +export { default } from './currency-display.js' \ No newline at end of file diff --git a/ui/app/components/send/index.js b/ui/app/components/send/index.js new file mode 100644 index 000000000..b5114babc --- /dev/null +++ b/ui/app/components/send/index.js @@ -0,0 +1 @@ +export { default } from './send.container' diff --git a/ui/app/components/send/send-content/index.js b/ui/app/components/send/send-content/index.js new file mode 100644 index 000000000..891c17e6a --- /dev/null +++ b/ui/app/components/send/send-content/index.js @@ -0,0 +1 @@ +export { default } from './send-content.component' diff --git a/ui/app/components/send/send-content/send-amount-row/README.md b/ui/app/components/send/send-content/send-amount-row/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js new file mode 100644 index 000000000..4d0d36ab4 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js @@ -0,0 +1,54 @@ +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, + }) + } + + render () { + const { setMaxModeTo, maxModeOn } = this.props + + return ( +
{ + event.preventDefault() + setMaxModeTo(true) + this.setMaxAmount() + }} + > + {!maxModeOn ? this.context.t('max') : ''} +
+ ) + } + +} diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js new file mode 100644 index 000000000..2d2ec42f7 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js @@ -0,0 +1,40 @@ +import { connect } from 'react-redux' +import { + getGasTotal, + getSelectedToken, + getSendFromBalance, + getTokenBalance, +} from '../../../send.selectors.js' +import { getMaxModeOn } from './amount-max-button.selectors.js' +import { calcMaxAmount } from './amount-max-button.utils.js' +import { + updateSendAmount, + setMaxModeTo, +} from '../../../../../actions' +import AmountMaxButton from './amount-max-button.component' +import { + updateSendErrors, +} from '../../../../../ducks/send.duck' + +export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton) + +function mapStateToProps (state) { + + return { + balance: getSendFromBalance(state), + gasTotal: getGasTotal(state), + maxModeOn: getMaxModeOn(state), + selectedToken: getSelectedToken(state), + tokenBalance: getTokenBalance(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + setAmountToMax: maxAmountDataObject => { + dispatch(updateSendErrors({ amount: null })) + dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject))) + }, + setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), + } +} diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js new file mode 100644 index 000000000..69fec1994 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js @@ -0,0 +1,9 @@ +const selectors = { + getMaxModeOn, +} + +module.exports = selectors + +function getMaxModeOn (state) { + return state.metamask.send.maxModeOn +} diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js new file mode 100644 index 000000000..b490a7fd7 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js @@ -0,0 +1,22 @@ +const { + multiplyCurrencies, + subtractCurrencies, +} = require('../../../../../conversion-util') +const ethUtil = require('ethereumjs-util') + +function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) { + const { decimals } = selectedToken || {} + const multiplier = Math.pow(10, Number(decimals || 0)) + + return selectedToken + ? multiplyCurrencies(tokenBalance, multiplier, {toNumericBase: 'hex'}) + : subtractCurrencies( + ethUtil.addHexPrefix(balance), + ethUtil.addHexPrefix(gasTotal), + { toNumericBase: 'hex' } + ) +} + +module.exports = { + calcMaxAmount, +} diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js new file mode 100644 index 000000000..ee8271494 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js @@ -0,0 +1 @@ +export { default } from './amount-max-button.container' diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js new file mode 100644 index 000000000..86a05ff21 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js @@ -0,0 +1,90 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import AmountMaxButton from '../amount-max-button.component.js' + +const propsMethodSpies = { + setAmountToMax: sinon.spy(), + setMaxModeTo: sinon.spy(), +} + +const MOCK_EVENT = { preventDefault: () => {} } + +sinon.spy(AmountMaxButton.prototype, 'setMaxAmount') + +describe('AmountMaxButton Component', function () { + let wrapper + let instance + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + instance = wrapper.instance() + }) + + afterEach(() => { + propsMethodSpies.setAmountToMax.resetHistory() + propsMethodSpies.setMaxModeTo.resetHistory() + AmountMaxButton.prototype.setMaxAmount.resetHistory() + }) + + describe('setMaxAmount', () => { + + it('should call setAmountToMax with the correct params', () => { + assert.equal(propsMethodSpies.setAmountToMax.callCount, 0) + instance.setMaxAmount() + assert.equal(propsMethodSpies.setAmountToMax.callCount, 1) + assert.deepEqual( + propsMethodSpies.setAmountToMax.getCall(0).args, + [{ + balance: 'mockBalance', + gasTotal: 'mockGasTotal', + selectedToken: { address: 'mockTokenAddress' }, + tokenBalance: 'mockTokenBalance', + }] + ) + }) + + }) + + describe('render', () => { + it('should render a div with a send-v2__amount-max class', () => { + assert.equal(wrapper.find('.send-v2__amount-max').length, 1) + assert(wrapper.find('.send-v2__amount-max').is('div')) + }) + + it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => { + const { + onClick, + } = wrapper.find('.send-v2__amount-max').props() + + assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 0) + assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0) + onClick(MOCK_EVENT) + assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 1) + assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1) + assert.deepEqual( + propsMethodSpies.setMaxModeTo.getCall(0).args, + [true] + ) + }) + + it('should not render text when maxModeOn is true', () => { + wrapper.setProps({ maxModeOn: true }) + assert.equal(wrapper.find('.send-v2__amount-max').text(), '') + }) + + 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/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js new file mode 100644 index 000000000..2cc00d6d6 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js @@ -0,0 +1,91 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + setMaxModeTo: sinon.spy(), + updateSendAmount: sinon.spy(), +} +const duckActionSpies = { + updateSendErrors: sinon.spy(), +} + +proxyquire('../amount-max-button.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../../send.selectors.js': { + getGasTotal: (s) => `mockGasTotal:${s}`, + getSelectedToken: (s) => `mockSelectedToken:${s}`, + getSendFromBalance: (s) => `mockBalance:${s}`, + getTokenBalance: (s) => `mockTokenBalance:${s}`, + }, + './amount-max-button.selectors.js': { getMaxModeOn: (s) => `mockMaxModeOn:${s}` }, + './amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 }, + '../../../../../actions': actionSpies, + '../../../../../ducks/send.duck': duckActionSpies, +}) + +describe('amount-max-button container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + balance: 'mockBalance:mockState', + gasTotal: 'mockGasTotal:mockState', + maxModeOn: 'mockMaxModeOn:mockState', + selectedToken: 'mockSelectedToken:mockState', + tokenBalance: 'mockTokenBalance:mockState', + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + describe('setAmountToMax()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.setAmountToMax({ val: 11, foo: 'bar' }) + assert(dispatchSpy.calledTwice) + assert(duckActionSpies.updateSendErrors.calledOnce) + assert.deepEqual( + duckActionSpies.updateSendErrors.getCall(0).args[0], + { amount: null } + ) + assert(actionSpies.updateSendAmount.calledOnce) + assert.equal( + actionSpies.updateSendAmount.getCall(0).args[0], + 12 + ) + }) + }) + + describe('setMaxModeTo()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.setMaxModeTo('mockVal') + assert(dispatchSpy.calledOnce) + assert.equal( + actionSpies.setMaxModeTo.getCall(0).args[0], + 'mockVal' + ) + }) + }) + + }) + +}) diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js new file mode 100644 index 000000000..655fe1969 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js @@ -0,0 +1,22 @@ +import assert from 'assert' +import { + getMaxModeOn, +} from '../amount-max-button.selectors.js' + +describe('amount-max-button selectors', () => { + + describe('getMaxModeOn()', () => { + it('should', () => { + const state = { + metamask: { + send: { + maxModeOn: null, + }, + }, + } + + assert.equal(getMaxModeOn(state), null) + }) + }) + +}) diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js new file mode 100644 index 000000000..816df6a12 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js @@ -0,0 +1,27 @@ +import assert from 'assert' +import { + calcMaxAmount, +} from '../amount-max-button.utils.js' + +describe('amount-max-button utils', () => { + + describe('calcMaxAmount()', () => { + it('should calculate the correct amount when no selectedToken defined', () => { + assert.deepEqual(calcMaxAmount({ + balance: 'ffffff', + gasTotal: 'ff', + selectedToken: false, + }), 'ffff00') + }) + + it('should calculate the correct amount when a selectedToken is defined', () => { + assert.deepEqual(calcMaxAmount({ + selectedToken: { + decimals: 10, + }, + tokenBalance: 100, + }), 'e8d4a51000') + }) + }) + +}) diff --git a/ui/app/components/send/send-content/send-amount-row/index.js b/ui/app/components/send/send-content/send-amount-row/index.js new file mode 100644 index 000000000..abc6852fe --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/index.js @@ -0,0 +1 @@ +export { default } from './send-amount-row.container' diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js new file mode 100644 index 000000000..c548a5695 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js @@ -0,0 +1,123 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper/' +import AmountMaxButton from './amount-max-button/' +import CurrencyDisplay from '../../currency-display' + +export default class SendAmountRow extends Component { + + static propTypes = { + amount: PropTypes.string, + amountConversionRate: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), + balance: PropTypes.string, + conversionRate: PropTypes.number, + convertedCurrency: PropTypes.string, + gasTotal: PropTypes.string, + inError: PropTypes.bool, + primaryCurrency: PropTypes.string, + selectedToken: PropTypes.object, + setMaxModeTo: PropTypes.func, + tokenBalance: PropTypes.string, + updateGasFeeError: PropTypes.func, + updateSendAmount: PropTypes.func, + updateSendAmountError: PropTypes.func, + updateGas: PropTypes.func, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + validateAmount (amount) { + const { + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + updateGasFeeError, + updateSendAmountError, + } = this.props + + updateSendAmountError({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + }) + + if (selectedToken) { + updateGasFeeError({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + }) + } + } + + updateAmount (amount) { + const { updateSendAmount, setMaxModeTo } = this.props + + setMaxModeTo(false) + updateSendAmount(amount) + } + + updateGas (amount) { + const { selectedToken, updateGas } = this.props + + if (selectedToken) { + updateGas({ amount }) + } + } + + render () { + const { + amount, + amountConversionRate, + convertedCurrency, + gasTotal, + inError, + primaryCurrency, + selectedToken, + } = this.props + + return ( + + {!inError && gasTotal && } + { + this.updateGas(newAmount) + this.updateAmount(newAmount) + }} + onChange={newAmount => this.validateAmount(newAmount)} + inError={inError} + primaryCurrency={primaryCurrency || 'ETH'} + selectedToken={selectedToken} + value={amount} + step="any" + /> + + ) + } + +} diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js new file mode 100644 index 000000000..3504d1b73 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js @@ -0,0 +1,54 @@ +import { connect } from 'react-redux' +import { + getAmountConversionRate, + getConversionRate, + getCurrentCurrency, + getGasTotal, + getPrimaryCurrency, + getSelectedToken, + getSendAmount, + getSendFromBalance, + getTokenBalance, +} from '../../send.selectors' +import { + sendAmountIsInError, +} from './send-amount-row.selectors' +import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils' +import { + setMaxModeTo, + updateSendAmount, +} from '../../../../actions' +import { + updateSendErrors, +} from '../../../../ducks/send.duck' +import SendAmountRow from './send-amount-row.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow) + +function mapStateToProps (state) { + return { + amount: getSendAmount(state), + amountConversionRate: getAmountConversionRate(state), + balance: getSendFromBalance(state), + conversionRate: getConversionRate(state), + convertedCurrency: getCurrentCurrency(state), + gasTotal: getGasTotal(state), + inError: sendAmountIsInError(state), + primaryCurrency: getPrimaryCurrency(state), + selectedToken: getSelectedToken(state), + tokenBalance: getTokenBalance(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), + updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)), + updateGasFeeError: (amountDataObject) => { + dispatch(updateSendErrors(getGasFeeErrorObject(amountDataObject))) + }, + updateSendAmountError: (amountDataObject) => { + dispatch(updateSendErrors(getAmountErrorObject(amountDataObject))) + }, + } +} diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.scss b/ui/app/components/send/send-content/send-amount-row/send-amount-row.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js new file mode 100644 index 000000000..fb08c7ed7 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js @@ -0,0 +1,9 @@ +const selectors = { + sendAmountIsInError, +} + +module.exports = selectors + +function sendAmountIsInError (state) { + return Boolean(state.send.errors.amount) +} diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js new file mode 100644 index 000000000..8425e076e --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js @@ -0,0 +1,202 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +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 CurrencyDisplay from '../../../currency-display' + +const propsMethodSpies = { + setMaxModeTo: sinon.spy(), + updateSendAmount: sinon.spy(), + updateSendAmountError: sinon.spy(), + updateGas: sinon.spy(), + updateGasFeeError: sinon.spy(), +} + +sinon.spy(SendAmountRow.prototype, 'updateAmount') +sinon.spy(SendAmountRow.prototype, 'validateAmount') +sinon.spy(SendAmountRow.prototype, 'updateGas') + +describe('SendAmountRow Component', function () { + let wrapper + let instance + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + instance = wrapper.instance() + }) + + afterEach(() => { + propsMethodSpies.setMaxModeTo.resetHistory() + propsMethodSpies.updateSendAmount.resetHistory() + propsMethodSpies.updateSendAmountError.resetHistory() + propsMethodSpies.updateGasFeeError.resetHistory() + SendAmountRow.prototype.validateAmount.resetHistory() + SendAmountRow.prototype.updateAmount.resetHistory() + }) + + describe('validateAmount', () => { + + it('should call updateSendAmountError with the correct params', () => { + assert.equal(propsMethodSpies.updateSendAmountError.callCount, 0) + instance.validateAmount('someAmount') + assert.equal(propsMethodSpies.updateSendAmountError.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendAmountError.getCall(0).args, + [{ + amount: 'someAmount', + amountConversionRate: 'mockAmountConversionRate', + balance: 'mockBalance', + conversionRate: 7, + gasTotal: 'mockGasTotal', + primaryCurrency: 'mockPrimaryCurrency', + selectedToken: { address: 'mockTokenAddress' }, + tokenBalance: 'mockTokenBalance', + }] + ) + }) + + it('should call updateGasFeeError if selectedToken is truthy', () => { + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) + instance.validateAmount('someAmount') + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateGasFeeError.getCall(0).args, + [{ + amount: 'someAmount', + amountConversionRate: 'mockAmountConversionRate', + balance: 'mockBalance', + conversionRate: 7, + gasTotal: 'mockGasTotal', + primaryCurrency: 'mockPrimaryCurrency', + selectedToken: { address: 'mockTokenAddress' }, + tokenBalance: 'mockTokenBalance', + }] + ) + }) + + it('should call not updateGasFeeError if selectedToken is falsey', () => { + wrapper.setProps({ selectedToken: null }) + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) + instance.validateAmount('someAmount') + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) + }) + + }) + + describe('updateAmount', () => { + + it('should call setMaxModeTo', () => { + assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0) + instance.updateAmount('someAmount') + assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1) + assert.deepEqual( + propsMethodSpies.setMaxModeTo.getCall(0).args, + [false] + ) + }) + + it('should call updateSendAmount', () => { + assert.equal(propsMethodSpies.updateSendAmount.callCount, 0) + instance.updateAmount('someAmount') + assert.equal(propsMethodSpies.updateSendAmount.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendAmount.getCall(0).args, + ['someAmount'] + ) + }) + + }) + + describe('render', () => { + it('should render a SendRowWrapper component', () => { + assert.equal(wrapper.find(SendRowWrapper).length, 1) + }) + + it('should pass the correct props to SendRowWrapper', () => { + const { + errorType, + label, + showError, + } = wrapper.find(SendRowWrapper).props() + + assert.equal(errorType, 'amount') + + assert.equal(label, 'amount_t:') + + assert.equal(showError, false) + }) + + it('should render an AmountMaxButton as the first child of the SendRowWrapper', () => { + assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton)) + }) + + it('should render a CurrencyDisplay as the second child of the SendRowWrapper', () => { + assert(wrapper.find(SendRowWrapper).childAt(1).is(CurrencyDisplay)) + }) + + it('should render the CurrencyDisplay with the correct props', () => { + const { + conversionRate, + convertedCurrency, + onBlur, + onChange, + inError, + primaryCurrency, + selectedToken, + value, + } = wrapper.find(SendRowWrapper).childAt(1).props() + assert.equal(conversionRate, 'mockAmountConversionRate') + assert.equal(convertedCurrency, 'mockConvertedCurrency') + assert.equal(inError, false) + assert.equal(primaryCurrency, 'mockPrimaryCurrency') + assert.deepEqual(selectedToken, { address: 'mockTokenAddress' }) + assert.equal(value, 'mockAmount') + assert.equal(SendAmountRow.prototype.updateGas.callCount, 0) + assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0) + onBlur('mockNewAmount') + assert.equal(SendAmountRow.prototype.updateGas.callCount, 1) + assert.deepEqual( + SendAmountRow.prototype.updateGas.getCall(0).args, + ['mockNewAmount'] + ) + assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1) + assert.deepEqual( + SendAmountRow.prototype.updateAmount.getCall(0).args, + ['mockNewAmount'] + ) + assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0) + onChange('mockNewAmount') + assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1) + assert.deepEqual( + SendAmountRow.prototype.validateAmount.getCall(0).args, + ['mockNewAmount'] + ) + }) + + it('should pass the default primaryCurrency to the CurrencyDisplay if primaryCurrency is falsy', () => { + wrapper.setProps({ primaryCurrency: null }) + const { primaryCurrency } = wrapper.find(SendRowWrapper).childAt(1).props() + assert.equal(primaryCurrency, 'ETH') + }) + }) +}) diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js new file mode 100644 index 000000000..52e351aee --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js @@ -0,0 +1,125 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + setMaxModeTo: sinon.spy(), + updateSendAmount: sinon.spy(), +} +const duckActionSpies = { + updateSendErrors: sinon.spy(), +} + +proxyquire('../send-amount-row.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../send.selectors': { + getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, + getConversionRate: (s) => `mockConversionRate:${s}`, + getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, + getGasTotal: (s) => `mockGasTotal:${s}`, + getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`, + getSelectedToken: (s) => `mockSelectedToken:${s}`, + getSendAmount: (s) => `mockAmount:${s}`, + getSendFromBalance: (s) => `mockBalance:${s}`, + getTokenBalance: (s) => `mockTokenBalance:${s}`, + }, + './send-amount-row.selectors': { sendAmountIsInError: (s) => `mockInError:${s}` }, + '../../send.utils': { + getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }), + getGasFeeErrorObject: (mockDataObject) => ({ ...mockDataObject, mockGasFeeErrorChange: true }), + }, + '../../../../actions': actionSpies, + '../../../../ducks/send.duck': duckActionSpies, +}) + +describe('send-amount-row container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + amount: 'mockAmount:mockState', + amountConversionRate: 'mockAmountConversionRate:mockState', + balance: 'mockBalance:mockState', + conversionRate: 'mockConversionRate:mockState', + convertedCurrency: 'mockConvertedCurrency:mockState', + gasTotal: 'mockGasTotal:mockState', + inError: 'mockInError:mockState', + primaryCurrency: 'mockPrimaryCurrency:mockState', + selectedToken: 'mockSelectedToken:mockState', + tokenBalance: 'mockTokenBalance:mockState', + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + duckActionSpies.updateSendErrors.resetHistory() + }) + + describe('setMaxModeTo()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.setMaxModeTo('mockBool') + assert(dispatchSpy.calledOnce) + assert(actionSpies.setMaxModeTo.calledOnce) + assert.equal( + actionSpies.setMaxModeTo.getCall(0).args[0], + 'mockBool' + ) + }) + }) + + describe('updateSendAmount()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateSendAmount('mockAmount') + assert(dispatchSpy.calledOnce) + assert(actionSpies.updateSendAmount.calledOnce) + assert.equal( + actionSpies.updateSendAmount.getCall(0).args[0], + 'mockAmount' + ) + }) + }) + + describe('updateGasFeeError()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateGasFeeError({ some: 'data' }) + assert(dispatchSpy.calledOnce) + assert(duckActionSpies.updateSendErrors.calledOnce) + assert.deepEqual( + duckActionSpies.updateSendErrors.getCall(0).args[0], + { some: 'data', mockGasFeeErrorChange: true } + ) + }) + }) + + describe('updateSendAmountError()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateSendAmountError({ some: 'data' }) + assert(dispatchSpy.calledOnce) + assert(duckActionSpies.updateSendErrors.calledOnce) + assert.deepEqual( + duckActionSpies.updateSendErrors.getCall(0).args[0], + { some: 'data', mockChange: true } + ) + }) + }) + + }) + +}) diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js new file mode 100644 index 000000000..4672cb8a7 --- /dev/null +++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js @@ -0,0 +1,34 @@ +import assert from 'assert' +import { + sendAmountIsInError, +} from '../send-amount-row.selectors.js' + +describe('send-amount-row selectors', () => { + + describe('sendAmountIsInError()', () => { + it('should return true if send.errors.amount is truthy', () => { + const state = { + send: { + errors: { + amount: 'abc', + }, + }, + } + + assert.equal(sendAmountIsInError(state), true) + }) + + it('should return false if send.errors.amount is falsy', () => { + const state = { + send: { + errors: { + amount: null, + }, + }, + } + + assert.equal(sendAmountIsInError(state), false) + }) + }) + +}) diff --git a/ui/app/components/send/send-content/send-content-README.md b/ui/app/components/send/send-content/send-content-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js new file mode 100644 index 000000000..adc114c0e --- /dev/null +++ b/ui/app/components/send/send-content/send-content.component.js @@ -0,0 +1,28 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import PageContainerContent from '../../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 SendToRow from './send-to-row/' + +export default class SendContent extends Component { + + static propTypes = { + updateGas: PropTypes.func, + }; + + render () { + return ( + +
+ + this.props.updateGas(updateData)} /> + this.props.updateGas(updateData)} /> + +
+
+ ) + } + +} diff --git a/ui/app/components/send/send-content/send-content.scss b/ui/app/components/send/send-content/send-content.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-dropdown-list/index.js b/ui/app/components/send/send-content/send-dropdown-list/index.js new file mode 100644 index 000000000..04af6536c --- /dev/null +++ b/ui/app/components/send/send-content/send-dropdown-list/index.js @@ -0,0 +1 @@ +export { default } from './send-dropdown-list.component' diff --git a/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js b/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js new file mode 100644 index 000000000..bedac1259 --- /dev/null +++ b/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js @@ -0,0 +1,52 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import AccountListItem from '../../account-list-item/' + +export default class SendDropdownList extends Component { + + static propTypes = { + accounts: PropTypes.array, + closeDropdown: PropTypes.func, + onSelect: PropTypes.func, + activeAddress: PropTypes.string, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + getListItemIcon (accountAddress, activeAddress) { + return accountAddress === activeAddress + ? + : null + } + + render () { + const { + accounts, + closeDropdown, + onSelect, + activeAddress, + } = this.props + + return (
+
closeDropdown()} + /> +
+ {accounts.map((account, index) => { + onSelect(account) + closeDropdown() + }} + icon={this.getListItemIcon(account.address, activeAddress)} + key={`send-dropdown-account-#${index}`} + />)} +
+
) + } + +} diff --git a/ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js b/ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js new file mode 100644 index 000000000..b92dd4dfe --- /dev/null +++ b/ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js @@ -0,0 +1,105 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import SendDropdownList from '../send-dropdown-list.component.js' + +import AccountListItem from '../../../account-list-item/account-list-item.container' + +const propsMethodSpies = { + closeDropdown: sinon.spy(), + onSelect: sinon.spy(), +} + +sinon.spy(SendDropdownList.prototype, 'getListItemIcon') + +describe('SendDropdownList Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + }) + + afterEach(() => { + propsMethodSpies.closeDropdown.resetHistory() + propsMethodSpies.onSelect.resetHistory() + SendDropdownList.prototype.getListItemIcon.resetHistory() + }) + + describe('getListItemIcon', () => { + it('should return check icon if the passed addresses are the same', () => { + assert.deepEqual( + wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount0'), + + ) + }) + + it('should return null if the passed addresses are different', () => { + assert.equal( + wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount1'), + null + ) + }) + }) + + describe('render', () => { + it('should render a single div with two children', () => { + assert(wrapper.is('div')) + assert.equal(wrapper.children().length, 2) + }) + + it('should render the children with the correct classes', () => { + assert(wrapper.childAt(0).hasClass('send-v2__from-dropdown__close-area')) + assert(wrapper.childAt(1).hasClass('send-v2__from-dropdown__list')) + }) + + it('should call closeDropdown onClick of the send-v2__from-dropdown__close-area', () => { + assert.equal(propsMethodSpies.closeDropdown.callCount, 0) + wrapper.childAt(0).props().onClick() + assert.equal(propsMethodSpies.closeDropdown.callCount, 1) + }) + + it('should render an AccountListItem for each item in accounts', () => { + assert.equal(wrapper.childAt(1).children().length, 3) + assert(wrapper.childAt(1).children().every(AccountListItem)) + }) + + it('should pass the correct props to the AccountListItem', () => { + wrapper.childAt(1).children().forEach((accountListItem, index) => { + const { + account, + className, + handleClick, + } = accountListItem.props() + assert.deepEqual(account, { address: 'mockAccount' + index }) + assert.equal(className, 'account-list-item__dropdown') + assert.equal(propsMethodSpies.onSelect.callCount, 0) + handleClick() + assert.equal(propsMethodSpies.onSelect.callCount, 1) + assert.deepEqual(propsMethodSpies.onSelect.getCall(0).args[0], { address: 'mockAccount' + index }) + propsMethodSpies.onSelect.resetHistory() + propsMethodSpies.closeDropdown.resetHistory() + assert.equal(propsMethodSpies.closeDropdown.callCount, 0) + handleClick() + assert.equal(propsMethodSpies.closeDropdown.callCount, 1) + propsMethodSpies.onSelect.resetHistory() + propsMethodSpies.closeDropdown.resetHistory() + }) + }) + + it('should call this.getListItemIcon for each AccountListItem', () => { + assert.equal(SendDropdownList.prototype.getListItemIcon.callCount, 3) + const getListItemIconCalls = SendDropdownList.prototype.getListItemIcon.getCalls() + assert(getListItemIconCalls.every(({ args }, index) => args[0] === 'mockAccount' + index)) + }) + }) +}) diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js new file mode 100644 index 000000000..4f43a9d61 --- /dev/null +++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import AccountListItem from '../../../account-list-item/' +import SendDropdownList from '../../send-dropdown-list/' + +export default class FromDropdown extends Component { + + static propTypes = { + accounts: PropTypes.array, + closeDropdown: PropTypes.func, + dropdownOpen: PropTypes.bool, + onSelect: PropTypes.func, + openDropdown: PropTypes.func, + selectedAccount: PropTypes.object, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + render () { + const { + accounts, + closeDropdown, + dropdownOpen, + openDropdown, + selectedAccount, + onSelect, + } = this.props + + return
+ } + /> + {dropdownOpen && } +
+ } + +} diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js new file mode 100644 index 000000000..2314ef4e3 --- /dev/null +++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js @@ -0,0 +1 @@ +export { default } from './from-dropdown.component' diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js new file mode 100644 index 000000000..84fcb281e --- /dev/null +++ b/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js @@ -0,0 +1,88 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import FromDropdown from '../from-dropdown.component.js' + +import AccountListItem from '../../../../account-list-item/account-list-item.container' +import SendDropdownList from '../../../send-dropdown-list/send-dropdown-list.component' + +const propsMethodSpies = { + closeDropdown: sinon.spy(), + openDropdown: sinon.spy(), + onSelect: sinon.spy(), +} + +describe('FromDropdown Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + }) + + afterEach(() => { + propsMethodSpies.closeDropdown.resetHistory() + propsMethodSpies.openDropdown.resetHistory() + propsMethodSpies.onSelect.resetHistory() + }) + + describe('render', () => { + it('should render a div with a .send-v2__from-dropdown class', () => { + assert.equal(wrapper.find('.send-v2__from-dropdown').length, 1) + }) + + it('should render an AccountListItem as the first child of the .send-v2__from-dropdown div', () => { + assert(wrapper.find('.send-v2__from-dropdown').childAt(0).is(AccountListItem)) + }) + + it('should pass the correct props to AccountListItem', () => { + const { + account, + handleClick, + icon, + } = wrapper.find('.send-v2__from-dropdown').childAt(0).props() + assert.deepEqual(account, { address: 'mockAddress' }) + assert.deepEqual( + icon, + + ) + assert.equal(propsMethodSpies.openDropdown.callCount, 0) + handleClick() + assert.equal(propsMethodSpies.openDropdown.callCount, 1) + }) + + it('should not render a SendDropdownList when dropdownOpen is false', () => { + assert.equal(wrapper.find(SendDropdownList).length, 0) + }) + + it('should render a SendDropdownList when dropdownOpen is true', () => { + wrapper.setProps({ dropdownOpen: true }) + assert(wrapper.find(SendDropdownList).length, 1) + }) + + it('should pass the correct props to the SendDropdownList]', () => { + wrapper.setProps({ dropdownOpen: true }) + const { + accounts, + closeDropdown, + onSelect, + activeAddress, + } = wrapper.find(SendDropdownList).props() + assert.deepEqual(accounts, ['mockAccount']) + assert.equal(activeAddress, 'mockAddress') + assert.equal(propsMethodSpies.closeDropdown.callCount, 0) + closeDropdown() + assert.equal(propsMethodSpies.closeDropdown.callCount, 1) + assert.equal(propsMethodSpies.onSelect.callCount, 0) + onSelect() + assert.equal(propsMethodSpies.onSelect.callCount, 1) + }) + }) +}) diff --git a/ui/app/components/send/send-content/send-from-row/index.js b/ui/app/components/send/send-content/send-from-row/index.js new file mode 100644 index 000000000..0a79726b2 --- /dev/null +++ b/ui/app/components/send/send-content/send-from-row/index.js @@ -0,0 +1 @@ +export { default } from './send-from-row.container' diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row-README.md b/ui/app/components/send/send-content/send-from-row/send-from-row-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row.component.js b/ui/app/components/send/send-content/send-from-row/send-from-row.component.js new file mode 100644 index 000000000..3e0e0de22 --- /dev/null +++ b/ui/app/components/send/send-content/send-from-row/send-from-row.component.js @@ -0,0 +1,63 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper/' +import FromDropdown from './from-dropdown/' + +export default class SendFromRow extends Component { + + static propTypes = { + closeFromDropdown: PropTypes.func, + conversionRate: PropTypes.number, + from: PropTypes.object, + fromAccounts: PropTypes.array, + fromDropdownOpen: PropTypes.bool, + openFromDropdown: PropTypes.func, + tokenContract: PropTypes.object, + updateSendFrom: PropTypes.func, + setSendTokenBalance: PropTypes.func, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + async handleFromChange (newFrom) { + const { + updateSendFrom, + tokenContract, + setSendTokenBalance, + } = this.props + + if (tokenContract) { + const usersToken = await tokenContract.balanceOf(newFrom.address) + setSendTokenBalance(usersToken) + } + updateSendFrom(newFrom) + } + + render () { + const { + closeFromDropdown, + conversionRate, + from, + fromAccounts, + fromDropdownOpen, + openFromDropdown, + } = this.props + + return ( + + closeFromDropdown()} + conversionRate={conversionRate} + dropdownOpen={fromDropdownOpen} + onSelect={newFrom => this.handleFromChange(newFrom)} + openDropdown={() => openFromDropdown()} + selectedAccount={from} + /> + + ) + } + +} diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row.container.js b/ui/app/components/send/send-content/send-from-row/send-from-row.container.js new file mode 100644 index 000000000..33cb63b43 --- /dev/null +++ b/ui/app/components/send/send-content/send-from-row/send-from-row.container.js @@ -0,0 +1,46 @@ +import { connect } from 'react-redux' +import { + accountsWithSendEtherInfoSelector, + getConversionRate, + getSelectedTokenContract, + getSendFromObject, +} from '../../send.selectors.js' +import { + getFromDropdownOpen, +} from './send-from-row.selectors.js' +import { calcTokenBalance } from '../../send.utils.js' +import { + updateSendFrom, + setSendTokenBalance, +} from '../../../../actions' +import { + closeFromDropdown, + openFromDropdown, +} from '../../../../ducks/send.duck' +import SendFromRow from './send-from-row.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendFromRow) + +function mapStateToProps (state) { + return { + conversionRate: getConversionRate(state), + from: getSendFromObject(state), + fromAccounts: accountsWithSendEtherInfoSelector(state), + fromDropdownOpen: getFromDropdownOpen(state), + tokenContract: getSelectedTokenContract(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + closeFromDropdown: () => dispatch(closeFromDropdown()), + openFromDropdown: () => dispatch(openFromDropdown()), + updateSendFrom: newFrom => dispatch(updateSendFrom(newFrom)), + setSendTokenBalance: (usersToken, selectedToken) => { + if (!usersToken) return + + const tokenBalance = calcTokenBalance({ usersToken, selectedToken }) + dispatch(setSendTokenBalance(tokenBalance)) + }, + } +} diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js b/ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js new file mode 100644 index 000000000..03ef4806b --- /dev/null +++ b/ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js @@ -0,0 +1,9 @@ +const selectors = { + getFromDropdownOpen, +} + +module.exports = selectors + +function getFromDropdownOpen (state) { + return state.send.fromDropdownOpen +} diff --git a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js new file mode 100644 index 000000000..9ba8d1739 --- /dev/null +++ b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js @@ -0,0 +1,121 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import SendFromRow from '../send-from-row.component.js' + +import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' +import FromDropdown from '../from-dropdown/from-dropdown.component' + +const propsMethodSpies = { + closeFromDropdown: sinon.spy(), + openFromDropdown: sinon.spy(), + updateSendFrom: sinon.spy(), + setSendTokenBalance: sinon.spy(), +} + +sinon.spy(SendFromRow.prototype, 'handleFromChange') + +describe('SendFromRow Component', function () { + let wrapper + let instance + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + instance = wrapper.instance() + }) + + afterEach(() => { + propsMethodSpies.closeFromDropdown.resetHistory() + propsMethodSpies.openFromDropdown.resetHistory() + propsMethodSpies.updateSendFrom.resetHistory() + propsMethodSpies.setSendTokenBalance.resetHistory() + SendFromRow.prototype.handleFromChange.resetHistory() + }) + + describe('handleFromChange', () => { + + it('should call updateSendFrom', () => { + assert.equal(propsMethodSpies.updateSendFrom.callCount, 0) + instance.handleFromChange('mockFrom') + assert.equal(propsMethodSpies.updateSendFrom.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendFrom.getCall(0).args, + ['mockFrom'] + ) + }) + + it('should call tokenContract.balanceOf and setSendTokenBalance if tokenContract is defined', async () => { + wrapper.setProps({ + tokenContract: { + balanceOf: () => new Promise((resolve) => resolve('mockUsersToken')), + }, + }) + assert.equal(propsMethodSpies.setSendTokenBalance.callCount, 0) + await instance.handleFromChange('mockFrom') + assert.equal(propsMethodSpies.setSendTokenBalance.callCount, 1) + assert.deepEqual( + propsMethodSpies.setSendTokenBalance.getCall(0).args, + ['mockUsersToken'] + ) + }) + + }) + + describe('render', () => { + it('should render a SendRowWrapper component', () => { + assert.equal(wrapper.find(SendRowWrapper).length, 1) + }) + + it('should pass the correct props to SendRowWrapper', () => { + const { + label, + } = wrapper.find(SendRowWrapper).props() + + assert.equal(label, 'from_t:') + }) + + it('should render an FromDropdown as a child of the SendRowWrapper', () => { + assert(wrapper.find(SendRowWrapper).childAt(0).is(FromDropdown)) + }) + + it('should render the FromDropdown with the correct props', () => { + const { + accounts, + closeDropdown, + conversionRate, + dropdownOpen, + onSelect, + openDropdown, + selectedAccount, + } = wrapper.find(SendRowWrapper).childAt(0).props() + assert.deepEqual(accounts, ['mockAccount']) + assert.equal(dropdownOpen, false) + assert.equal(conversionRate, 15) + assert.deepEqual(selectedAccount, { address: 'mockAddress' }) + assert.equal(propsMethodSpies.closeFromDropdown.callCount, 0) + closeDropdown() + assert.equal(propsMethodSpies.closeFromDropdown.callCount, 1) + assert.equal(propsMethodSpies.openFromDropdown.callCount, 0) + openDropdown() + assert.equal(propsMethodSpies.openFromDropdown.callCount, 1) + assert.equal(SendFromRow.prototype.handleFromChange.callCount, 0) + onSelect('mockNewFrom') + assert.equal(SendFromRow.prototype.handleFromChange.callCount, 1) + assert.deepEqual( + SendFromRow.prototype.handleFromChange.getCall(0).args, + ['mockNewFrom'] + ) + }) + }) +}) diff --git a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js new file mode 100644 index 000000000..e080b2fe3 --- /dev/null +++ b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js @@ -0,0 +1,110 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + updateSendFrom: sinon.spy(), + setSendTokenBalance: sinon.spy(), +} +const duckActionSpies = { + closeFromDropdown: sinon.spy(), + openFromDropdown: sinon.spy(), +} + +proxyquire('../send-from-row.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../send.selectors.js': { + accountsWithSendEtherInfoSelector: (s) => `mockFromAccounts:${s}`, + getConversionRate: (s) => `mockConversionRate:${s}`, + getSelectedTokenContract: (s) => `mockTokenContract:${s}`, + getSendFromObject: (s) => `mockFrom:${s}`, + }, + './send-from-row.selectors.js': { getFromDropdownOpen: (s) => `mockFromDropdownOpen:${s}` }, + '../../send.utils.js': { calcTokenBalance: ({ usersToken, selectedToken }) => usersToken + selectedToken }, + '../../../../actions': actionSpies, + '../../../../ducks/send.duck': duckActionSpies, +}) + +describe('send-from-row container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + conversionRate: 'mockConversionRate:mockState', + from: 'mockFrom:mockState', + fromAccounts: 'mockFromAccounts:mockState', + fromDropdownOpen: 'mockFromDropdownOpen:mockState', + tokenContract: 'mockTokenContract:mockState', + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + describe('closeFromDropdown()', () => { + it('should dispatch a closeFromDropdown action', () => { + mapDispatchToPropsObject.closeFromDropdown() + assert(dispatchSpy.calledOnce) + assert(duckActionSpies.closeFromDropdown.calledOnce) + assert.equal( + duckActionSpies.closeFromDropdown.getCall(0).args[0], + undefined + ) + }) + }) + + describe('openFromDropdown()', () => { + it('should dispatch a openFromDropdown action', () => { + mapDispatchToPropsObject.openFromDropdown() + assert(dispatchSpy.calledOnce) + assert(duckActionSpies.openFromDropdown.calledOnce) + assert.equal( + duckActionSpies.openFromDropdown.getCall(0).args[0], + undefined + ) + }) + }) + + describe('updateSendFrom()', () => { + it('should dispatch an updateSendFrom action', () => { + mapDispatchToPropsObject.updateSendFrom('mockFrom') + assert(dispatchSpy.calledOnce) + assert.equal( + actionSpies.updateSendFrom.getCall(0).args[0], + 'mockFrom' + ) + }) + }) + + describe('setSendTokenBalance()', () => { + it('should dispatch an setSendTokenBalance action', () => { + mapDispatchToPropsObject.setSendTokenBalance('mockUsersToken', 'mockSelectedToken') + assert(dispatchSpy.calledOnce) + assert.equal( + actionSpies.setSendTokenBalance.getCall(0).args[0], + 'mockUsersTokenmockSelectedToken' + ) + }) + }) + + }) + +}) diff --git a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js new file mode 100644 index 000000000..ecb57bbc3 --- /dev/null +++ b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js @@ -0,0 +1,20 @@ +import assert from 'assert' +import { + getFromDropdownOpen, +} from '../send-from-row.selectors.js' + +describe('send-from-row selectors', () => { + + describe('getFromDropdownOpen()', () => { + it('should get send.fromDropdownOpen', () => { + const state = { + send: { + fromDropdownOpen: null, + }, + } + + assert.equal(getFromDropdownOpen(state), null) + }) + }) + +}) diff --git a/ui/app/components/send/send-content/send-gas-row/README.md b/ui/app/components/send/send-content/send-gas-row/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js new file mode 100644 index 000000000..bb9a94428 --- /dev/null +++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js @@ -0,0 +1,61 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import CurrencyDisplay from '../../../../send/currency-display' + + +export default class GasFeeDisplay extends Component { + + static propTypes = { + conversionRate: PropTypes.number, + primaryCurrency: PropTypes.string, + convertedCurrency: PropTypes.string, + gasLoadingError: PropTypes.bool, + gasTotal: PropTypes.string, + onClick: PropTypes.func, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + render () { + const { + conversionRate, + gasTotal, + onClick, + primaryCurrency = 'ETH', + convertedCurrency, + gasLoadingError, + } = this.props + + return ( +
+ {gasTotal + ? + : gasLoadingError + ?
+ {this.context.t('setGasPrice')} +
+ :
+ {this.context.t('loading')} +
+ } + +
+ ) + } +} diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js new file mode 100644 index 000000000..dba0edb7b --- /dev/null +++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js @@ -0,0 +1 @@ +export { default } from './gas-fee-display.component' diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js new file mode 100644 index 000000000..7cbe8d0df --- /dev/null +++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js @@ -0,0 +1,55 @@ +import React from 'react' +import assert from 'assert' +import {shallow} from 'enzyme' +import GasFeeDisplay from '../gas-fee-display.component' +import CurrencyDisplay from '../../../../../send/currency-display' +import sinon from 'sinon' + + +const propsMethodSpies = { + showCustomizeGasModal: sinon.spy(), +} + +describe('SendGasRow Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, {context: {t: str => str + '_t'}}) + }) + + afterEach(() => { + propsMethodSpies.showCustomizeGasModal.resetHistory() + }) + + describe('render', () => { + it('should render a CurrencyDisplay component', () => { + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + }) + + it('should render the CurrencyDisplay with the correct props', () => { + const { + conversionRate, + convertedCurrency, + value, + } = wrapper.find(CurrencyDisplay).props() + assert.equal(conversionRate, 20) + assert.equal(convertedCurrency, 'mockConvertedCurrency') + assert.equal(value, 'mockGasTotal') + }) + + it('should render the Button with the correct props', () => { + const { + onClick, + } = wrapper.find('button').props() + assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) + onClick() + assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) + }) + }) +}) diff --git a/ui/app/components/send/send-content/send-gas-row/index.js b/ui/app/components/send/send-content/send-gas-row/index.js new file mode 100644 index 000000000..3c7ff1d5f --- /dev/null +++ b/ui/app/components/send/send-content/send-gas-row/index.js @@ -0,0 +1 @@ +export { default } from './send-gas-row.container' diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js new file mode 100644 index 000000000..91b58cfd0 --- /dev/null +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js @@ -0,0 +1,48 @@ +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' + +export default class SendGasRow extends Component { + + static propTypes = { + conversionRate: PropTypes.number, + convertedCurrency: PropTypes.string, + gasFeeError: PropTypes.bool, + gasLoadingError: PropTypes.bool, + gasTotal: PropTypes.string, + showCustomizeGasModal: PropTypes.func, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + render () { + const { + conversionRate, + convertedCurrency, + gasLoadingError, + gasTotal, + gasFeeError, + showCustomizeGasModal, + } = this.props + + return ( + + showCustomizeGasModal()} + /> + + ) + } + +} diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js new file mode 100644 index 000000000..8f8e3e4dd --- /dev/null +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux' +import { + getConversionRate, + getCurrentCurrency, + getGasTotal, +} from '../../send.selectors.js' +import { getGasLoadingError, gasFeeIsInError } from './send-gas-row.selectors.js' +import { showModal } from '../../../../actions' +import SendGasRow from './send-gas-row.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow) + +function mapStateToProps (state) { + return { + conversionRate: getConversionRate(state), + convertedCurrency: getCurrentCurrency(state), + gasTotal: getGasTotal(state), + gasFeeError: gasFeeIsInError(state), + gasLoadingError: getGasLoadingError(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS' })), + } +} diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.scss b/ui/app/components/send/send-content/send-gas-row/send-gas-row.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js new file mode 100644 index 000000000..96f6293c2 --- /dev/null +++ b/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js @@ -0,0 +1,14 @@ +const selectors = { + gasFeeIsInError, + getGasLoadingError, +} + +module.exports = selectors + +function getGasLoadingError (state) { + return state.send.errors.gasLoading +} + +function gasFeeIsInError (state) { + return Boolean(state.send.errors.gasFee) +} diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js new file mode 100644 index 000000000..54a92bd2d --- /dev/null +++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js @@ -0,0 +1,70 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +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' + +const propsMethodSpies = { + showCustomizeGasModal: sinon.spy(), +} + +describe('SendGasRow Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + }) + + afterEach(() => { + propsMethodSpies.showCustomizeGasModal.resetHistory() + }) + + describe('render', () => { + it('should render a SendRowWrapper component', () => { + assert.equal(wrapper.find(SendRowWrapper).length, 1) + }) + + it('should pass the correct props to SendRowWrapper', () => { + const { + label, + showError, + errorType, + } = wrapper.find(SendRowWrapper).props() + + assert.equal(label, 'gasFee_t:') + assert.equal(showError, 'mockGasFeeError') + assert.equal(errorType, 'gasFee') + }) + + it('should render a GasFeeDisplay as a child of the SendRowWrapper', () => { + assert(wrapper.find(SendRowWrapper).childAt(0).is(GasFeeDisplay)) + }) + + it('should render the GasFeeDisplay with the correct props', () => { + const { + conversionRate, + convertedCurrency, + gasLoadingError, + gasTotal, + onClick, + } = wrapper.find(SendRowWrapper).childAt(0).props() + assert.equal(conversionRate, 20) + assert.equal(convertedCurrency, 'mockConvertedCurrency') + assert.equal(gasLoadingError, false) + assert.equal(gasTotal, 'mockGasTotal') + assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) + onClick() + assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) + }) + }) +}) diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js new file mode 100644 index 000000000..2ce062505 --- /dev/null +++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js @@ -0,0 +1,70 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + showModal: sinon.spy(), +} + +proxyquire('../send-gas-row.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../send.selectors.js': { + getConversionRate: (s) => `mockConversionRate:${s}`, + getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, + getGasTotal: (s) => `mockGasTotal:${s}`, + }, + './send-gas-row.selectors.js': { + getGasLoadingError: (s) => `mockGasLoadingError:${s}`, + gasFeeIsInError: (s) => `mockGasFeeError:${s}`, + }, + '../../../../actions': actionSpies, +}) + +describe('send-gas-row container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + conversionRate: 'mockConversionRate:mockState', + convertedCurrency: 'mockConvertedCurrency:mockState', + gasTotal: 'mockGasTotal:mockState', + gasFeeError: 'mockGasFeeError:mockState', + gasLoadingError: 'mockGasLoadingError:mockState', + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + describe('showCustomizeGasModal()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.showCustomizeGasModal() + assert(dispatchSpy.calledOnce) + assert.deepEqual( + actionSpies.showModal.getCall(0).args[0], + { name: 'CUSTOMIZE_GAS' } + ) + }) + }) + + }) + +}) diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js new file mode 100644 index 000000000..d46dd9d8b --- /dev/null +++ b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js @@ -0,0 +1,49 @@ +import assert from 'assert' +import { + gasFeeIsInError, + getGasLoadingError, +} from '../send-gas-row.selectors.js' + +describe('send-gas-row selectors', () => { + + describe('getGasLoadingError()', () => { + it('should return send.errors.gasLoading', () => { + const state = { + send: { + errors: { + gasLoading: 'abc', + }, + }, + } + + assert.equal(getGasLoadingError(state), 'abc') + }) + }) + + describe('gasFeeIsInError()', () => { + it('should return true if send.errors.gasFee is truthy', () => { + const state = { + send: { + errors: { + gasFee: 'def', + }, + }, + } + + assert.equal(gasFeeIsInError(state), true) + }) + + it('should return false send.errors.gasFee is falsely', () => { + const state = { + send: { + errors: { + gasFee: null, + }, + }, + } + + assert.equal(gasFeeIsInError(state), false) + }) + }) + +}) diff --git a/ui/app/components/send/send-content/send-row-wrapper/index.js b/ui/app/components/send/send-content/send-row-wrapper/index.js new file mode 100644 index 000000000..d17545dcc --- /dev/null +++ b/ui/app/components/send/send-content/send-row-wrapper/index.js @@ -0,0 +1 @@ +export { default } from './send-row-wrapper.component' diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js new file mode 100644 index 000000000..c00617f83 --- /dev/null +++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js @@ -0,0 +1 @@ +export { default } from './send-row-error-message.container' diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js new file mode 100644 index 000000000..61bc7bab7 --- /dev/null +++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js @@ -0,0 +1,27 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class SendRowErrorMessage extends Component { + + static propTypes = { + errors: PropTypes.object, + errorType: PropTypes.string, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + render () { + const { errors, errorType } = this.props + + const errorMessage = errors[errorType] + + return ( + errorMessage + ?
{this.context.t(errorMessage)}
+ : null + ) + } + +} diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js new file mode 100644 index 000000000..59622047f --- /dev/null +++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import { getSendErrors } from '../../../send.selectors' +import SendRowErrorMessage from './send-row-error-message.component' + +export default connect(mapStateToProps)(SendRowErrorMessage) + +function mapStateToProps (state, ownProps) { + return { + errors: getSendErrors(state), + errorType: ownProps.errorType, + } +} diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js new file mode 100644 index 000000000..2304a43d2 --- /dev/null +++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js @@ -0,0 +1,28 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import SendRowErrorMessage from '../send-row-error-message.component.js' + +describe('SendRowErrorMessage Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + }) + + describe('render', () => { + it('should render null if the passed errors do not contain an error of errorType', () => { + assert.equal(wrapper.find('.send-v2__error').length, 0) + assert.equal(wrapper.html(), null) + }) + + it('should render an error message if the passed errors contain an error of errorType', () => { + wrapper.setProps({ errors: { error1: 'abc', error2: 'def', error3: 'xyz' } }) + assert.equal(wrapper.find('.send-v2__error').length, 1) + assert.equal(wrapper.find('.send-v2__error').text(), 'xyz_t') + }) + }) +}) diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js new file mode 100644 index 000000000..eecff165d --- /dev/null +++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js @@ -0,0 +1,28 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../send-row-error-message.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + return () => ({}) + }, + }, + '../../../send.selectors': { getSendErrors: (s) => `mockErrors:${s}` }, +}) + +describe('send-row-error-message container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState', { errorType: 'someType' }), { + errors: 'mockErrors:mockState', + errorType: 'someType' }) + }) + + }) + +}) diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js new file mode 100644 index 000000000..b7528a15f --- /dev/null +++ b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js @@ -0,0 +1,43 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowErrorMessage from './send-row-error-message/' + +export default class SendRowWrapper extends Component { + + static propTypes = { + children: PropTypes.node, + errorType: PropTypes.string, + label: PropTypes.string, + showError: PropTypes.bool, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + render () { + const { + children, + errorType = '', + label, + showError = false, + } = this.props + + const formField = Array.isArray(children) ? children[1] || children[0] : children + const customLabelContent = children.length > 1 ? children[0] : null + + return ( +
+
+ {label} + {showError && } + {customLabelContent} +
+
+ {formField} +
+
+ ) + } + +} diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss b/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js b/ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js new file mode 100644 index 000000000..30280e1d0 --- /dev/null +++ b/ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js @@ -0,0 +1,79 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import SendRowWrapper from '../send-row-wrapper.component.js' + +import SendRowErrorMessage from '../send-row-error-message/send-row-error-message.container' + +describe('SendContent Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow( + Mock Form Field + ) + }) + + describe('render', () => { + it('should render a div with a send-v2__form-row class', () => { + assert.equal(wrapper.find('div.send-v2__form-row').length, 1) + }) + + it('should render two children of the root div, with send-v2_form label and field classes', () => { + assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-label').length, 1) + assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-field').length, 1) + }) + + it('should render the label as a child of the send-v2__form-label', () => { + assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-label').childAt(0).text(), 'mockLabel') + }) + + it('should render its first child as a child of the send-v2__form-field', () => { + assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-field').childAt(0).text(), 'Mock Form Field') + }) + + it('should not render a SendRowErrorMessage if showError is false', () => { + assert.equal(wrapper.find(SendRowErrorMessage).length, 0) + }) + + it('should render a SendRowErrorMessage with and errorType props if showError is true', () => { + wrapper.setProps({showError: true}) + assert.equal(wrapper.find(SendRowErrorMessage).length, 1) + + const expectedSendRowErrorMessage = wrapper.find('.send-v2__form-row > .send-v2__form-label').childAt(1) + assert(expectedSendRowErrorMessage.is(SendRowErrorMessage)) + assert.deepEqual( + expectedSendRowErrorMessage.props(), + { errorType: 'mockErrorType' } + ) + }) + + it('should render its second child as a child of the send-v2__form-field, if it has two children', () => { + wrapper = shallow( + Mock Custom Label Content + Mock Form Field + ) + assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-field').childAt(0).text(), 'Mock Form Field') + }) + + it('should render its first child as the last child of the send-v2__form-label, if it has two children', () => { + wrapper = shallow( + Mock Custom Label Content + Mock Form Field + ) + assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-label').childAt(1).text(), 'Mock Custom Label Content') + }) + }) +}) diff --git a/ui/app/components/send/send-content/send-to-row/index.js b/ui/app/components/send/send-content/send-to-row/index.js new file mode 100644 index 000000000..121f15148 --- /dev/null +++ b/ui/app/components/send/send-content/send-to-row/index.js @@ -0,0 +1 @@ +export { default } from './send-to-row.container' diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row-README.md b/ui/app/components/send/send-content/send-to-row/send-to-row-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js new file mode 100644 index 000000000..892ad5d67 --- /dev/null +++ b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper/' +import EnsInput from '../../../ens-input' +import { getToErrorObject } from './send-to-row.utils.js' + +export default class SendToRow extends Component { + + static propTypes = { + closeToDropdown: PropTypes.func, + inError: PropTypes.bool, + network: PropTypes.string, + openToDropdown: PropTypes.func, + to: PropTypes.string, + toAccounts: PropTypes.array, + toDropdownOpen: PropTypes.bool, + updateGas: PropTypes.func, + updateSendTo: PropTypes.func, + updateSendToError: PropTypes.func, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + handleToChange (to, nickname = '', toError) { + const { updateSendTo, updateSendToError, updateGas } = this.props + const toErrorObject = getToErrorObject(to, toError) + updateSendTo(to, nickname) + updateSendToError(toErrorObject) + if (toErrorObject.to === null) { + updateGas({ to }) + } + } + + render () { + const { + closeToDropdown, + inError, + network, + openToDropdown, + to, + toAccounts, + toDropdownOpen, + } = this.props + + return ( + + closeToDropdown()} + dropdownOpen={toDropdownOpen} + inError={inError} + name={'address'} + network={network} + onChange={({ toAddress, nickname, toError }) => this.handleToChange(toAddress, nickname, toError)} + openDropdown={() => openToDropdown()} + placeholder={this.context.t('recipientAddress')} + to={to} + /> + + ) + } + +} diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.container.js b/ui/app/components/send/send-content/send-to-row/send-to-row.container.js new file mode 100644 index 000000000..1c9c9d518 --- /dev/null +++ b/ui/app/components/send/send-content/send-to-row/send-to-row.container.js @@ -0,0 +1,42 @@ +import { connect } from 'react-redux' +import { + getCurrentNetwork, + getSendTo, + getSendToAccounts, +} from '../../send.selectors.js' +import { + getToDropdownOpen, + sendToIsInError, +} from './send-to-row.selectors.js' +import { + updateSendTo, +} from '../../../../actions' +import { + updateSendErrors, + openToDropdown, + closeToDropdown, +} from '../../../../ducks/send.duck' +import SendToRow from './send-to-row.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendToRow) + +function mapStateToProps (state) { + return { + inError: sendToIsInError(state), + network: getCurrentNetwork(state), + to: getSendTo(state), + toAccounts: getSendToAccounts(state), + toDropdownOpen: getToDropdownOpen(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + closeToDropdown: () => dispatch(closeToDropdown()), + openToDropdown: () => dispatch(openToDropdown()), + updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), + updateSendToError: (toErrorObject) => { + dispatch(updateSendErrors(toErrorObject)) + }, + } +} diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js b/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js new file mode 100644 index 000000000..8919014be --- /dev/null +++ b/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js @@ -0,0 +1,14 @@ +const selectors = { + getToDropdownOpen, + sendToIsInError, +} + +module.exports = selectors + +function getToDropdownOpen (state) { + return state.send.toDropdownOpen +} + +function sendToIsInError (state) { + return Boolean(state.send.errors.to) +} diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js new file mode 100644 index 000000000..6b90a9f09 --- /dev/null +++ b/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js @@ -0,0 +1,19 @@ +const { + REQUIRED_ERROR, + INVALID_RECIPIENT_ADDRESS_ERROR, +} = require('../../send.constants') +const { isValidAddress } = require('../../../../util') + +function getToErrorObject (to, toError = null) { + if (!to) { + toError = REQUIRED_ERROR + } else if (!isValidAddress(to) && !toError) { + toError = INVALID_RECIPIENT_ADDRESS_ERROR + } + + return { to: toError } +} + +module.exports = { + getToErrorObject, +} diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js new file mode 100644 index 000000000..781371004 --- /dev/null +++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js @@ -0,0 +1,149 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import proxyquire from 'proxyquire' + +const SendToRow = proxyquire('../send-to-row.component.js', { + './send-to-row.utils.js': { + getToErrorObject: (to, toError) => ({ + to: to === false ? null : `mockToErrorObject:${to}${toError}`, + }), + }, +}).default + +import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' +import EnsInput from '../../../../ens-input' + +const propsMethodSpies = { + closeToDropdown: sinon.spy(), + openToDropdown: sinon.spy(), + updateGas: sinon.spy(), + updateSendTo: sinon.spy(), + updateSendToError: sinon.spy(), +} + +sinon.spy(SendToRow.prototype, 'handleToChange') + +describe('SendToRow Component', function () { + let wrapper + let instance + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + instance = wrapper.instance() + }) + + afterEach(() => { + propsMethodSpies.closeToDropdown.resetHistory() + propsMethodSpies.openToDropdown.resetHistory() + propsMethodSpies.updateSendTo.resetHistory() + propsMethodSpies.updateSendToError.resetHistory() + SendToRow.prototype.handleToChange.resetHistory() + }) + + describe('handleToChange', () => { + + it('should call updateSendTo', () => { + assert.equal(propsMethodSpies.updateSendTo.callCount, 0) + instance.handleToChange('mockTo2', 'mockNickname') + assert.equal(propsMethodSpies.updateSendTo.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendTo.getCall(0).args, + ['mockTo2', 'mockNickname'] + ) + }) + + it('should call updateSendToError', () => { + assert.equal(propsMethodSpies.updateSendToError.callCount, 0) + instance.handleToChange('mockTo2', '', 'mockToError') + assert.equal(propsMethodSpies.updateSendToError.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendToError.getCall(0).args, + [{ to: 'mockToErrorObject:mockTo2mockToError' }] + ) + }) + + it('should not call updateGas if there is a to error', () => { + assert.equal(propsMethodSpies.updateGas.callCount, 0) + instance.handleToChange('mockTo2') + assert.equal(propsMethodSpies.updateGas.callCount, 0) + }) + + it('should call updateGas if there is no to error', () => { + assert.equal(propsMethodSpies.updateGas.callCount, 0) + instance.handleToChange(false) + assert.equal(propsMethodSpies.updateGas.callCount, 1) + }) + }) + + describe('render', () => { + it('should render a SendRowWrapper component', () => { + assert.equal(wrapper.find(SendRowWrapper).length, 1) + }) + + it('should pass the correct props to SendRowWrapper', () => { + const { + errorType, + label, + showError, + } = wrapper.find(SendRowWrapper).props() + + assert.equal(errorType, 'to') + + assert.equal(label, 'to_t') + + assert.equal(showError, false) + }) + + it('should render an EnsInput as a child of the SendRowWrapper', () => { + assert(wrapper.find(SendRowWrapper).childAt(0).is(EnsInput)) + }) + + it('should render the EnsInput with the correct props', () => { + const { + accounts, + closeDropdown, + dropdownOpen, + inError, + name, + network, + onChange, + openDropdown, + placeholder, + to, + } = wrapper.find(SendRowWrapper).childAt(0).props() + assert.deepEqual(accounts, ['mockAccount']) + assert.equal(dropdownOpen, false) + assert.equal(inError, false) + assert.equal(name, 'address') + assert.equal(network, 'mockNetwork') + assert.equal(placeholder, 'recipientAddress_t') + assert.equal(to, 'mockTo') + assert.equal(propsMethodSpies.closeToDropdown.callCount, 0) + closeDropdown() + assert.equal(propsMethodSpies.closeToDropdown.callCount, 1) + assert.equal(propsMethodSpies.openToDropdown.callCount, 0) + openDropdown() + assert.equal(propsMethodSpies.openToDropdown.callCount, 1) + assert.equal(SendToRow.prototype.handleToChange.callCount, 0) + onChange({ toAddress: 'mockNewTo', nickname: 'mockNewNickname', toError: 'mockToError' }) + assert.equal(SendToRow.prototype.handleToChange.callCount, 1) + assert.deepEqual( + SendToRow.prototype.handleToChange.getCall(0).args, + ['mockNewTo', 'mockNewNickname', 'mockToError'] + ) + }) + }) +}) diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js new file mode 100644 index 000000000..92355c00a --- /dev/null +++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js @@ -0,0 +1,113 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + updateSendTo: sinon.spy(), +} +const duckActionSpies = { + closeToDropdown: sinon.spy(), + openToDropdown: sinon.spy(), + updateSendErrors: sinon.spy(), +} + +proxyquire('../send-to-row.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../send.selectors.js': { + getCurrentNetwork: (s) => `mockNetwork:${s}`, + getSendTo: (s) => `mockTo:${s}`, + getSendToAccounts: (s) => `mockToAccounts:${s}`, + }, + './send-to-row.selectors.js': { + getToDropdownOpen: (s) => `mockToDropdownOpen:${s}`, + sendToIsInError: (s) => `mockInError:${s}`, + }, + '../../../../actions': actionSpies, + '../../../../ducks/send.duck': duckActionSpies, +}) + +describe('send-to-row container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + inError: 'mockInError:mockState', + network: 'mockNetwork:mockState', + to: 'mockTo:mockState', + toAccounts: 'mockToAccounts:mockState', + toDropdownOpen: 'mockToDropdownOpen:mockState', + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + describe('closeToDropdown()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.closeToDropdown() + assert(dispatchSpy.calledOnce) + assert(duckActionSpies.closeToDropdown.calledOnce) + assert.equal( + duckActionSpies.closeToDropdown.getCall(0).args[0], + undefined + ) + }) + }) + + describe('openToDropdown()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.openToDropdown() + assert(dispatchSpy.calledOnce) + assert(duckActionSpies.openToDropdown.calledOnce) + assert.equal( + duckActionSpies.openToDropdown.getCall(0).args[0], + undefined + ) + }) + }) + + describe('updateSendTo()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateSendTo('mockTo', 'mockNickname') + assert(dispatchSpy.calledOnce) + assert(actionSpies.updateSendTo.calledOnce) + assert.deepEqual( + actionSpies.updateSendTo.getCall(0).args, + ['mockTo', 'mockNickname'] + ) + }) + }) + + describe('updateSendToError()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateSendToError('mockToErrorObject') + assert(dispatchSpy.calledOnce) + assert(duckActionSpies.updateSendErrors.calledOnce) + assert.equal( + duckActionSpies.updateSendErrors.getCall(0).args[0], + 'mockToErrorObject' + ) + }) + }) + + }) + +}) diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js new file mode 100644 index 000000000..122ad3265 --- /dev/null +++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js @@ -0,0 +1,47 @@ +import assert from 'assert' +import { + getToDropdownOpen, + sendToIsInError, +} from '../send-to-row.selectors.js' + +describe('send-to-row selectors', () => { + + describe('getToDropdownOpen()', () => { + it('should return send.getToDropdownOpen', () => { + const state = { + send: { + toDropdownOpen: false, + }, + } + + assert.equal(getToDropdownOpen(state), false) + }) + }) + + describe('sendToIsInError()', () => { + it('should return true if send.errors.to is truthy', () => { + const state = { + send: { + errors: { + to: 'abc', + }, + }, + } + + assert.equal(sendToIsInError(state), true) + }) + + it('should return false if send.errors.to is falsy', () => { + const state = { + send: { + errors: { + to: null, + }, + }, + } + + assert.equal(sendToIsInError(state), false) + }) + }) + +}) diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js new file mode 100644 index 000000000..4d2447c32 --- /dev/null +++ b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js @@ -0,0 +1,51 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +import { + REQUIRED_ERROR, + INVALID_RECIPIENT_ADDRESS_ERROR, +} from '../../../send.constants' + +const stubs = { + isValidAddress: sinon.stub().callsFake(to => Boolean(to.match(/^[0xabcdef123456798]+$/))), +} + +const toRowUtils = proxyquire('../send-to-row.utils.js', { + '../../../../util': { + isValidAddress: stubs.isValidAddress, + }, +}) +const { + getToErrorObject, +} = toRowUtils + +describe('send-to-row utils', () => { + + describe('getToErrorObject()', () => { + it('should return a required error if to is falsy', () => { + assert.deepEqual(getToErrorObject(null), { + to: REQUIRED_ERROR, + }) + }) + + it('should return an invalid recipient error if to is truthy but invalid', () => { + assert.deepEqual(getToErrorObject('mockInvalidTo'), { + to: INVALID_RECIPIENT_ADDRESS_ERROR, + }) + }) + + it('should return null if to is truthy and valid', () => { + assert.deepEqual(getToErrorObject('0xabc123'), { + to: null, + }) + }) + + it('should return the passed error if to is truthy but invalid if to is truthy and valid', () => { + assert.deepEqual(getToErrorObject('invalid #$ 345878', 'someExplicitError'), { + to: 'someExplicitError', + }) + }) + }) + +}) diff --git a/ui/app/components/send/send-content/tests/send-content-component.test.js b/ui/app/components/send/send-content/tests/send-content-component.test.js new file mode 100644 index 000000000..d5bb6693c --- /dev/null +++ b/ui/app/components/send/send-content/tests/send-content-component.test.js @@ -0,0 +1,38 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import SendContent from '../send-content.component.js' + +import PageContainerContent from '../../../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' + +describe('SendContent Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow() + }) + + describe('render', () => { + it('should render a PageContainerContent component', () => { + assert.equal(wrapper.find(PageContainerContent).length, 1) + }) + + it('should render a div with a .send-v2__form class as a child of PageContainerContent', () => { + const PageContainerContentChild = wrapper.find(PageContainerContent).children() + PageContainerContentChild.is('div') + PageContainerContentChild.is('.send-v2__form') + }) + + it('should render the correct row components as grandchildren of the PageContainerContent component', () => { + 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)) + }) + }) +}) diff --git a/ui/app/components/send/send-footer/README.md b/ui/app/components/send/send-footer/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-footer/index.js b/ui/app/components/send/send-footer/index.js new file mode 100644 index 000000000..58e91d622 --- /dev/null +++ b/ui/app/components/send/send-footer/index.js @@ -0,0 +1 @@ +export { default } from './send-footer.container' diff --git a/ui/app/components/send/send-footer/send-footer.component.js b/ui/app/components/send/send-footer/send-footer.component.js new file mode 100644 index 000000000..2085f1dce --- /dev/null +++ b/ui/app/components/send/send-footer/send-footer.component.js @@ -0,0 +1,101 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import PageContainerFooter from '../../page-container/page-container-footer' +import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../routes' + +export default class SendFooter extends Component { + + static propTypes = { + addToAddressBookIfNew: PropTypes.func, + amount: PropTypes.string, + clearSend: PropTypes.func, + disabled: PropTypes.bool, + editingTransactionId: PropTypes.string, + errors: PropTypes.object, + from: PropTypes.object, + gasLimit: PropTypes.string, + gasPrice: PropTypes.string, + gasTotal: PropTypes.string, + history: PropTypes.object, + inError: PropTypes.bool, + selectedToken: PropTypes.object, + sign: PropTypes.func, + to: PropTypes.string, + toAccounts: PropTypes.array, + tokenBalance: PropTypes.string, + unapprovedTxs: PropTypes.object, + update: PropTypes.func, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + onCancel () { + this.props.clearSend() + this.props.history.push(DEFAULT_ROUTE) + } + + onSubmit (event) { + event.preventDefault() + const { + addToAddressBookIfNew, + amount, + editingTransactionId, + from: {address: from}, + gasLimit: gas, + gasPrice, + selectedToken, + sign, + to, + unapprovedTxs, + // updateTx, + update, + toAccounts, + history, + } = this.props + + // Should not be needed because submit should be disabled if there are errors. + // const noErrors = !amountError && toError === null + + // if (!noErrors) { + // return + // } + + // TODO: add nickname functionality + addToAddressBookIfNew(to, toAccounts) + + const promise = editingTransactionId + ? update({ + amount, + editingTransactionId, + from, + gas, + gasPrice, + selectedToken, + to, + unapprovedTxs, + }) + : sign({ selectedToken, to, amount, from, gas, gasPrice }) + + Promise.resolve(promise) + .then(() => history.push(CONFIRM_TRANSACTION_ROUTE)) + } + + formShouldBeDisabled () { + const { inError, selectedToken, tokenBalance, gasTotal, to } = this.props + const missingTokenBalance = selectedToken && !tokenBalance + return inError || !gasTotal || missingTokenBalance || !to + } + + render () { + return ( + this.onCancel()} + onSubmit={e => this.onSubmit(e)} + disabled={this.formShouldBeDisabled()} + /> + ) + } + +} diff --git a/ui/app/components/send/send-footer/send-footer.container.js b/ui/app/components/send/send-footer/send-footer.container.js new file mode 100644 index 000000000..0af6fcfa1 --- /dev/null +++ b/ui/app/components/send/send-footer/send-footer.container.js @@ -0,0 +1,100 @@ +import { connect } from 'react-redux' +import ethUtil from 'ethereumjs-util' +import { + addToAddressBook, + clearSend, + signTokenTx, + signTx, + updateTransaction, +} from '../../../actions' +import SendFooter from './send-footer.component' +import { + getGasLimit, + getGasPrice, + getGasTotal, + getSelectedToken, + getSendAmount, + getSendEditingTransactionId, + getSendFromObject, + getSendTo, + getSendToAccounts, + getTokenBalance, + getUnapprovedTxs, +} from '../send.selectors' +import { + isSendFormInError, +} from './send-footer.selectors' +import { + addressIsNew, + constructTxParams, + constructUpdatedTx, +} from './send-footer.utils' + +export default connect(mapStateToProps, mapDispatchToProps)(SendFooter) + +function mapStateToProps (state) { + return { + amount: getSendAmount(state), + editingTransactionId: getSendEditingTransactionId(state), + from: getSendFromObject(state), + gasLimit: getGasLimit(state), + gasPrice: getGasPrice(state), + gasTotal: getGasTotal(state), + inError: isSendFormInError(state), + selectedToken: getSelectedToken(state), + to: getSendTo(state), + toAccounts: getSendToAccounts(state), + tokenBalance: getTokenBalance(state), + unapprovedTxs: getUnapprovedTxs(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + clearSend: () => dispatch(clearSend()), + sign: ({ selectedToken, to, amount, from, gas, gasPrice }) => { + const txParams = constructTxParams({ + amount, + from, + gas, + gasPrice, + selectedToken, + to, + }) + + selectedToken + ? dispatch(signTokenTx(selectedToken.address, to, amount, txParams)) + : dispatch(signTx(txParams)) + }, + update: ({ + amount, + editingTransactionId, + from, + gas, + gasPrice, + selectedToken, + to, + unapprovedTxs, + }) => { + const editingTx = constructUpdatedTx({ + amount, + editingTransactionId, + from, + gas, + gasPrice, + selectedToken, + to, + unapprovedTxs, + }) + + return dispatch(updateTransaction(editingTx)) + }, + addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => { + const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress) + if (addressIsNew(toAccounts)) { + // TODO: nickname, i.e. addToAddressBook(recipient, nickname) + dispatch(addToAddressBook(hexPrefixedAddress, nickname)) + } + }, + } +} diff --git a/ui/app/components/send/send-footer/send-footer.scss b/ui/app/components/send/send-footer/send-footer.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-footer/send-footer.selectors.js b/ui/app/components/send/send-footer/send-footer.selectors.js new file mode 100644 index 000000000..e20addfdc --- /dev/null +++ b/ui/app/components/send/send-footer/send-footer.selectors.js @@ -0,0 +1,11 @@ +const { getSendErrors } = require('../send.selectors') + +const selectors = { + isSendFormInError, +} + +module.exports = selectors + +function isSendFormInError (state) { + return Object.values(getSendErrors(state)).some(n => n) +} diff --git a/ui/app/components/send/send-footer/send-footer.utils.js b/ui/app/components/send/send-footer/send-footer.utils.js new file mode 100644 index 000000000..875e7d948 --- /dev/null +++ b/ui/app/components/send/send-footer/send-footer.utils.js @@ -0,0 +1,81 @@ +const ethAbi = require('ethereumjs-abi') +const ethUtil = require('ethereumjs-util') +const { TOKEN_TRANSFER_FUNCTION_SIGNATURE } = require('../send.constants') + +function addHexPrefixToObjectValues (obj) { + return Object.keys(obj).reduce((newObj, key) => { + return { ...newObj, [key]: ethUtil.addHexPrefix(obj[key]) } + }, {}) +} + +function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice }) { + const txParams = { + from, + value: '0', + gas, + gasPrice, + } + + if (!selectedToken) { + txParams.value = amount + txParams.to = to + } + + const hexPrefixedTxParams = addHexPrefixToObjectValues(txParams) + + return hexPrefixedTxParams +} + +function constructUpdatedTx ({ + amount, + editingTransactionId, + from, + gas, + gasPrice, + selectedToken, + to, + unapprovedTxs, +}) { + const editingTx = { + ...unapprovedTxs[editingTransactionId], + txParams: addHexPrefixToObjectValues({ from, gas, gasPrice }), + } + + if (selectedToken) { + const data = TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( + ethAbi.rawEncode(['address', 'uint256'], [to, ethUtil.addHexPrefix(amount)]), + x => ('00' + x.toString(16)).slice(-2) + ).join('') + + Object.assign(editingTx.txParams, addHexPrefixToObjectValues({ + value: '0', + to: selectedToken.address, + data, + })) + } else { + const { data } = unapprovedTxs[editingTransactionId].txParams + + Object.assign(editingTx.txParams, addHexPrefixToObjectValues({ + value: amount, + to, + data, + })) + + if (typeof editingTx.txParams.data === 'undefined') { + delete editingTx.txParams.data + } + } + + return editingTx +} + +function addressIsNew (toAccounts, newAddress) { + return !toAccounts.find(({ address }) => newAddress === address) +} + +module.exports = { + addressIsNew, + constructTxParams, + constructUpdatedTx, + addHexPrefixToObjectValues, +} diff --git a/ui/app/components/send/send-footer/tests/send-footer-component.test.js b/ui/app/components/send/send-footer/tests/send-footer-component.test.js new file mode 100644 index 000000000..4b2cd327d --- /dev/null +++ b/ui/app/components/send/send-footer/tests/send-footer-component.test.js @@ -0,0 +1,230 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../routes' +import SendFooter from '../send-footer.component.js' + +import PageContainerFooter from '../../../page-container/page-container-footer' + +const propsMethodSpies = { + addToAddressBookIfNew: sinon.spy(), + clearSend: sinon.spy(), + sign: sinon.spy(), + update: sinon.spy(), +} +const historySpies = { + push: sinon.spy(), +} +const MOCK_EVENT = { preventDefault: () => {} } + +sinon.spy(SendFooter.prototype, 'onCancel') +sinon.spy(SendFooter.prototype, 'onSubmit') + +describe('SendFooter Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str } }) + }) + + afterEach(() => { + propsMethodSpies.clearSend.resetHistory() + propsMethodSpies.addToAddressBookIfNew.resetHistory() + propsMethodSpies.clearSend.resetHistory() + propsMethodSpies.sign.resetHistory() + propsMethodSpies.update.resetHistory() + historySpies.push.resetHistory() + SendFooter.prototype.onCancel.resetHistory() + SendFooter.prototype.onSubmit.resetHistory() + }) + + describe('onCancel', () => { + it('should call clearSend', () => { + assert.equal(propsMethodSpies.clearSend.callCount, 0) + wrapper.instance().onCancel() + assert.equal(propsMethodSpies.clearSend.callCount, 1) + }) + + it('should call history.push', () => { + assert.equal(historySpies.push.callCount, 0) + wrapper.instance().onCancel() + assert.equal(historySpies.push.callCount, 1) + assert.equal(historySpies.push.getCall(0).args[0], DEFAULT_ROUTE) + }) + }) + + + describe('formShouldBeDisabled()', () => { + const config = { + 'should return true if inError is truthy': { + inError: true, + expectedResult: true, + }, + 'should return true if gasTotal is falsy': { + inError: false, + gasTotal: false, + expectedResult: true, + }, + 'should return true if to is truthy': { + to: '0xsomevalidAddress', + inError: false, + gasTotal: false, + expectedResult: true, + }, + 'should return true if selectedToken is truthy and tokenBalance is falsy': { + selectedToken: true, + tokenBalance: null, + expectedResult: true, + }, + 'should return false if inError is false and all other params are truthy': { + inError: false, + gasTotal: '0x123', + selectedToken: true, + tokenBalance: 123, + expectedResult: false, + }, + } + Object.entries(config).map(([description, obj]) => { + it(description, () => { + wrapper.setProps(obj) + assert.equal(wrapper.instance().formShouldBeDisabled(), obj.expectedResult) + }) + }) + }) + + describe('onSubmit', () => { + it('should call addToAddressBookIfNew with the correct params', () => { + wrapper.instance().onSubmit(MOCK_EVENT) + assert(propsMethodSpies.addToAddressBookIfNew.calledOnce) + assert.deepEqual( + propsMethodSpies.addToAddressBookIfNew.getCall(0).args, + ['mockTo', ['mockAccount']] + ) + }) + + it('should call props.update if editingTransactionId is truthy', () => { + wrapper.instance().onSubmit(MOCK_EVENT) + assert(propsMethodSpies.update.calledOnce) + assert.deepEqual( + propsMethodSpies.update.getCall(0).args[0], + { + amount: 'mockAmount', + editingTransactionId: 'mockEditingTransactionId', + from: 'mockAddress', + gas: 'mockGasLimit', + gasPrice: 'mockGasPrice', + selectedToken: { mockProp: 'mockSelectedTokenProp' }, + to: 'mockTo', + unapprovedTxs: ['mockTx'], + } + ) + }) + + it('should not call props.sign if editingTransactionId is truthy', () => { + assert.equal(propsMethodSpies.sign.callCount, 0) + }) + + it('should call props.sign if editingTransactionId is falsy', () => { + wrapper.setProps({ editingTransactionId: null }) + wrapper.instance().onSubmit(MOCK_EVENT) + assert(propsMethodSpies.sign.calledOnce) + assert.deepEqual( + propsMethodSpies.sign.getCall(0).args[0], + { + amount: 'mockAmount', + from: 'mockAddress', + gas: 'mockGasLimit', + gasPrice: 'mockGasPrice', + selectedToken: { mockProp: 'mockSelectedTokenProp' }, + to: 'mockTo', + } + ) + }) + + it('should not call props.update if editingTransactionId is falsy', () => { + assert.equal(propsMethodSpies.update.callCount, 0) + }) + + it('should call history.push', done => { + Promise.resolve(wrapper.instance().onSubmit(MOCK_EVENT)) + .then(() => { + assert.equal(historySpies.push.callCount, 1) + assert.equal(historySpies.push.getCall(0).args[0], CONFIRM_TRANSACTION_ROUTE) + done() + }) + }) + }) + + describe('render', () => { + beforeEach(() => { + sinon.stub(SendFooter.prototype, 'formShouldBeDisabled').returns('formShouldBeDisabledReturn') + wrapper = shallow(, { context: { t: str => str } }) + }) + + afterEach(() => { + SendFooter.prototype.formShouldBeDisabled.restore() + }) + + it('should render a PageContainerFooter component', () => { + assert.equal(wrapper.find(PageContainerFooter).length, 1) + }) + + it('should pass the correct props to PageContainerFooter', () => { + const { + onCancel, + onSubmit, + disabled, + } = wrapper.find(PageContainerFooter).props() + assert.equal(disabled, 'formShouldBeDisabledReturn') + + assert.equal(SendFooter.prototype.onSubmit.callCount, 0) + onSubmit(MOCK_EVENT) + assert.equal(SendFooter.prototype.onSubmit.callCount, 1) + + assert.equal(SendFooter.prototype.onCancel.callCount, 0) + onCancel() + assert.equal(SendFooter.prototype.onCancel.callCount, 1) + }) + }) +}) diff --git a/ui/app/components/send/send-footer/tests/send-footer-container.test.js b/ui/app/components/send/send-footer/tests/send-footer-container.test.js new file mode 100644 index 000000000..39d6a7686 --- /dev/null +++ b/ui/app/components/send/send-footer/tests/send-footer-container.test.js @@ -0,0 +1,191 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + addToAddressBook: sinon.spy(), + clearSend: sinon.spy(), + signTokenTx: sinon.spy(), + signTx: sinon.spy(), + updateTransaction: sinon.spy(), +} +const utilsStubs = { + addressIsNew: sinon.stub().returns(true), + constructTxParams: sinon.stub().returns('mockConstructedTxParams'), + constructUpdatedTx: sinon.stub().returns('mockConstructedUpdatedTxParams'), +} + +proxyquire('../send-footer.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../../actions': actionSpies, + '../send.selectors': { + getGasLimit: (s) => `mockGasLimit:${s}`, + getGasPrice: (s) => `mockGasPrice:${s}`, + getGasTotal: (s) => `mockGasTotal:${s}`, + getSelectedToken: (s) => `mockSelectedToken:${s}`, + getSendAmount: (s) => `mockAmount:${s}`, + getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, + getSendFromObject: (s) => `mockFromObject:${s}`, + getSendTo: (s) => `mockTo:${s}`, + getSendToAccounts: (s) => `mockToAccounts:${s}`, + getTokenBalance: (s) => `mockTokenBalance:${s}`, + getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`, + }, + './send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` }, + './send-footer.utils': utilsStubs, +}) + +describe('send-footer container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + amount: 'mockAmount:mockState', + selectedToken: 'mockSelectedToken:mockState', + editingTransactionId: 'mockEditingTransactionId:mockState', + from: 'mockFromObject:mockState', + gasLimit: 'mockGasLimit:mockState', + gasPrice: 'mockGasPrice:mockState', + gasTotal: 'mockGasTotal:mockState', + inError: 'mockInError:mockState', + to: 'mockTo:mockState', + toAccounts: 'mockToAccounts:mockState', + tokenBalance: 'mockTokenBalance:mockState', + unapprovedTxs: 'mockUnapprovedTxs:mockState', + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + describe('clearSend()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.clearSend() + assert(dispatchSpy.calledOnce) + assert(actionSpies.clearSend.calledOnce) + }) + }) + + describe('sign()', () => { + it('should dispatch a signTokenTx action if selectedToken is defined', () => { + mapDispatchToPropsObject.sign({ + selectedToken: { + address: '0xabc', + }, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + }) + assert(dispatchSpy.calledOnce) + assert.deepEqual( + utilsStubs.constructTxParams.getCall(0).args[0], + { + selectedToken: { + address: '0xabc', + }, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + } + ) + assert.deepEqual( + actionSpies.signTokenTx.getCall(0).args, + [ '0xabc', 'mockTo', 'mockAmount', 'mockConstructedTxParams' ] + ) + }) + + it('should dispatch a sign action if selectedToken is not defined', () => { + utilsStubs.constructTxParams.resetHistory() + mapDispatchToPropsObject.sign({ + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + }) + assert(dispatchSpy.calledOnce) + assert.deepEqual( + utilsStubs.constructTxParams.getCall(0).args[0], + { + selectedToken: undefined, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + } + ) + assert.deepEqual( + actionSpies.signTx.getCall(0).args, + [ 'mockConstructedTxParams' ] + ) + }) + }) + + describe('update()', () => { + it('should dispatch an updateTransaction action', () => { + mapDispatchToPropsObject.update({ + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + editingTransactionId: 'mockEditingTransactionId', + selectedToken: 'mockSelectedToken', + unapprovedTxs: 'mockUnapprovedTxs', + }) + assert(dispatchSpy.calledOnce) + assert.deepEqual( + utilsStubs.constructUpdatedTx.getCall(0).args[0], + { + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + editingTransactionId: 'mockEditingTransactionId', + selectedToken: 'mockSelectedToken', + unapprovedTxs: 'mockUnapprovedTxs', + } + ) + assert.equal(actionSpies.updateTransaction.getCall(0).args[0], 'mockConstructedUpdatedTxParams') + }) + }) + + describe('addToAddressBookIfNew()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.addToAddressBookIfNew('mockNewAddress', 'mockToAccounts', 'mockNickname') + assert(dispatchSpy.calledOnce) + assert.equal(utilsStubs.addressIsNew.getCall(0).args[0], 'mockToAccounts') + assert.deepEqual( + actionSpies.addToAddressBook.getCall(0).args, + [ '0xmockNewAddress', 'mockNickname' ] + ) + }) + }) + + }) + +}) diff --git a/ui/app/components/send/send-footer/tests/send-footer-selectors.test.js b/ui/app/components/send/send-footer/tests/send-footer-selectors.test.js new file mode 100644 index 000000000..8de032f57 --- /dev/null +++ b/ui/app/components/send/send-footer/tests/send-footer-selectors.test.js @@ -0,0 +1,24 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +const { + isSendFormInError, +} = proxyquire('../send-footer.selectors', { + '../send.selectors': { + getSendErrors: (mockState) => mockState.errors, + }, +}) + +describe('send-footer selectors', () => { + + describe('getTitleKey()', () => { + it('should return true if any of the values of the object returned by getSendErrors are truthy', () => { + assert.equal(isSendFormInError({ errors: { a: 'abc', b: false} }), true) + }) + + it('should return false if all of the values of the object returned by getSendErrors are falsy', () => { + assert.equal(isSendFormInError({ errors: { a: false, b: null} }), false) + }) + }) + +}) diff --git a/ui/app/components/send/send-footer/tests/send-footer-utils.test.js b/ui/app/components/send/send-footer/tests/send-footer-utils.test.js new file mode 100644 index 000000000..2d3135995 --- /dev/null +++ b/ui/app/components/send/send-footer/tests/send-footer-utils.test.js @@ -0,0 +1,210 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' +const { TOKEN_TRANSFER_FUNCTION_SIGNATURE } = require('../../send.constants') + +const stubs = { + rawEncode: sinon.stub().callsFake((arr1, arr2) => { + return [ ...arr1, ...arr2 ] + }), +} + +const sendUtils = proxyquire('../send-footer.utils.js', { + 'ethereumjs-abi': { + rawEncode: stubs.rawEncode, + }, +}) +const { + addressIsNew, + constructTxParams, + constructUpdatedTx, + addHexPrefixToObjectValues, +} = sendUtils + +describe('send-footer utils', () => { + + describe('addHexPrefixToObjectValues()', () => { + it('should return a new object with the same properties with a 0x prefix', () => { + assert.deepEqual( + addHexPrefixToObjectValues({ + prop1: '0x123', + prop2: '456', + prop3: 'x', + }), + { + prop1: '0x123', + prop2: '0x456', + prop3: '0xx', + } + ) + }) + }) + + describe('addressIsNew()', () => { + it('should return false if the address exists in toAccounts', () => { + assert.equal( + addressIsNew([ + { address: '0xabc' }, + { address: '0xdef' }, + { address: '0xghi' }, + ], '0xdef'), + false + ) + }) + + it('should return true if the address does not exists in toAccounts', () => { + assert.equal( + addressIsNew([ + { address: '0xabc' }, + { address: '0xdef' }, + { address: '0xghi' }, + ], '0xxyz'), + true + ) + }) + }) + + describe('constructTxParams()', () => { + it('should return a new txParams object with value and to properties if there is no selectedToken', () => { + assert.deepEqual( + constructTxParams({ + selectedToken: false, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + }), + { + to: '0xmockTo', + value: '0xmockAmount', + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + } + ) + }) + + it('should return a new txParams object without a to property and a 0 value if there is a selectedToken', () => { + assert.deepEqual( + constructTxParams({ + selectedToken: true, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + }), + { + value: '0x0', + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + } + ) + }) + }) + + describe('constructUpdatedTx()', () => { + it('should return a new object with an updated txParams', () => { + const result = constructUpdatedTx({ + amount: 'mockAmount', + editingTransactionId: '0x456', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + selectedToken: false, + to: 'mockTo', + unapprovedTxs: { + '0x123': {}, + '0x456': { + unapprovedTxParam: 'someOtherParam', + txParams: { + data: 'someData', + }, + }, + }, + }) + + assert.deepEqual(result, { + unapprovedTxParam: 'someOtherParam', + txParams: { + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + value: '0xmockAmount', + to: '0xmockTo', + data: '0xsomeData', + }, + }) + }) + + it('should not have data property if there is non in the original tx', () => { + const result = constructUpdatedTx({ + amount: 'mockAmount', + editingTransactionId: '0x456', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + selectedToken: false, + to: 'mockTo', + unapprovedTxs: { + '0x123': {}, + '0x456': { + unapprovedTxParam: 'someOtherParam', + txParams: { + from: 'oldFrom', + gas: 'oldGas', + gasPrice: 'oldGasPrice', + }, + }, + }, + }) + + assert.deepEqual(result, { + unapprovedTxParam: 'someOtherParam', + txParams: { + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + value: '0xmockAmount', + to: '0xmockTo', + }, + }) + }) + + it('should have token property values if selectedToken is truthy', () => { + const result = constructUpdatedTx({ + amount: 'mockAmount', + editingTransactionId: '0x456', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + selectedToken: { + address: 'mockTokenAddress', + }, + to: 'mockTo', + unapprovedTxs: { + '0x123': {}, + '0x456': { + unapprovedTxParam: 'someOtherParam', + txParams: {}, + }, + }, + }) + + assert.deepEqual(result, { + unapprovedTxParam: 'someOtherParam', + txParams: { + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + value: '0x0', + to: '0xmockTokenAddress', + data: `${TOKEN_TRANSFER_FUNCTION_SIGNATURE}ss56Tont`, + }, + }) + }) + }) + +}) diff --git a/ui/app/components/send/send-header/README.md b/ui/app/components/send/send-header/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send-header/index.js b/ui/app/components/send/send-header/index.js new file mode 100644 index 000000000..0b17f0b7d --- /dev/null +++ b/ui/app/components/send/send-header/index.js @@ -0,0 +1 @@ +export { default } from './send-header.container' diff --git a/ui/app/components/send/send-header/send-header.component.js b/ui/app/components/send/send-header/send-header.component.js new file mode 100644 index 000000000..efc4bbf27 --- /dev/null +++ b/ui/app/components/send/send-header/send-header.component.js @@ -0,0 +1,34 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import PageContainerHeader from '../../page-container/page-container-header' +import { DEFAULT_ROUTE } from '../../../routes' + +export default class SendHeader extends Component { + + static propTypes = { + clearSend: PropTypes.func, + history: PropTypes.object, + titleKey: PropTypes.string, + subtitleParams: PropTypes.array, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + onClose () { + this.props.clearSend() + this.props.history.push(DEFAULT_ROUTE) + } + + render () { + return ( + this.onClose()} + subtitle={this.context.t(...this.props.subtitleParams)} + title={this.context.t(this.props.titleKey)} + /> + ) + } + +} diff --git a/ui/app/components/send/send-header/send-header.container.js b/ui/app/components/send/send-header/send-header.container.js new file mode 100644 index 000000000..4bcd0d1b6 --- /dev/null +++ b/ui/app/components/send/send-header/send-header.container.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux' +import { clearSend } from '../../../actions' +import SendHeader from './send-header.component' +import { getSubtitleParams, getTitleKey } from './send-header.selectors' + +export default connect(mapStateToProps, mapDispatchToProps)(SendHeader) + +function mapStateToProps (state) { + return { + titleKey: getTitleKey(state), + subtitleParams: getSubtitleParams(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + clearSend: () => dispatch(clearSend()), + } +} diff --git a/ui/app/components/send/send-header/send-header.selectors.js b/ui/app/components/send/send-header/send-header.selectors.js new file mode 100644 index 000000000..d7c9d3766 --- /dev/null +++ b/ui/app/components/send/send-header/send-header.selectors.js @@ -0,0 +1,37 @@ +const { + getSelectedToken, + getSendEditingTransactionId, +} = require('../send.selectors.js') + +const selectors = { + getTitleKey, + getSubtitleParams, +} + +module.exports = selectors + +function getTitleKey (state) { + const isEditing = Boolean(getSendEditingTransactionId(state)) + const isToken = Boolean(getSelectedToken(state)) + + if (isEditing) { + return 'edit' + } else if (isToken) { + return 'sendTokens' + } else { + return 'sendETH' + } +} + +function getSubtitleParams (state) { + const isEditing = Boolean(getSendEditingTransactionId(state)) + const token = getSelectedToken(state) + + if (isEditing) { + return [ 'editingTransaction' ] + } else if (token) { + return [ 'onlySendTokensToAccountAddress', [ token.symbol ] ] + } else { + return [ 'onlySendToEtherAddress' ] + } +} diff --git a/ui/app/components/send/send-header/tests/send-header-component.test.js b/ui/app/components/send/send-header/tests/send-header-component.test.js new file mode 100644 index 000000000..930bfa387 --- /dev/null +++ b/ui/app/components/send/send-header/tests/send-header-component.test.js @@ -0,0 +1,70 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import { DEFAULT_ROUTE } from '../../../../routes' +import SendHeader from '../send-header.component.js' + +import PageContainerHeader from '../../../page-container/page-container-header' + +const propsMethodSpies = { + clearSend: sinon.spy(), +} +const historySpies = { + push: sinon.spy(), +} + +sinon.spy(SendHeader.prototype, 'onClose') + +describe('SendHeader Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) + }) + + afterEach(() => { + propsMethodSpies.clearSend.resetHistory() + historySpies.push.resetHistory() + SendHeader.prototype.onClose.resetHistory() + }) + + describe('onClose', () => { + it('should call clearSend', () => { + assert.equal(propsMethodSpies.clearSend.callCount, 0) + wrapper.instance().onClose() + assert.equal(propsMethodSpies.clearSend.callCount, 1) + }) + + it('should call history.push', () => { + assert.equal(historySpies.push.callCount, 0) + wrapper.instance().onClose() + assert.equal(historySpies.push.callCount, 1) + assert.equal(historySpies.push.getCall(0).args[0], DEFAULT_ROUTE) + }) + }) + + describe('render', () => { + it('should render a PageContainerHeader compenent', () => { + assert.equal(wrapper.find(PageContainerHeader).length, 1) + }) + + 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() + assert.equal(SendHeader.prototype.onClose.callCount, 1) + }) + }) +}) diff --git a/ui/app/components/send/send-header/tests/send-header-container.test.js b/ui/app/components/send/send-header/tests/send-header-container.test.js new file mode 100644 index 000000000..41a7e8a89 --- /dev/null +++ b/ui/app/components/send/send-header/tests/send-header-container.test.js @@ -0,0 +1,59 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + clearSend: sinon.spy(), +} + +proxyquire('../send-header.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../../actions': actionSpies, + './send-header.selectors': { + getTitleKey: (s) => `mockTitleKey:${s}`, + getSubtitleParams: (s) => `mockSubtitleParams:${s}`, + }, +}) + +describe('send-header container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + titleKey: 'mockTitleKey:mockState', + subtitleParams: 'mockSubtitleParams:mockState', + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + describe('clearSend()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.clearSend() + assert(dispatchSpy.calledOnce) + assert(actionSpies.clearSend.calledOnce) + }) + }) + + }) + +}) diff --git a/ui/app/components/send/send-header/tests/send-header-selectors.test.js b/ui/app/components/send/send-header/tests/send-header-selectors.test.js new file mode 100644 index 000000000..e0c6a3ab3 --- /dev/null +++ b/ui/app/components/send/send-header/tests/send-header-selectors.test.js @@ -0,0 +1,47 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +const { + getTitleKey, + getSubtitleParams, +} = proxyquire('../send-header.selectors', { + '../send.selectors': { + getSelectedToken: (mockState) => mockState.t, + getSendEditingTransactionId: (mockState) => mockState.e, + }, +}) + +describe('send-header selectors', () => { + + describe('getTitleKey()', () => { + it('should return the correct key when getSendEditingTransactionId is truthy', () => { + assert.equal(getTitleKey({ e: 1, t: true }), 'edit') + }) + + it('should return the correct key when getSendEditingTransactionId is falsy and getSelectedToken is truthy', () => { + assert.equal(getTitleKey({ e: null, t: 'abc' }), 'sendTokens') + }) + + it('should return the correct key when getSendEditingTransactionId is falsy and getSelectedToken is falsy', () => { + assert.equal(getTitleKey({ e: null }), 'sendETH') + }) + }) + + describe('getSubtitleParams()', () => { + it('should return the correct params when getSendEditingTransactionId is truthy', () => { + assert.deepEqual(getSubtitleParams({ e: 1, t: true }), [ 'editingTransaction' ]) + }) + + it('should return the correct params when getSendEditingTransactionId is falsy and getSelectedToken is truthy', () => { + assert.deepEqual( + getSubtitleParams({ e: null, t: { symbol: 'ABC' } }), + [ 'onlySendTokensToAccountAddress', [ 'ABC' ] ] + ) + }) + + it('should return the correct params when getSendEditingTransactionId is falsy and getSelectedToken is falsy', () => { + assert.deepEqual(getSubtitleParams({ e: null }), [ 'onlySendToEtherAddress' ]) + }) + }) + +}) diff --git a/ui/app/components/send/send.component.js b/ui/app/components/send/send.component.js new file mode 100644 index 000000000..6f1b20c55 --- /dev/null +++ b/ui/app/components/send/send.component.js @@ -0,0 +1,179 @@ +import React from 'react' +import PropTypes from 'prop-types' +import PersistentForm from '../../../lib/persistent-form' +import { + getAmountErrorObject, + getGasFeeErrorObject, + getToAddressForGasUpdate, + doesAmountErrorRequireUpdate, +} from './send.utils' + +import SendHeader from './send-header/' +import SendContent from './send-content/' +import SendFooter from './send-footer/' + +export default class SendTransactionScreen extends PersistentForm { + + static propTypes = { + amount: PropTypes.string, + amountConversionRate: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), + blockGasLimit: PropTypes.string, + conversionRate: PropTypes.number, + editingTransactionId: PropTypes.string, + from: PropTypes.object, + gasLimit: PropTypes.string, + gasPrice: PropTypes.string, + gasTotal: PropTypes.string, + history: PropTypes.object, + network: PropTypes.string, + primaryCurrency: PropTypes.string, + recentBlocks: PropTypes.array, + selectedAddress: PropTypes.string, + selectedToken: PropTypes.object, + tokenBalance: PropTypes.string, + tokenContract: PropTypes.object, + updateAndSetGasTotal: PropTypes.func, + updateSendErrors: PropTypes.func, + updateSendTokenBalance: PropTypes.func, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + updateGas ({ to: updatedToAddress, amount: value } = {}) { + const { + amount, + blockGasLimit, + editingTransactionId, + gasLimit, + gasPrice, + recentBlocks, + selectedAddress, + selectedToken = {}, + to: currentToAddress, + updateAndSetGasTotal, + } = this.props + + updateAndSetGasTotal({ + blockGasLimit, + editingTransactionId, + gasLimit, + gasPrice, + recentBlocks, + selectedAddress, + selectedToken, + to: getToAddressForGasUpdate(updatedToAddress, currentToAddress), + value: value || amount, + }) + } + + componentDidUpdate (prevProps) { + const { + amount, + amountConversionRate, + conversionRate, + from: { address, balance }, + gasTotal, + network, + primaryCurrency, + selectedToken, + tokenBalance, + updateSendErrors, + updateSendTokenBalance, + tokenContract, + } = this.props + + const { + from: { balance: prevBalance }, + gasTotal: prevGasTotal, + tokenBalance: prevTokenBalance, + network: prevNetwork, + } = prevProps + + const uninitialized = [prevBalance, prevGasTotal].every(n => n === null) + + const amountErrorRequiresUpdate = doesAmountErrorRequireUpdate({ + balance, + gasTotal, + prevBalance, + prevGasTotal, + prevTokenBalance, + selectedToken, + tokenBalance, + }) + + if (amountErrorRequiresUpdate) { + const amountErrorObject = getAmountErrorObject({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + }) + const gasFeeErrorObject = selectedToken + ? getGasFeeErrorObject({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + }) + : { gasFee: null } + updateSendErrors(Object.assign(amountErrorObject, gasFeeErrorObject)) + } + + if (!uninitialized) { + + if (network !== prevNetwork && network !== 'loading') { + updateSendTokenBalance({ + selectedToken, + tokenContract, + address, + }) + this.updateGas() + } + } + } + + componentWillMount () { + const { + from: { address }, + selectedToken, + tokenContract, + updateSendTokenBalance, + } = this.props + updateSendTokenBalance({ + selectedToken, + tokenContract, + address, + }) + this.updateGas() + } + + componentWillUnmount () { + this.props.resetSendState() + } + + render () { + const { history } = this.props + + return ( +
+ + this.updateGas(updateData)}/> + +
+ ) + } + +} diff --git a/ui/app/components/send/send.constants.js b/ui/app/components/send/send.constants.js new file mode 100644 index 000000000..8acdf0641 --- /dev/null +++ b/ui/app/components/send/send.constants.js @@ -0,0 +1,57 @@ +const ethUtil = require('ethereumjs-util') +const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') + +const MIN_GAS_PRICE_DEC = '0' +const MIN_GAS_PRICE_HEX = (parseInt(MIN_GAS_PRICE_DEC)).toString(16) +const MIN_GAS_LIMIT_DEC = '21000' +const MIN_GAS_LIMIT_HEX = (parseInt(MIN_GAS_LIMIT_DEC)).toString(16) + +const MIN_GAS_PRICE_GWEI = ethUtil.addHexPrefix(conversionUtil(MIN_GAS_PRICE_HEX, { + fromDenomination: 'WEI', + toDenomination: 'GWEI', + fromNumericBase: 'hex', + toNumericBase: 'hex', + numberOfDecimals: 1, +})) + +const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT_HEX, MIN_GAS_PRICE_HEX, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, +}) + +const TOKEN_TRANSFER_FUNCTION_SIGNATURE = '0xa9059cbb' + +const INSUFFICIENT_FUNDS_ERROR = 'insufficientFunds' +const INSUFFICIENT_TOKENS_ERROR = 'insufficientTokens' +const NEGATIVE_ETH_ERROR = 'negativeETH' +const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient' +const REQUIRED_ERROR = 'required' + +const ONE_GWEI_IN_WEI_HEX = ethUtil.addHexPrefix(conversionUtil('0x1', { + fromDenomination: 'GWEI', + toDenomination: 'WEI', + fromNumericBase: 'hex', + toNumericBase: 'hex', +})) + +const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. +const BASE_TOKEN_GAS_COST = '0x186a0' // Hex for 100000, a base estimate for token transfers. + +module.exports = { + INSUFFICIENT_FUNDS_ERROR, + INSUFFICIENT_TOKENS_ERROR, + INVALID_RECIPIENT_ADDRESS_ERROR, + MIN_GAS_LIMIT_DEC, + MIN_GAS_LIMIT_HEX, + MIN_GAS_PRICE_DEC, + MIN_GAS_PRICE_GWEI, + MIN_GAS_PRICE_HEX, + MIN_GAS_TOTAL, + NEGATIVE_ETH_ERROR, + ONE_GWEI_IN_WEI_HEX, + REQUIRED_ERROR, + SIMPLE_GAS_COST, + TOKEN_TRANSFER_FUNCTION_SIGNATURE, + BASE_TOKEN_GAS_COST, +} diff --git a/ui/app/components/send/send.container.js b/ui/app/components/send/send.container.js new file mode 100644 index 000000000..44ebd2792 --- /dev/null +++ b/ui/app/components/send/send.container.js @@ -0,0 +1,93 @@ +import { connect } from 'react-redux' +import SendEther from './send.component' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import { + getAmountConversionRate, + getBlockGasLimit, + getConversionRate, + getCurrentNetwork, + getGasLimit, + getGasPrice, + getGasTotal, + getPrimaryCurrency, + getRecentBlocks, + getSelectedAddress, + getSelectedToken, + getSelectedTokenContract, + getSelectedTokenToFiatRate, + getSendAmount, + getSendEditingTransactionId, + getSendFromObject, + getSendTo, + getTokenBalance, +} from './send.selectors' +import { + updateSendTokenBalance, + updateGasData, + setGasTotal, +} from '../../actions' +import { + resetSendState, + updateSendErrors, +} from '../../ducks/send.duck' +import { + calcGasTotal, +} from './send.utils.js' + +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(SendEther) + +function mapStateToProps (state) { + return { + amount: getSendAmount(state), + amountConversionRate: getAmountConversionRate(state), + blockGasLimit: getBlockGasLimit(state), + conversionRate: getConversionRate(state), + editingTransactionId: getSendEditingTransactionId(state), + from: getSendFromObject(state), + gasLimit: getGasLimit(state), + gasPrice: getGasPrice(state), + gasTotal: getGasTotal(state), + network: getCurrentNetwork(state), + primaryCurrency: getPrimaryCurrency(state), + recentBlocks: getRecentBlocks(state), + selectedAddress: getSelectedAddress(state), + selectedToken: getSelectedToken(state), + to: getSendTo(state), + tokenBalance: getTokenBalance(state), + tokenContract: getSelectedTokenContract(state), + tokenToFiatRate: getSelectedTokenToFiatRate(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + updateAndSetGasTotal: ({ + blockGasLimit, + editingTransactionId, + gasLimit, + gasPrice, + recentBlocks, + selectedAddress, + selectedToken, + to, + value, + }) => { + !editingTransactionId + ? dispatch(updateGasData({ recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value })) + : dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice))) + }, + updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => { + dispatch(updateSendTokenBalance({ + selectedToken, + tokenContract, + address, + })) + }, + updateSendErrors: newError => dispatch(updateSendErrors(newError)), + resetSendState: () => dispatch(resetSendState()), + } +} diff --git a/ui/app/components/send/send.scss b/ui/app/components/send/send.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js new file mode 100644 index 000000000..f910f7caf --- /dev/null +++ b/ui/app/components/send/send.selectors.js @@ -0,0 +1,279 @@ +const { valuesFor } = require('../../util') +const abi = require('human-standard-token-abi') +const { + multiplyCurrencies, +} = require('../../conversion-util') +const { + estimateGasPriceFromRecentBlocks, +} = require('./send.utils') + +const selectors = { + accountsWithSendEtherInfoSelector, + // autoAddToBetaUI, + getAddressBook, + getAmountConversionRate, + getBlockGasLimit, + getConversionRate, + getCurrentAccountWithSendEtherInfo, + getCurrentCurrency, + getCurrentNetwork, + getCurrentViewContext, + getForceGasMin, + getGasLimit, + getGasPrice, + getGasPriceFromRecentBlocks, + getGasTotal, + getPrimaryCurrency, + getRecentBlocks, + getSelectedAccount, + getSelectedAddress, + getSelectedIdentity, + getSelectedToken, + getSelectedTokenContract, + getSelectedTokenExchangeRate, + getSelectedTokenToFiatRate, + getSendAmount, + getSendEditingTransactionId, + getSendErrors, + getSendFrom, + getSendFromBalance, + getSendFromObject, + getSendMaxModeState, + getSendTo, + getSendToAccounts, + getTokenBalance, + getTokenExchangeRate, + getUnapprovedTxs, + transactionsSelector, +} + +module.exports = selectors + +function accountsWithSendEtherInfoSelector (state) { + const { + accounts, + identities, + } = state.metamask + + const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => { + return Object.assign({}, account, identities[key]) + }) + + return accountsWithSendEtherInfo +} + +// function autoAddToBetaUI (state) { +// const autoAddTransactionThreshold = 12 +// const autoAddAccountsThreshold = 2 +// const autoAddTokensThreshold = 1 + +// const numberOfTransactions = state.metamask.selectedAddressTxList.length +// const numberOfAccounts = Object.keys(state.metamask.accounts).length +// const numberOfTokensAdded = state.metamask.tokens.length + +// const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) && +// (numberOfAccounts > autoAddAccountsThreshold) && +// (numberOfTokensAdded > autoAddTokensThreshold) +// const userIsNotInBeta = !state.metamask.featureFlags.betaUI + +// return userIsNotInBeta && userPassesThreshold +// } + +function getAddressBook (state) { + return state.metamask.addressBook +} + +function getAmountConversionRate (state) { + return getSelectedToken(state) + ? getSelectedTokenToFiatRate(state) + : getConversionRate(state) +} + +function getBlockGasLimit (state) { + return state.metamask.currentBlockGasLimit +} + +function getConversionRate (state) { + return state.metamask.conversionRate +} + +function getCurrentAccountWithSendEtherInfo (state) { + const currentAddress = getSelectedAddress(state) + const accounts = accountsWithSendEtherInfoSelector(state) + + return accounts.find(({ address }) => address === currentAddress) +} + +function getCurrentCurrency (state) { + return state.metamask.currentCurrency +} + +function getCurrentNetwork (state) { + return state.metamask.network +} + +function getCurrentViewContext (state) { + const { currentView = {} } = state.appState + return currentView.context +} + +function getForceGasMin (state) { + return state.metamask.send.forceGasMin +} + +function getGasLimit (state) { + return state.metamask.send.gasLimit +} + +function getGasPrice (state) { + return state.metamask.send.gasPrice +} + +function getGasPriceFromRecentBlocks (state) { + return estimateGasPriceFromRecentBlocks(state.metamask.recentBlocks) +} + +function getGasTotal (state) { + return state.metamask.send.gasTotal +} + +function getPrimaryCurrency (state) { + const selectedToken = getSelectedToken(state) + return selectedToken && selectedToken.symbol +} + +function getRecentBlocks (state) { + return state.metamask.recentBlocks +} + +function getSelectedAccount (state) { + const accounts = state.metamask.accounts + const selectedAddress = getSelectedAddress(state) + + return accounts[selectedAddress] +} + +function getSelectedAddress (state) { + const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0] + + return selectedAddress +} + +function getSelectedIdentity (state) { + const selectedAddress = getSelectedAddress(state) + const identities = state.metamask.identities + + return identities[selectedAddress] +} + +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 + + return selectedToken || sendToken || null +} + +function getSelectedTokenContract (state) { + const selectedToken = getSelectedToken(state) + + return selectedToken + ? global.eth.contract(abi).at(selectedToken.address) + : null +} + +function getSelectedTokenExchangeRate (state) { + const tokenExchangeRates = state.metamask.tokenExchangeRates + const selectedToken = getSelectedToken(state) || {} + const { symbol = '' } = selectedToken + const pair = `${symbol.toLowerCase()}_eth` + const { rate: tokenExchangeRate = 0 } = tokenExchangeRates && tokenExchangeRates[pair] || {} + + return tokenExchangeRate +} + +function getSelectedTokenToFiatRate (state) { + const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state) + const conversionRate = getConversionRate(state) + + const tokenToFiatRate = multiplyCurrencies( + conversionRate, + selectedTokenExchangeRate, + { toNumericBase: 'dec' } + ) + + return tokenToFiatRate +} + +function getSendAmount (state) { + return state.metamask.send.amount +} + +function getSendEditingTransactionId (state) { + return state.metamask.send.editingTransactionId +} + +function getSendErrors (state) { + return state.send.errors +} + +function getSendFrom (state) { + return state.metamask.send.from +} + +function getSendFromBalance (state) { + const from = getSendFrom(state) || getSelectedAccount(state) + return from.balance +} + +function getSendFromObject (state) { + return getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state) +} + +function getSendMaxModeState (state) { + return state.metamask.send.maxModeOn +} + +function getSendTo (state) { + return state.metamask.send.to +} + +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) +} + +function getTokenBalance (state) { + return state.metamask.send.tokenBalance +} + +function getTokenExchangeRate (state, tokenSymbol) { + const pair = `${tokenSymbol.toLowerCase()}_eth` + const tokenExchangeRates = state.metamask.tokenExchangeRates + const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} + + return tokenExchangeRate +} + +function getUnapprovedTxs (state) { + return state.metamask.unapprovedTxs +} + +function transactionsSelector (state) { + const { network, selectedTokenAddress } = state.metamask + const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs) + const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined + const transactions = state.metamask.selectedAddressTxList || [] + const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) + + return selectedTokenAddress + ? txsToRender + .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress) + .sort((a, b) => b.time - a.time) + : txsToRender + .sort((a, b) => b.time - a.time) +} diff --git a/ui/app/components/send/send.utils.js b/ui/app/components/send/send.utils.js new file mode 100644 index 000000000..aa255c3d4 --- /dev/null +++ b/ui/app/components/send/send.utils.js @@ -0,0 +1,312 @@ +const { + addCurrencies, + conversionUtil, + conversionGTE, + multiplyCurrencies, + conversionGreaterThan, + conversionLessThan, +} = require('../../conversion-util') +const { + calcTokenAmount, +} = require('../../token-util') +const { + BASE_TOKEN_GAS_COST, + INSUFFICIENT_FUNDS_ERROR, + INSUFFICIENT_TOKENS_ERROR, + NEGATIVE_ETH_ERROR, + ONE_GWEI_IN_WEI_HEX, + SIMPLE_GAS_COST, + TOKEN_TRANSFER_FUNCTION_SIGNATURE, +} = require('./send.constants') +const abi = require('ethereumjs-abi') +const ethUtil = require('ethereumjs-util') + +module.exports = { + addGasBuffer, + calcGasTotal, + calcTokenBalance, + doesAmountErrorRequireUpdate, + estimateGas, + estimateGasPriceFromRecentBlocks, + generateTokenTransferData, + getAmountErrorObject, + getGasFeeErrorObject, + getToAddressForGasUpdate, + isBalanceSufficient, + isTokenBalanceSufficient, + removeLeadingZeroes, +} + +function calcGasTotal (gasLimit = '0', gasPrice = '0') { + return multiplyCurrencies(gasLimit, gasPrice, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + }) +} + +function isBalanceSufficient ({ + amount = '0x0', + amountConversionRate = 1, + balance = '0x0', + conversionRate = 1, + gasTotal = '0x0', + primaryCurrency, +}) { + const totalAmount = addCurrencies(amount, gasTotal, { + aBase: 16, + bBase: 16, + toNumericBase: 'hex', + }) + + const balanceIsSufficient = conversionGTE( + { + value: balance, + fromNumericBase: 'hex', + fromCurrency: primaryCurrency, + conversionRate, + }, + { + value: totalAmount, + fromNumericBase: 'hex', + conversionRate: Number(amountConversionRate) || conversionRate, + fromCurrency: primaryCurrency, + }, + ) + + return balanceIsSufficient +} + +function isTokenBalanceSufficient ({ + amount = '0x0', + tokenBalance, + decimals, +}) { + const amountInDec = conversionUtil(amount, { + fromNumericBase: 'hex', + }) + + const tokenBalanceIsSufficient = conversionGTE( + { + value: tokenBalance, + fromNumericBase: 'dec', + }, + { + value: calcTokenAmount(amountInDec, decimals), + fromNumericBase: 'dec', + }, + ) + + return tokenBalanceIsSufficient +} + +function getAmountErrorObject ({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, +}) { + let insufficientFunds = false + if (gasTotal && conversionRate && !selectedToken) { + insufficientFunds = !isBalanceSufficient({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + }) + } + + let inSufficientTokens = false + if (selectedToken && tokenBalance !== null) { + const { decimals } = selectedToken + inSufficientTokens = !isTokenBalanceSufficient({ + tokenBalance, + amount, + decimals, + }) + } + + const amountLessThanZero = conversionGreaterThan( + { value: 0, fromNumericBase: 'dec' }, + { value: amount, fromNumericBase: 'hex' }, + ) + + let amountError = null + + if (insufficientFunds) { + amountError = INSUFFICIENT_FUNDS_ERROR + } else if (inSufficientTokens) { + amountError = INSUFFICIENT_TOKENS_ERROR + } else if (amountLessThanZero) { + amountError = NEGATIVE_ETH_ERROR + } + + return { amount: amountError } +} + +function getGasFeeErrorObject ({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, +}) { + let gasFeeError = null + + if (gasTotal && conversionRate) { + const insufficientFunds = !isBalanceSufficient({ + amount: '0x0', + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + }) + + if (insufficientFunds) { + gasFeeError = INSUFFICIENT_FUNDS_ERROR + } + } + + return { gasFee: gasFeeError } +} + +function calcTokenBalance ({ selectedToken, usersToken }) { + const { decimals } = selectedToken || {} + return calcTokenAmount(usersToken.balance.toString(), decimals) + '' +} + +function doesAmountErrorRequireUpdate ({ + balance, + gasTotal, + prevBalance, + prevGasTotal, + prevTokenBalance, + selectedToken, + tokenBalance, +}) { + const balanceHasChanged = balance !== prevBalance + const gasTotalHasChange = gasTotal !== prevGasTotal + const tokenBalanceHasChanged = selectedToken && tokenBalance !== prevTokenBalance + const amountErrorRequiresUpdate = balanceHasChanged || gasTotalHasChange || tokenBalanceHasChanged + + return amountErrorRequiresUpdate +} + +async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to, value, gasPrice, estimateGasMethod }) { + const paramsForGasEstimate = { from: selectedAddress, value, gasPrice } + + if (selectedToken) { + paramsForGasEstimate.value = '0x0' + paramsForGasEstimate.data = generateTokenTransferData({ toAddress: to, amount: value, selectedToken }) + } + + // if recipient has no code, gas is 21k max: + if (!selectedToken) { + const code = Boolean(to) && await global.eth.getCode(to) + if (!code || code === '0x') { + return SIMPLE_GAS_COST + } + } else if (selectedToken && !to) { + return BASE_TOKEN_GAS_COST + } + + paramsForGasEstimate.to = selectedToken ? selectedToken.address : to + + // if not, fall back to block gasLimit + paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit, 0.95, { + multiplicandBase: 16, + multiplierBase: 10, + roundDown: '0', + toNumericBase: 'hex', + })) + // run tx + return new Promise((resolve, reject) => { + return estimateGasMethod(paramsForGasEstimate, (err, estimatedGas) => { + if (err) { + const simulationFailed = ( + err.message.includes('Transaction execution error.') || + err.message.includes('gas required exceeds allowance or always failing transaction') + ) + if (simulationFailed) { + const estimateWithBuffer = addGasBuffer(paramsForGasEstimate.gas, blockGasLimit, 1.5) + return resolve(ethUtil.addHexPrefix(estimateWithBuffer)) + } else { + return reject(err) + } + } + const estimateWithBuffer = addGasBuffer(estimatedGas.toString(16), blockGasLimit, 1.5) + return resolve(ethUtil.addHexPrefix(estimateWithBuffer)) + }) + }) +} + +function addGasBuffer (initialGasLimitHex, blockGasLimitHex, bufferMultiplier = 1.5) { + const upperGasLimit = multiplyCurrencies(blockGasLimitHex, 0.9, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 10, + numberOfDecimals: '0', + }) + const bufferedGasLimit = multiplyCurrencies(initialGasLimitHex, bufferMultiplier, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 10, + numberOfDecimals: '0', + }) + + // if initialGasLimit is above blockGasLimit, dont modify it + if (conversionGreaterThan( + { value: initialGasLimitHex, fromNumericBase: 'hex' }, + { value: upperGasLimit, fromNumericBase: 'hex' }, + )) return initialGasLimitHex + // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit + if (conversionLessThan( + { value: bufferedGasLimit, fromNumericBase: 'hex' }, + { value: upperGasLimit, fromNumericBase: 'hex' }, + )) return bufferedGasLimit + // otherwise use blockGasLimit + return upperGasLimit +} + +function generateTokenTransferData ({ toAddress = '0x0', amount = '0x0', selectedToken }) { + if (!selectedToken) return + return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( + abi.rawEncode(['address', 'uint256'], [toAddress, ethUtil.addHexPrefix(amount)]), + x => ('00' + x.toString(16)).slice(-2) + ).join('') +} + +function estimateGasPriceFromRecentBlocks (recentBlocks) { + // Return 1 gwei if no blocks have been observed: + if (!recentBlocks || recentBlocks.length === 0) { + return ONE_GWEI_IN_WEI_HEX + } + + const lowestPrices = recentBlocks.map((block) => { + if (!block.gasPrices || block.gasPrices.length < 1) { + return ONE_GWEI_IN_WEI_HEX + } + return block.gasPrices.reduce((currentLowest, next) => { + return parseInt(next, 16) < parseInt(currentLowest, 16) ? next : currentLowest + }) + }) + .sort((a, b) => parseInt(a, 16) > parseInt(b, 16) ? 1 : -1) + + return lowestPrices[Math.floor(lowestPrices.length / 2)] +} + +function getToAddressForGasUpdate (...addresses) { + return [...addresses, ''].find(str => str !== undefined && str !== null).toLowerCase() +} + +function removeLeadingZeroes (str) { + return str.replace(/^0*(?=\d)/, '') +} diff --git a/ui/app/components/send/tests/send-component.test.js b/ui/app/components/send/tests/send-component.test.js new file mode 100644 index 000000000..6194ec508 --- /dev/null +++ b/ui/app/components/send/tests/send-component.test.js @@ -0,0 +1,332 @@ +import React from 'react' +import assert from 'assert' +import proxyquire from 'proxyquire' +import { shallow } from 'enzyme' +import sinon from 'sinon' + +import SendHeader from '../send-header/send-header.container' +import SendContent from '../send-content/send-content.component' +import SendFooter from '../send-footer/send-footer.container' + +const propsMethodSpies = { + updateAndSetGasTotal: sinon.spy(), + updateSendErrors: sinon.spy(), + updateSendTokenBalance: sinon.spy(), + resetSendState: sinon.spy(), +} +const utilsMethodStubs = { + getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }), + getGasFeeErrorObject: sinon.stub().returns({ gasFee: 'mockGasFeeError' }), + doesAmountErrorRequireUpdate: sinon.stub().callsFake(obj => obj.balance !== obj.prevBalance), +} + +const SendTransactionScreen = proxyquire('../send.component.js', { + './send.utils': utilsMethodStubs, +}).default + +sinon.spy(SendTransactionScreen.prototype, 'componentDidMount') +sinon.spy(SendTransactionScreen.prototype, 'updateGas') + +describe('Send Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow() + }) + + afterEach(() => { + SendTransactionScreen.prototype.componentDidMount.resetHistory() + SendTransactionScreen.prototype.updateGas.resetHistory() + utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory() + utilsMethodStubs.getAmountErrorObject.resetHistory() + utilsMethodStubs.getGasFeeErrorObject.resetHistory() + propsMethodSpies.updateAndSetGasTotal.resetHistory() + propsMethodSpies.updateSendErrors.resetHistory() + propsMethodSpies.updateSendTokenBalance.resetHistory() + }) + + it('should call componentDidMount', () => { + assert(SendTransactionScreen.prototype.componentDidMount.calledOnce) + }) + + describe('componentWillMount', () => { + it('should call this.updateGas', () => { + SendTransactionScreen.prototype.updateGas.resetHistory() + propsMethodSpies.updateSendErrors.resetHistory() + assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0) + wrapper.instance().componentWillMount() + assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1) + }) + }) + + describe('componentWillUnmount', () => { + it('should call this.props.resetSendState', () => { + propsMethodSpies.resetSendState.resetHistory() + assert.equal(propsMethodSpies.resetSendState.callCount, 0) + wrapper.instance().componentWillUnmount() + assert.equal(propsMethodSpies.resetSendState.callCount, 1) + }) + }) + + describe('componentDidUpdate', () => { + it('should call doesAmountErrorRequireUpdate with the expected params', () => { + utilsMethodStubs.getAmountErrorObject.resetHistory() + wrapper.instance().componentDidUpdate({ + from: { + balance: '', + }, + }) + assert(utilsMethodStubs.doesAmountErrorRequireUpdate.calledOnce) + assert.deepEqual( + utilsMethodStubs.doesAmountErrorRequireUpdate.getCall(0).args[0], + { + balance: 'mockBalance', + gasTotal: 'mockGasTotal', + prevBalance: '', + prevGasTotal: undefined, + prevTokenBalance: undefined, + selectedToken: 'mockSelectedToken', + tokenBalance: 'mockTokenBalance', + } + ) + }) + + it('should not call getAmountErrorObject if doesAmountErrorRequireUpdate returns false', () => { + utilsMethodStubs.getAmountErrorObject.resetHistory() + wrapper.instance().componentDidUpdate({ + from: { + balance: 'mockBalance', + }, + }) + assert.equal(utilsMethodStubs.getAmountErrorObject.callCount, 0) + }) + + it('should call getAmountErrorObject if doesAmountErrorRequireUpdate returns true', () => { + utilsMethodStubs.getAmountErrorObject.resetHistory() + wrapper.instance().componentDidUpdate({ + from: { + balance: 'balanceChanged', + }, + }) + assert.equal(utilsMethodStubs.getAmountErrorObject.callCount, 1) + assert.deepEqual( + utilsMethodStubs.getAmountErrorObject.getCall(0).args[0], + { + amount: 'mockAmount', + amountConversionRate: 'mockAmountConversionRate', + balance: 'mockBalance', + conversionRate: 10, + gasTotal: 'mockGasTotal', + primaryCurrency: 'mockPrimaryCurrency', + selectedToken: 'mockSelectedToken', + tokenBalance: 'mockTokenBalance', + } + ) + }) + + it('should call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true and selectedToken is truthy', () => { + utilsMethodStubs.getGasFeeErrorObject.resetHistory() + wrapper.instance().componentDidUpdate({ + from: { + balance: 'balanceChanged', + }, + }) + assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 1) + assert.deepEqual( + utilsMethodStubs.getGasFeeErrorObject.getCall(0).args[0], + { + amount: 'mockAmount', + amountConversionRate: 'mockAmountConversionRate', + balance: 'mockBalance', + conversionRate: 10, + gasTotal: 'mockGasTotal', + primaryCurrency: 'mockPrimaryCurrency', + selectedToken: 'mockSelectedToken', + tokenBalance: 'mockTokenBalance', + } + ) + }) + + it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns false', () => { + utilsMethodStubs.getGasFeeErrorObject.resetHistory() + wrapper.instance().componentDidUpdate({ + from: { address: 'mockAddress', balance: 'mockBalance' }, + }) + assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0) + }) + + it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true but selectedToken is falsy', () => { + utilsMethodStubs.getGasFeeErrorObject.resetHistory() + wrapper.setProps({ selectedToken: null }) + wrapper.instance().componentDidUpdate({ + from: { + balance: 'balanceChanged', + }, + }) + assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0) + }) + + it('should call updateSendErrors with the expected params if selectedToken is falsy', () => { + propsMethodSpies.updateSendErrors.resetHistory() + wrapper.setProps({ selectedToken: null }) + wrapper.instance().componentDidUpdate({ + from: { + balance: 'balanceChanged', + }, + }) + assert.equal(propsMethodSpies.updateSendErrors.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendErrors.getCall(0).args[0], + { amount: 'mockAmountError', gasFee: null } + ) + }) + + it('should call updateSendErrors with the expected params if selectedToken is truthy', () => { + propsMethodSpies.updateSendErrors.resetHistory() + wrapper.setProps({ selectedToken: 'someToken' }) + wrapper.instance().componentDidUpdate({ + from: { + balance: 'balanceChanged', + }, + }) + assert.equal(propsMethodSpies.updateSendErrors.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendErrors.getCall(0).args[0], + { amount: 'mockAmountError', gasFee: 'mockGasFeeError' } + ) + }) + + it('should not call updateSendTokenBalance or this.updateGas if network === prevNetwork', () => { + SendTransactionScreen.prototype.updateGas.resetHistory() + propsMethodSpies.updateSendTokenBalance.resetHistory() + wrapper.instance().componentDidUpdate({ + from: { + balance: 'balanceChanged', + }, + network: '3', + }) + assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 0) + assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0) + }) + + it('should not call updateSendTokenBalance or this.updateGas if network === loading', () => { + wrapper.setProps({ network: 'loading' }) + SendTransactionScreen.prototype.updateGas.resetHistory() + propsMethodSpies.updateSendTokenBalance.resetHistory() + wrapper.instance().componentDidUpdate({ + from: { + balance: 'balanceChanged', + }, + network: '3', + }) + assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 0) + assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0) + }) + + it('should call updateSendTokenBalance and this.updateGas with the correct params', () => { + SendTransactionScreen.prototype.updateGas.resetHistory() + propsMethodSpies.updateSendTokenBalance.resetHistory() + wrapper.instance().componentDidUpdate({ + from: { + balance: 'balanceChanged', + }, + network: '2', + }) + assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendTokenBalance.getCall(0).args[0], + { + selectedToken: 'mockSelectedToken', + tokenContract: 'mockTokenContract', + address: 'mockAddress', + } + ) + assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1) + assert.deepEqual( + SendTransactionScreen.prototype.updateGas.getCall(0).args, + [] + ) + }) + }) + + describe('updateGas', () => { + it('should call updateAndSetGasTotal with the correct params if no to prop is passed', () => { + propsMethodSpies.updateAndSetGasTotal.resetHistory() + wrapper.instance().updateGas() + assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0], + { + blockGasLimit: 'mockBlockGasLimit', + editingTransactionId: 'mockEditingTransactionId', + gasLimit: 'mockGasLimit', + gasPrice: 'mockGasPrice', + recentBlocks: ['mockBlock'], + selectedAddress: 'mockSelectedAddress', + selectedToken: 'mockSelectedToken', + to: '', + value: 'mockAmount', + } + ) + }) + + it('should call updateAndSetGasTotal with the correct params if a to prop is passed', () => { + propsMethodSpies.updateAndSetGasTotal.resetHistory() + wrapper.setProps({ to: 'someAddress' }) + wrapper.instance().updateGas() + assert.equal( + propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, + 'someaddress', + ) + }) + + it('should call updateAndSetGasTotal with to set to lowercase if passed', () => { + propsMethodSpies.updateAndSetGasTotal.resetHistory() + wrapper.instance().updateGas({ to: '0xABC' }) + assert.equal(propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, '0xabc') + }) + }) + + describe('render', () => { + it('should render a page-container class', () => { + assert.equal(wrapper.find('.page-container').length, 1) + }) + + it('should render SendHeader, SendContent and SendFooter', () => { + assert.equal(wrapper.find(SendHeader).length, 1) + assert.equal(wrapper.find(SendContent).length, 1) + assert.equal(wrapper.find(SendFooter).length, 1) + }) + + it('should pass the history prop to SendHeader and SendFooter', () => { + assert.deepEqual( + wrapper.find(SendFooter).props(), + { + history: { mockProp: 'history-abc' }, + } + ) + }) + }) +}) diff --git a/ui/app/components/send/tests/send-container.test.js b/ui/app/components/send/tests/send-container.test.js new file mode 100644 index 000000000..7a9120d24 --- /dev/null +++ b/ui/app/components/send/tests/send-container.test.js @@ -0,0 +1,169 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + updateSendTokenBalance: sinon.spy(), + updateGasData: sinon.spy(), + setGasTotal: sinon.spy(), +} +const duckActionSpies = { + updateSendErrors: sinon.spy(), + resetSendState: sinon.spy(), +} + +proxyquire('../send.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + 'react-router-dom': { withRouter: () => {} }, + 'recompose': { compose: (arg1, arg2) => () => arg2() }, + './send.selectors': { + getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, + getBlockGasLimit: (s) => `mockBlockGasLimit:${s}`, + getConversionRate: (s) => `mockConversionRate:${s}`, + getCurrentNetwork: (s) => `mockNetwork:${s}`, + getGasLimit: (s) => `mockGasLimit:${s}`, + getGasPrice: (s) => `mockGasPrice:${s}`, + 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}`, + getSendAmount: (s) => `mockAmount:${s}`, + getSendTo: (s) => `mockTo:${s}`, + getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, + getSendFromObject: (s) => `mockFrom:${s}`, + getTokenBalance: (s) => `mockTokenBalance:${s}`, + }, + '../../actions': actionSpies, + '../../ducks/send.duck': duckActionSpies, + './send.utils.js': { + calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice, + }, +}) + +describe('send container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + amount: 'mockAmount:mockState', + amountConversionRate: 'mockAmountConversionRate:mockState', + blockGasLimit: 'mockBlockGasLimit:mockState', + conversionRate: 'mockConversionRate:mockState', + editingTransactionId: 'mockEditingTransactionId:mockState', + from: 'mockFrom:mockState', + gasLimit: 'mockGasLimit:mockState', + gasPrice: 'mockGasPrice:mockState', + gasTotal: 'mockGasTotal:mockState', + network: 'mockNetwork:mockState', + primaryCurrency: 'mockPrimaryCurrency:mockState', + recentBlocks: 'mockRecentBlocks:mockState', + selectedAddress: 'mockSelectedAddress:mockState', + selectedToken: 'mockSelectedToken:mockState', + to: 'mockTo:mockState', + tokenBalance: 'mockTokenBalance:mockState', + tokenContract: 'mockTokenContract:mockState', + tokenToFiatRate: 'mockTokenToFiatRate:mockState', + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + describe('updateAndSetGasTotal()', () => { + const mockProps = { + blockGasLimit: 'mockBlockGasLimit', + editingTransactionId: '0x2', + gasLimit: '0x3', + gasPrice: '0x4', + recentBlocks: ['mockBlock'], + selectedAddress: '0x4', + selectedToken: { address: '0x1' }, + to: 'mockTo', + value: 'mockValue', + } + + it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => { + mapDispatchToPropsObject.updateAndSetGasTotal(mockProps) + assert(dispatchSpy.calledOnce) + assert.equal( + actionSpies.setGasTotal.getCall(0).args[0], + '0x30x4' + ) + }) + + it('should dispatch an updateGasData action when editingTransactionId is falsy', () => { + const { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value } = mockProps + mapDispatchToPropsObject.updateAndSetGasTotal( + Object.assign({}, mockProps, {editingTransactionId: false}) + ) + assert(dispatchSpy.calledOnce) + assert.deepEqual( + actionSpies.updateGasData.getCall(0).args[0], + { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value } + ) + }) + }) + + describe('updateSendTokenBalance()', () => { + const mockProps = { + address: '0x10', + tokenContract: '0x00a', + selectedToken: {address: '0x1'}, + } + + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateSendTokenBalance(Object.assign({}, mockProps)) + assert(dispatchSpy.calledOnce) + assert.deepEqual( + actionSpies.updateSendTokenBalance.getCall(0).args[0], + mockProps + ) + }) + }) + + describe('updateSendErrors()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateSendErrors('mockError') + assert(dispatchSpy.calledOnce) + assert.equal( + duckActionSpies.updateSendErrors.getCall(0).args[0], + 'mockError' + ) + }) + }) + + describe('resetSendState()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.resetSendState() + assert(dispatchSpy.calledOnce) + assert.equal( + duckActionSpies.resetSendState.getCall(0).args.length, + 0 + ) + }) + }) + + }) + +}) diff --git a/ui/app/components/send/tests/send-selectors-test-data.js b/ui/app/components/send/tests/send-selectors-test-data.js new file mode 100644 index 000000000..8f9c19314 --- /dev/null +++ b/ui/app/components/send/tests/send-selectors-test-data.js @@ -0,0 +1,230 @@ +module.exports = { + 'metamask': { + 'isInitialized': true, + 'isUnlocked': true, + 'featureFlags': {'betaUI': true}, + 'rpcTarget': 'https://rawtestrpc.metamask.io/', + 'identities': { + '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { + 'address': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + 'name': 'Send Account 1', + }, + '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { + 'address': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + 'name': 'Send Account 2', + }, + '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { + 'address': '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', + 'name': 'Send Account 3', + }, + '0xd85a4b6a394794842887b8284293d69163007bbb': { + 'address': '0xd85a4b6a394794842887b8284293d69163007bbb', + 'name': 'Send Account 4', + }, + }, + 'currentBlockGasLimit': '0x4c1878', + 'currentCurrency': 'USD', + 'conversionRate': 1200.88200327, + 'conversionDate': 1489013762, + 'noActiveNotices': true, + 'frequentRpcList': [], + 'network': '3', + 'accounts': { + '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { + 'code': '0x', + 'balance': '0x47c9d71831c76efe', + 'nonce': '0x1b', + 'address': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + }, + '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { + 'code': '0x', + 'balance': '0x37452b1315889f80', + 'nonce': '0xa', + 'address': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + }, + '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { + 'code': '0x', + 'balance': '0x30c9d71831c76efe', + 'nonce': '0x1c', + 'address': '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', + }, + '0xd85a4b6a394794842887b8284293d69163007bbb': { + 'code': '0x', + 'balance': '0x0', + 'nonce': '0x0', + 'address': '0xd85a4b6a394794842887b8284293d69163007bbb', + }, + }, + 'addressBook': [ + { + 'address': '0x06195827297c7a80a443b6894d3bdb8824b43896', + 'name': 'Address Book Account 1', + }, + ], + 'tokens': [ + { + 'address': '0x1a195821297c7a80a433b6894d3bdb8824b43896', + 'decimals': 18, + 'symbol': 'ABC', + }, + { + 'address': '0x8d6b81208414189a58339873ab429b6c47ab92d3', + 'decimals': 4, + 'symbol': 'DEF', + }, + { + 'address': '0xa42084c8d1d9a2198631988579bb36b48433a72b', + 'decimals': 18, + 'symbol': 'GHI', + }, + ], + 'tokenExchangeRates': { + 'def_eth': { + rate: 2.0, + }, + 'ghi_eth': { + rate: 31.01, + }, + }, + 'transactions': {}, + 'selectedAddressTxList': [ + { + 'id': 'mockTokenTx1', + 'txParams': { + 'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3', + }, + 'time': 1700000000000, + }, + { + 'id': 'mockTokenTx2', + 'txParams': { + 'to': '0xafaketokenaddress', + }, + 'time': 1600000000000, + }, + { + 'id': 'mockTokenTx3', + 'txParams': { + 'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3', + }, + 'time': 1500000000000, + }, + { + 'id': 'mockEthTx1', + 'txParams': { + 'to': '0xd85a4b6a394794842887b8284293d69163007bbb', + }, + 'time': 1400000000000, + }, + ], + 'selectedTokenAddress': '0x8d6b81208414189a58339873ab429b6c47ab92d3', + 'unapprovedMsgs': { + '0xabc': { id: 'unapprovedMessage1', 'time': 1650000000000 }, + '0xdef': { id: 'unapprovedMessage2', 'time': 1550000000000 }, + '0xghi': { id: 'unapprovedMessage3', 'time': 1450000000000 }, + }, + 'unapprovedMsgCount': 0, + 'unapprovedPersonalMsgs': {}, + 'unapprovedPersonalMsgCount': 0, + 'keyringTypes': [ + 'Simple Key Pair', + 'HD Key Tree', + ], + 'keyrings': [ + { + 'type': 'HD Key Tree', + 'accounts': [ + 'fdea65c8e26263f6d9a1b5de9555d2931a33b825', + 'c5b8dbac4c1d3f152cdeb400e2313f309c410acb', + '2f8d4a878cfa04a6e60d46362f5644deab66572d', + ], + }, + { + 'type': 'Simple Key Pair', + 'accounts': [ + '0xd85a4b6a394794842887b8284293d69163007bbb', + ], + }, + ], + 'selectedAddress': '0xd85a4b6a394794842887b8284293d69163007bbb', + 'provider': { + 'type': 'testnet', + }, + 'shapeShiftTxList': [ + { id: 'shapeShiftTx1', 'time': 1675000000000 }, + { id: 'shapeShiftTx2', 'time': 1575000000000 }, + { id: 'shapeShiftTx3', 'time': 1475000000000 }, + ], + 'lostAccounts': [], + 'send': { + 'gasLimit': '0xFFFF', + 'gasPrice': '0xaa', + 'gasTotal': '0xb451dc41b578', + 'tokenBalance': 3434, + 'from': { + 'address': '0xabcdefg', + 'balance': '0x5f4e3d2c1', + }, + 'to': '0x987fedabc', + 'amount': '0x080', + 'memo': '', + 'errors': { + 'someError': null, + }, + 'maxModeOn': false, + 'editingTransactionId': 97531, + 'forceGasMin': true, + }, + 'unapprovedTxs': { + '4768706228115573': { + 'id': 4768706228115573, + 'time': 1487363153561, + 'status': 'unapproved', + 'gasMultiplier': 1, + 'metamaskNetworkId': '3', + 'txParams': { + 'from': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + 'to': '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761', + 'value': '0xde0b6b3a7640000', + 'metamaskId': 4768706228115573, + 'metamaskNetworkId': '3', + 'gas': '0x5209', + }, + 'gasLimitSpecified': false, + 'estimatedGas': '0x5209', + 'txFee': '17e0186e60800', + 'txValue': 'de0b6b3a7640000', + 'maxCost': 'de234b52e4a0800', + 'gasPrice': '4a817c800', + }, + }, + 'currentLocale': 'en', + recentBlocks: ['mockBlock1', 'mockBlock2', 'mockBlock3'], + }, + 'appState': { + 'menuOpen': false, + 'currentView': { + 'name': 'accountDetail', + 'detailView': null, + 'context': '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + }, + 'accountDetail': { + 'subview': 'transactions', + }, + 'modal': { + 'modalState': {}, + 'previousModalState': {}, + }, + 'transForward': true, + 'isLoading': false, + 'warning': null, + 'scrollToBottom': false, + 'forgottenPassword': null, + }, + 'identities': {}, + 'send': { + 'fromDropdownOpen': false, + 'toDropdownOpen': false, + 'errors': { 'someError': null }, + }, +} diff --git a/ui/app/components/send/tests/send-selectors.test.js b/ui/app/components/send/tests/send-selectors.test.js new file mode 100644 index 000000000..218da656b --- /dev/null +++ b/ui/app/components/send/tests/send-selectors.test.js @@ -0,0 +1,685 @@ +import assert from 'assert' +import sinon from 'sinon' +import selectors from '../send.selectors.js' +const { + accountsWithSendEtherInfoSelector, + // autoAddToBetaUI, + getAddressBook, + getBlockGasLimit, + getAmountConversionRate, + getConversionRate, + getCurrentAccountWithSendEtherInfo, + getCurrentCurrency, + getCurrentNetwork, + getCurrentViewContext, + getForceGasMin, + getGasLimit, + getGasPrice, + getGasTotal, + getPrimaryCurrency, + getRecentBlocks, + getSelectedAccount, + getSelectedAddress, + getSelectedIdentity, + getSelectedToken, + getSelectedTokenContract, + getSelectedTokenExchangeRate, + getSelectedTokenToFiatRate, + getSendAmount, + getSendEditingTransactionId, + getSendErrors, + getSendFrom, + getSendFromBalance, + getSendFromObject, + getSendMaxModeState, + getSendTo, + getSendToAccounts, + getTokenBalance, + getTokenExchangeRate, + getUnapprovedTxs, + transactionsSelector, +} = selectors +import mockState from './send-selectors-test-data' + +describe('send selectors', () => { + const tempGlobalEth = Object.assign({}, global.eth) + beforeEach(() => { + global.eth = { + contract: sinon.stub().returns({ + at: address => 'mockAt:' + address, + }), + } + }) + + afterEach(() => { + global.eth = tempGlobalEth + }) + + describe('accountsWithSendEtherInfoSelector()', () => { + it('should return an array of account objects with name info from identities', () => { + assert.deepEqual( + accountsWithSendEtherInfoSelector(mockState), + [ + { + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + name: 'Send Account 1', + }, + { + code: '0x', + balance: '0x37452b1315889f80', + nonce: '0xa', + address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + name: 'Send Account 2', + }, + { + code: '0x', + balance: '0x30c9d71831c76efe', + nonce: '0x1c', + address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', + name: 'Send Account 3', + }, + { + code: '0x', + balance: '0x0', + nonce: '0x0', + address: '0xd85a4b6a394794842887b8284293d69163007bbb', + name: 'Send Account 4', + }, + ] + ) + }) + }) + + // describe('autoAddToBetaUI()', () => { + // it('should', () => { + // assert.deepEqual( + // autoAddToBetaUI(mockState), + + // ) + // }) + // }) + + describe('getAddressBook()', () => { + it('should return the address book', () => { + assert.deepEqual( + getAddressBook(mockState), + [ + { + address: '0x06195827297c7a80a443b6894d3bdb8824b43896', + name: 'Address Book Account 1', + }, + ], + ) + }) + }) + + describe('getAmountConversionRate()', () => { + it('should return the token conversion rate if a token is selected', () => { + assert.equal( + getAmountConversionRate(mockState), + 2401.76400654 + ) + }) + + it('should return the eth conversion rate if no token is selected', () => { + const editedMockState = { + metamask: Object.assign({}, mockState.metamask, { selectedTokenAddress: null }), + } + assert.equal( + getAmountConversionRate(editedMockState), + 1200.88200327 + ) + }) + }) + + describe('getBlockGasLimit', () => { + it('should return the current block gas limit', () => { + assert.deepEqual( + getBlockGasLimit(mockState), + '0x4c1878' + ) + }) + }) + + describe('getConversionRate()', () => { + it('should return the eth conversion rate', () => { + assert.deepEqual( + getConversionRate(mockState), + 1200.88200327 + ) + }) + }) + + describe('getCurrentAccountWithSendEtherInfo()', () => { + it('should return the currently selected account with identity info', () => { + assert.deepEqual( + getCurrentAccountWithSendEtherInfo(mockState), + { + code: '0x', + balance: '0x0', + nonce: '0x0', + address: '0xd85a4b6a394794842887b8284293d69163007bbb', + name: 'Send Account 4', + } + ) + }) + }) + + describe('getCurrentCurrency()', () => { + it('should return the currently selected currency', () => { + assert.equal( + getCurrentCurrency(mockState), + 'USD' + ) + }) + }) + + describe('getCurrentNetwork()', () => { + it('should return the id of the currently selected network', () => { + assert.equal( + getCurrentNetwork(mockState), + '3' + ) + }) + }) + + describe('getCurrentViewContext()', () => { + it('should return the context of the current view', () => { + assert.equal( + getCurrentViewContext(mockState), + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' + ) + }) + }) + + describe('getForceGasMin()', () => { + it('should get the send.forceGasMin property', () => { + assert.equal( + getForceGasMin(mockState), + true + ) + }) + }) + + describe('getGasLimit()', () => { + it('should return the send.gasLimit', () => { + assert.equal( + getGasLimit(mockState), + '0xFFFF' + ) + }) + }) + + describe('getGasPrice()', () => { + it('should return the send.gasPrice', () => { + assert.equal( + getGasPrice(mockState), + '0xaa' + ) + }) + }) + + describe('getGasTotal()', () => { + it('should return the send.gasTotal', () => { + assert.equal( + getGasTotal(mockState), + '0xb451dc41b578' + ) + }) + }) + + describe('getPrimaryCurrency()', () => { + it('should return the symbol of the selected token', () => { + assert.equal( + getPrimaryCurrency(mockState), + 'DEF' + ) + }) + }) + + describe('getRecentBlocks()', () => { + it('should return the recent blocks', () => { + assert.deepEqual( + getRecentBlocks(mockState), + ['mockBlock1', 'mockBlock2', 'mockBlock3'] + ) + }) + }) + + describe('getSelectedAccount()', () => { + it('should return the currently selected account', () => { + assert.deepEqual( + getSelectedAccount(mockState), + { + code: '0x', + balance: '0x0', + nonce: '0x0', + address: '0xd85a4b6a394794842887b8284293d69163007bbb', + } + ) + }) + }) + + describe('getSelectedAddress()', () => { + it('should', () => { + assert.equal( + getSelectedAddress(mockState), + '0xd85a4b6a394794842887b8284293d69163007bbb' + ) + }) + }) + + describe('getSelectedIdentity()', () => { + it('should return the identity object of the currently selected address', () => { + assert.deepEqual( + getSelectedIdentity(mockState), + { + address: '0xd85a4b6a394794842887b8284293d69163007bbb', + name: 'Send Account 4', + } + ) + }) + }) + + describe('getSelectedToken()', () => { + it('should return the currently selected token if selected', () => { + assert.deepEqual( + getSelectedToken(mockState), + { + address: '0x8d6b81208414189a58339873ab429b6c47ab92d3', + decimals: 4, + symbol: 'DEF', + } + ) + }) + + it('should return the send token if none is currently selected, but a send token exists', () => { + const mockSendToken = { + address: '0x123456708414189a58339873ab429b6c47ab92d3', + decimals: 4, + symbol: 'JKL', + } + const editedMockState = { + metamask: Object.assign({}, mockState.metamask, { + selectedTokenAddress: null, + send: { + token: mockSendToken, + }, + }), + } + assert.deepEqual( + getSelectedToken(editedMockState), + Object.assign({}, mockSendToken) + ) + }) + }) + + describe('getSelectedTokenContract()', () => { + it('should return the contract at the selected token address', () => { + assert.equal( + getSelectedTokenContract(mockState), + 'mockAt:0x8d6b81208414189a58339873ab429b6c47ab92d3' + ) + }) + + it('should return null if no token is selected', () => { + const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false }) + assert.equal( + getSelectedTokenContract(Object.assign({}, mockState, { metamask: modifiedMetamaskState })), + null + ) + }) + }) + + describe('getSelectedTokenExchangeRate()', () => { + it('should return the exchange rate for the selected token', () => { + assert.equal( + getSelectedTokenExchangeRate(mockState), + 2.0 + ) + }) + }) + + describe('getSelectedTokenToFiatRate()', () => { + it('should return rate for converting the selected token to fiat', () => { + assert.equal( + getSelectedTokenToFiatRate(mockState), + 2401.76400654 + ) + }) + }) + + describe('getSendAmount()', () => { + it('should return the send.amount', () => { + assert.equal( + getSendAmount(mockState), + '0x080' + ) + }) + }) + + describe('getSendEditingTransactionId()', () => { + it('should return the send.editingTransactionId', () => { + assert.equal( + getSendEditingTransactionId(mockState), + 97531 + ) + }) + }) + + describe('getSendErrors()', () => { + it('should return the send.errors', () => { + assert.deepEqual( + getSendErrors(mockState), + { someError: null } + ) + }) + }) + + describe('getSendFrom()', () => { + it('should return the send.from', () => { + assert.deepEqual( + getSendFrom(mockState), + { + address: '0xabcdefg', + balance: '0x5f4e3d2c1', + } + ) + }) + }) + + describe('getSendFromBalance()', () => { + it('should get the send.from balance if it exists', () => { + assert.equal( + getSendFromBalance(mockState), + '0x5f4e3d2c1' + ) + }) + + it('should get the selected account balance if the send.from does not exist', () => { + const editedMockState = { + metamask: Object.assign({}, mockState.metamask, { + send: { + from: null, + }, + }), + } + assert.equal( + getSendFromBalance(editedMockState), + '0x0' + ) + }) + }) + + describe('getSendFromObject()', () => { + it('should return send.from if it exists', () => { + assert.deepEqual( + getSendFromObject(mockState), + { + address: '0xabcdefg', + balance: '0x5f4e3d2c1', + } + ) + }) + + it('should return the current account with send ether info if send.from does not exist', () => { + const editedMockState = { + metamask: Object.assign({}, mockState.metamask, { + send: { + from: null, + }, + }), + } + assert.deepEqual( + getSendFromObject(editedMockState), + { + code: '0x', + balance: '0x0', + nonce: '0x0', + address: '0xd85a4b6a394794842887b8284293d69163007bbb', + name: 'Send Account 4', + } + ) + }) + }) + + describe('getSendMaxModeState()', () => { + it('should return send.maxModeOn', () => { + assert.equal( + getSendMaxModeState(mockState), + false + ) + }) + }) + + describe('getSendTo()', () => { + it('should return send.to', () => { + assert.equal( + getSendTo(mockState), + '0x987fedabc' + ) + }) + }) + + describe('getSendToAccounts()', () => { + it('should return an array including all the users accounts and the address book', () => { + assert.deepEqual( + getSendToAccounts(mockState), + [ + { + code: '0x', + balance: '0x47c9d71831c76efe', + nonce: '0x1b', + address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', + name: 'Send Account 1', + }, + { + code: '0x', + balance: '0x37452b1315889f80', + nonce: '0xa', + address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + name: 'Send Account 2', + }, + { + code: '0x', + balance: '0x30c9d71831c76efe', + nonce: '0x1c', + address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', + name: 'Send Account 3', + }, + { + code: '0x', + balance: '0x0', + nonce: '0x0', + address: '0xd85a4b6a394794842887b8284293d69163007bbb', + name: 'Send Account 4', + }, + { + address: '0x06195827297c7a80a443b6894d3bdb8824b43896', + name: 'Address Book Account 1', + }, + ] + ) + }) + }) + + describe('getTokenBalance()', () => { + it('should', () => { + assert.equal( + getTokenBalance(mockState), + 3434 + ) + }) + }) + + describe('getTokenExchangeRate()', () => { + it('should return the passed tokens exchange rates', () => { + assert.equal( + getTokenExchangeRate(mockState, 'GHI'), + 31.01 + ) + }) + }) + + describe('getUnapprovedTxs()', () => { + it('should return the unapproved txs', () => { + assert.deepEqual( + getUnapprovedTxs(mockState), + { + 4768706228115573: { + id: 4768706228115573, + time: 1487363153561, + status: 'unapproved', + gasMultiplier: 1, + metamaskNetworkId: '3', + txParams: { + from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', + to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761', + value: '0xde0b6b3a7640000', + metamaskId: 4768706228115573, + metamaskNetworkId: '3', + gas: '0x5209', + }, + gasLimitSpecified: false, + estimatedGas: '0x5209', + txFee: '17e0186e60800', + txValue: 'de0b6b3a7640000', + maxCost: 'de234b52e4a0800', + gasPrice: '4a817c800', + }, + } + ) + }) + }) + + describe('transactionsSelector()', () => { + it('should return the selected addresses selected token transactions', () => { + assert.deepEqual( + transactionsSelector(mockState), + [ + { + id: 'mockTokenTx1', + txParams: { + to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', + }, + time: 1700000000000, + }, + { + id: 'mockTokenTx3', + txParams: { + to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', + }, + time: 1500000000000, + }, + ] + ) + }) + + it('should return all transactions if no token is selected', () => { + const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false }) + const modifiedState = Object.assign({}, mockState, { metamask: modifiedMetamaskState }) + assert.deepEqual( + transactionsSelector(modifiedState), + [ + { + id: 'mockTokenTx1', + time: 1700000000000, + txParams: { + to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', + }, + }, + { + id: 'unapprovedMessage1', + time: 1650000000000, + }, + { + id: 'mockTokenTx2', + time: 1600000000000, + txParams: { + to: '0xafaketokenaddress', + }, + }, + { + id: 'unapprovedMessage2', + time: 1550000000000, + }, + { + id: 'mockTokenTx3', + time: 1500000000000, + txParams: { + to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', + }, + }, + { + id: 'unapprovedMessage3', + time: 1450000000000, + }, + { + id: 'mockEthTx1', + time: 1400000000000, + txParams: { + to: '0xd85a4b6a394794842887b8284293d69163007bbb', + }, + }, + ] + ) + }) + + it('should return shapeshift transactions if current network is 1', () => { + const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false, network: '1' }) + const modifiedState = Object.assign({}, mockState, { metamask: modifiedMetamaskState }) + assert.deepEqual( + transactionsSelector(modifiedState), + [ + { + id: 'mockTokenTx1', + time: 1700000000000, + txParams: { + to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', + }, + }, + { id: 'shapeShiftTx1', 'time': 1675000000000 }, + { + id: 'unapprovedMessage1', + time: 1650000000000, + }, + { + id: 'mockTokenTx2', + time: 1600000000000, + txParams: { + to: '0xafaketokenaddress', + }, + }, + { id: 'shapeShiftTx2', 'time': 1575000000000 }, + { + id: 'unapprovedMessage2', + time: 1550000000000, + }, + { + id: 'mockTokenTx3', + time: 1500000000000, + txParams: { + to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', + }, + }, + { id: 'shapeShiftTx3', 'time': 1475000000000 }, + { + id: 'unapprovedMessage3', + time: 1450000000000, + }, + { + id: 'mockEthTx1', + time: 1400000000000, + txParams: { + to: '0xd85a4b6a394794842887b8284293d69163007bbb', + }, + }, + ] + ) + }) + }) + +}) diff --git a/ui/app/components/send/tests/send-utils.test.js b/ui/app/components/send/tests/send-utils.test.js new file mode 100644 index 000000000..b8579e0e4 --- /dev/null +++ b/ui/app/components/send/tests/send-utils.test.js @@ -0,0 +1,486 @@ +import assert from 'assert' +import sinon from 'sinon' +import proxyquire from 'proxyquire' +import { + BASE_TOKEN_GAS_COST, + ONE_GWEI_IN_WEI_HEX, + SIMPLE_GAS_COST, +} from '../send.constants' +const { + addCurrencies, + subtractCurrencies, +} = require('../../../conversion-util') + +const { + INSUFFICIENT_FUNDS_ERROR, + INSUFFICIENT_TOKENS_ERROR, +} = require('../send.constants') + +const stubs = { + addCurrencies: sinon.stub().callsFake((a, b, obj) => { + if (String(a).match(/^0x.+/)) a = Number(String(a).slice(2)) + if (String(b).match(/^0x.+/)) b = Number(String(b).slice(2)) + return a + b + }), + conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)), + conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value), + multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`), + calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d), + rawEncode: sinon.stub().returns([16, 1100]), + conversionGreaterThan: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value), + conversionLessThan: sinon.stub().callsFake((obj1, obj2) => obj1.value < obj2.value), +} + +const sendUtils = proxyquire('../send.utils.js', { + '../../conversion-util': { + addCurrencies: stubs.addCurrencies, + conversionUtil: stubs.conversionUtil, + conversionGTE: stubs.conversionGTE, + multiplyCurrencies: stubs.multiplyCurrencies, + conversionGreaterThan: stubs.conversionGreaterThan, + conversionLessThan: stubs.conversionLessThan, + }, + '../../token-util': { calcTokenAmount: stubs.calcTokenAmount }, + 'ethereumjs-abi': { + rawEncode: stubs.rawEncode, + }, +}) + +const { + calcGasTotal, + estimateGas, + doesAmountErrorRequireUpdate, + estimateGasPriceFromRecentBlocks, + generateTokenTransferData, + getAmountErrorObject, + getGasFeeErrorObject, + getToAddressForGasUpdate, + calcTokenBalance, + isBalanceSufficient, + isTokenBalanceSufficient, +} = sendUtils + +describe('send utils', () => { + + describe('calcGasTotal()', () => { + it('should call multiplyCurrencies with the correct params and return the multiplyCurrencies return', () => { + const result = calcGasTotal(12, 15) + assert.equal(result, '12x15') + const call_ = stubs.multiplyCurrencies.getCall(0).args + assert.deepEqual( + call_, + [12, 15, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + } ] + ) + }) + }) + + describe('doesAmountErrorRequireUpdate()', () => { + const config = { + 'should return true if balances are different': { + balance: 0, + prevBalance: 1, + expectedResult: true, + }, + 'should return true if gasTotals are different': { + gasTotal: 0, + prevGasTotal: 1, + expectedResult: true, + }, + 'should return true if token balances are different': { + tokenBalance: 0, + prevTokenBalance: 1, + selectedToken: 'someToken', + expectedResult: true, + }, + 'should return false if they are all the same': { + balance: 1, + prevBalance: 1, + gasTotal: 1, + prevGasTotal: 1, + tokenBalance: 1, + prevTokenBalance: 1, + selectedToken: 'someToken', + expectedResult: false, + }, + } + Object.entries(config).map(([description, obj]) => { + it(description, () => { + assert.equal(doesAmountErrorRequireUpdate(obj), obj.expectedResult) + }) + }) + + }) + + describe('generateTokenTransferData()', () => { + it('should return undefined if not passed a selected token', () => { + assert.equal(generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: false}), undefined) + }) + + it('should call abi.rawEncode with the correct params', () => { + stubs.rawEncode.resetHistory() + generateTokenTransferData({ toAddress: 'mockAddress', amount: 'ab', selectedToken: true}) + assert.deepEqual( + stubs.rawEncode.getCall(0).args, + [['address', 'uint256'], ['mockAddress', '0xab']] + ) + }) + + it('should return encoded token transfer data', () => { + assert.equal( + generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: true}), + '0xa9059cbb104c' + ) + }) + }) + + describe('getAmountErrorObject()', () => { + const config = { + 'should return insufficientFunds error if isBalanceSufficient returns false': { + amount: 15, + amountConversionRate: 2, + balance: 1, + conversionRate: 3, + gasTotal: 17, + primaryCurrency: 'ABC', + expectedResult: { amount: INSUFFICIENT_FUNDS_ERROR }, + }, + 'should not return insufficientFunds error if selectedToken is truthy': { + amount: '0x0', + amountConversionRate: 2, + balance: 1, + conversionRate: 3, + gasTotal: 17, + primaryCurrency: 'ABC', + selectedToken: { symbole: 'DEF', decimals: 0 }, + decimals: 0, + tokenBalance: 'sometokenbalance', + expectedResult: { amount: null }, + }, + 'should return insufficientTokens error if token is selected and isTokenBalanceSufficient returns false': { + amount: '0x10', + amountConversionRate: 2, + balance: 100, + conversionRate: 3, + decimals: 10, + gasTotal: 17, + primaryCurrency: 'ABC', + selectedToken: 'someToken', + tokenBalance: 123, + expectedResult: { amount: INSUFFICIENT_TOKENS_ERROR }, + }, + } + Object.entries(config).map(([description, obj]) => { + it(description, () => { + assert.deepEqual(getAmountErrorObject(obj), obj.expectedResult) + }) + }) + }) + + describe('getGasFeeErrorObject()', () => { + const config = { + 'should return insufficientFunds error if isBalanceSufficient returns false': { + amountConversionRate: 2, + balance: 16, + conversionRate: 3, + gasTotal: 17, + primaryCurrency: 'ABC', + expectedResult: { gasFee: INSUFFICIENT_FUNDS_ERROR }, + }, + 'should return null error if isBalanceSufficient returns true': { + amountConversionRate: 2, + balance: 16, + conversionRate: 3, + gasTotal: 15, + primaryCurrency: 'ABC', + expectedResult: { gasFee: null }, + }, + } + Object.entries(config).map(([description, obj]) => { + it(description, () => { + assert.deepEqual(getGasFeeErrorObject(obj), obj.expectedResult) + }) + }) + }) + + describe('calcTokenBalance()', () => { + it('should return the calculated token blance', () => { + assert.equal(calcTokenBalance({ + selectedToken: { + decimals: 11, + }, + usersToken: { + balance: 20, + }, + }), 'calc:2011') + }) + }) + + describe('isBalanceSufficient()', () => { + it('should correctly call addCurrencies and return the result of calling conversionGTE', () => { + stubs.conversionGTE.resetHistory() + const result = isBalanceSufficient({ + amount: 15, + amountConversionRate: 2, + balance: 100, + conversionRate: 3, + gasTotal: 17, + primaryCurrency: 'ABC', + }) + assert.deepEqual( + stubs.addCurrencies.getCall(0).args, + [ + 15, 17, { + aBase: 16, + bBase: 16, + toNumericBase: 'hex', + }, + ] + ) + assert.deepEqual( + stubs.conversionGTE.getCall(0).args, + [ + { + value: 100, + fromNumericBase: 'hex', + fromCurrency: 'ABC', + conversionRate: 3, + }, + { + value: 32, + fromNumericBase: 'hex', + conversionRate: 2, + fromCurrency: 'ABC', + }, + ] + ) + + assert.equal(result, true) + }) + }) + + describe('isTokenBalanceSufficient()', () => { + it('should correctly call conversionUtil and return the result of calling conversionGTE', () => { + stubs.conversionGTE.resetHistory() + stubs.conversionUtil.resetHistory() + const result = isTokenBalanceSufficient({ + amount: '0x10', + tokenBalance: 123, + decimals: 10, + }) + assert.deepEqual( + stubs.conversionUtil.getCall(0).args, + [ + '0x10', { + fromNumericBase: 'hex', + }, + ] + ) + assert.deepEqual( + stubs.conversionGTE.getCall(0).args, + [ + { + value: 123, + fromNumericBase: 'dec', + }, + { + value: 'calc:1610', + fromNumericBase: 'dec', + }, + ] + ) + + assert.equal(result, false) + }) + }) + + describe('estimateGas', () => { + const baseMockParams = { + blockGasLimit: '0x64', + selectedAddress: 'mockAddress', + to: '0xisContract', + estimateGasMethod: sinon.stub().callsFake( + (data, cb) => cb( + data.to.match(/willFailBecauseOf:/) ? { message: data.to.match(/:(.+)$/)[1] } : null, + { toString: (n) => `0xabc${n}` } + ) + ), + } + const baseExpectedCall = { + from: 'mockAddress', + gas: '0x64x0.95', + to: '0xisContract', + } + + beforeEach(() => { + global.eth = { + getCode: sinon.stub().callsFake( + (address) => Promise.resolve(address.match(/isContract/) ? 'not-0x' : '0x') + ), + } + }) + + afterEach(() => { + baseMockParams.estimateGasMethod.resetHistory() + global.eth.getCode.resetHistory() + }) + + it('should call ethQuery.estimateGas with the expected params', async () => { + const result = await sendUtils.estimateGas(baseMockParams) + assert.equal(baseMockParams.estimateGasMethod.callCount, 1) + assert.deepEqual( + baseMockParams.estimateGasMethod.getCall(0).args[0], + Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall) + ) + assert.equal(result, '0xabc16') + }) + + it('should call ethQuery.estimateGas with the expected params when initialGasLimitHex is lower than the upperGasLimit', async () => { + const result = await estimateGas(Object.assign({}, baseMockParams, { blockGasLimit: '0xbcd' })) + assert.equal(baseMockParams.estimateGasMethod.callCount, 1) + assert.deepEqual( + baseMockParams.estimateGasMethod.getCall(0).args[0], + Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall, { gas: '0xbcdx0.95' }) + ) + assert.equal(result, '0xabc16x1.5') + }) + + it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => { + const result = await estimateGas(Object.assign({ data: 'mockData', selectedToken: { address: 'mockAddress' } }, baseMockParams)) + assert.equal(baseMockParams.estimateGasMethod.callCount, 1) + assert.deepEqual( + baseMockParams.estimateGasMethod.getCall(0).args[0], + Object.assign({}, baseExpectedCall, { + gasPrice: undefined, + value: '0x0', + data: '0xa9059cbb104c', + to: 'mockAddress', + }) + ) + assert.equal(result, '0xabc16') + }) + + it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => { + assert.equal(baseMockParams.estimateGasMethod.callCount, 0) + const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123' })) + assert.equal(result, SIMPLE_GAS_COST) + }) + + it(`should return ${SIMPLE_GAS_COST} if not passed a selectedToken or truthy to address`, async () => { + assert.equal(baseMockParams.estimateGasMethod.callCount, 0) + const result = await estimateGas(Object.assign({}, baseMockParams, { to: null })) + assert.equal(result, SIMPLE_GAS_COST) + }) + + it(`should not return ${SIMPLE_GAS_COST} if passed a selectedToken`, async () => { + assert.equal(baseMockParams.estimateGasMethod.callCount, 0) + const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } })) + assert.notEqual(result, SIMPLE_GAS_COST) + }) + + it(`should return ${BASE_TOKEN_GAS_COST} if passed a selectedToken but no to address`, async () => { + const result = await estimateGas(Object.assign({}, baseMockParams, { to: null, selectedToken: { address: '' } })) + assert.equal(result, BASE_TOKEN_GAS_COST) + }) + + it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => { + const result = await estimateGas(Object.assign({}, baseMockParams, { + to: 'isContract willFailBecauseOf:Transaction execution error.', + })) + assert.equal(result, '0x64x0.95') + }) + + it(`should return the adjusted blockGasLimit if it fails with a 'gas required exceeds allowance or always failing transaction.'`, async () => { + const result = await estimateGas(Object.assign({}, baseMockParams, { + to: 'isContract willFailBecauseOf:gas required exceeds allowance or always failing transaction.', + })) + assert.equal(result, '0x64x0.95') + }) + + it(`should reject other errors`, async () => { + try { + await estimateGas(Object.assign({}, baseMockParams, { + to: 'isContract willFailBecauseOf:some other error', + })) + } catch (err) { + assert.deepEqual(err, { message: 'some other error' }) + } + }) + }) + + describe('estimateGasPriceFromRecentBlocks', () => { + const ONE_GWEI_IN_WEI_HEX_PLUS_ONE = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', { + aBase: 16, + bBase: 16, + toNumericBase: 'hex', + }) + const ONE_GWEI_IN_WEI_HEX_PLUS_TWO = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x2', { + aBase: 16, + bBase: 16, + toNumericBase: 'hex', + }) + const ONE_GWEI_IN_WEI_HEX_MINUS_ONE = subtractCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', { + aBase: 16, + bBase: 16, + toNumericBase: 'hex', + }) + + it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is falsy`, () => { + assert.equal(estimateGasPriceFromRecentBlocks(), ONE_GWEI_IN_WEI_HEX) + }) + + it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is empty`, () => { + assert.equal(estimateGasPriceFromRecentBlocks([]), ONE_GWEI_IN_WEI_HEX) + }) + + it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has no gas prices`, () => { + const mockRecentBlocks = [ + { gasPrices: null }, + { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] }, + { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] }, + ] + assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX) + }) + + it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has empty gas prices`, () => { + const mockRecentBlocks = [ + { gasPrices: [] }, + { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] }, + { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] }, + ] + assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX) + }) + + it(`should return the middle value of all blocks lowest prices`, () => { + const mockRecentBlocks = [ + { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_TWO ] }, + { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] }, + { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] }, + ] + assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX_PLUS_ONE) + }) + + it(`should work if a block has multiple gas prices`, () => { + const mockRecentBlocks = [ + { gasPrices: [ '0x1', '0x2', '0x3', '0x4', '0x5' ] }, + { gasPrices: [ '0x101', '0x100', '0x103', '0x104', '0x102' ] }, + { gasPrices: [ '0x150', '0x50', '0x100', '0x200', '0x5' ] }, + ] + assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5') + }) + }) + + describe('getToAddressForGasUpdate()', () => { + it('should return empty string if all params are undefined or null', () => { + assert.equal(getToAddressForGasUpdate(undefined, null), '') + }) + + it('should return the first string that is not defined or null in lower case', () => { + assert.equal(getToAddressForGasUpdate('A', null), 'a') + assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b') + }) + }) +}) diff --git a/ui/app/components/send/to-autocomplete/index.js b/ui/app/components/send/to-autocomplete/index.js new file mode 100644 index 000000000..afa2eb5a4 --- /dev/null +++ b/ui/app/components/send/to-autocomplete/index.js @@ -0,0 +1 @@ +export { default } from './to-autocomplete.js' \ No newline at end of file diff --git a/ui/app/components/send/to-autocomplete/to-autocomplete.js b/ui/app/components/send/to-autocomplete/to-autocomplete.js new file mode 100644 index 000000000..80cfa7a85 --- /dev/null +++ b/ui/app/components/send/to-autocomplete/to-autocomplete.js @@ -0,0 +1,120 @@ +const Component = require('react').Component +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const AccountListItem = require('../account-list-item/account-list-item.component').default +const connect = require('react-redux').connect + +ToAutoComplete.contextTypes = { + t: PropTypes.func, +} + +module.exports = connect()(ToAutoComplete) + + +inherits(ToAutoComplete, Component) +function ToAutoComplete () { + Component.call(this) + + this.state = { accountsToRender: [] } +} + +ToAutoComplete.prototype.getListItemIcon = function (listItemAddress, toAddress) { + const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } }) + + return toAddress && listItemAddress === toAddress + ? listItemIcon + : null +} + +ToAutoComplete.prototype.renderDropdown = function () { + const { + closeDropdown, + onChange, + to, + } = this.props + const { accountsToRender } = this.state + + return accountsToRender.length && h('div', {}, [ + + h('div.send-v2__from-dropdown__close-area', { + onClick: closeDropdown, + }), + + h('div.send-v2__from-dropdown__list', {}, [ + + ...accountsToRender.map(account => h(AccountListItem, { + account, + className: 'account-list-item__dropdown', + handleClick: () => { + onChange(account.address) + closeDropdown() + }, + icon: this.getListItemIcon(account.address, to), + displayBalance: false, + displayAddress: true, + })), + + ]), + + ]) +} + +ToAutoComplete.prototype.handleInputEvent = function (event = {}, cb) { + const { + to, + accounts, + closeDropdown, + openDropdown, + } = this.props + + const matchingAccounts = accounts.filter(({ address }) => address.match(to || '')) + const matches = matchingAccounts.length + + if (!matches || matchingAccounts[0].address === to) { + this.setState({ accountsToRender: [] }) + event.target && event.target.select() + closeDropdown() + } else { + this.setState({ accountsToRender: matchingAccounts }) + openDropdown() + } + cb && cb(event.target.value) +} + +ToAutoComplete.prototype.componentDidUpdate = function (nextProps, nextState) { + if (this.props.to !== nextProps.to) { + this.handleInputEvent() + } +} + +ToAutoComplete.prototype.render = function () { + const { + to, + dropdownOpen, + onChange, + inError, + } = this.props + + return h('div.send-v2__to-autocomplete', {}, [ + + h('input.send-v2__to-autocomplete__input', { + placeholder: this.context.t('recipientAddress'), + className: inError ? `send-v2__error-border` : '', + value: to, + onChange: event => onChange(event.target.value), + onFocus: event => this.handleInputEvent(event), + style: { + borderColor: inError ? 'red' : null, + }, + }), + + !to && h(`i.fa.fa-caret-down.fa-lg.send-v2__to-autocomplete__down-caret`, { + style: { color: '#dedede' }, + onClick: () => this.handleInputEvent(), + }), + + dropdownOpen && this.renderDropdown(), + + ]) +} diff --git a/ui/app/components/send_/README.md b/ui/app/components/send_/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/account-list-item/account-list-item-README.md b/ui/app/components/send_/account-list-item/account-list-item-README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/account-list-item/account-list-item.component.js b/ui/app/components/send_/account-list-item/account-list-item.component.js deleted file mode 100644 index 322246f61..000000000 --- a/ui/app/components/send_/account-list-item/account-list-item.component.js +++ /dev/null @@ -1,73 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { checksumAddress } from '../../../util' -import Identicon from '../../identicon' -import CurrencyDisplay from '../../send/currency-display' - -export default class AccountListItem extends Component { - - static propTypes = { - account: PropTypes.object, - className: PropTypes.string, - conversionRate: PropTypes.number, - currentCurrency: PropTypes.string, - displayAddress: PropTypes.bool, - displayBalance: PropTypes.bool, - handleClick: PropTypes.func, - icon: PropTypes.node, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - render () { - const { - account, - className, - conversionRate, - currentCurrency, - displayAddress = false, - displayBalance = true, - handleClick, - icon = null, - } = this.props - - const { name, address, balance } = account || {} - - return (
handleClick({ name, address, balance })} - > - -
- - -
{ name || address }
- - {icon &&
{ icon }
} - -
- - {displayAddress && name &&
- { checksumAddress(address) } -
} - - {displayBalance && } - -
) - } -} diff --git a/ui/app/components/send_/account-list-item/account-list-item.container.js b/ui/app/components/send_/account-list-item/account-list-item.container.js deleted file mode 100644 index 4b4519288..000000000 --- a/ui/app/components/send_/account-list-item/account-list-item.container.js +++ /dev/null @@ -1,15 +0,0 @@ -import { connect } from 'react-redux' -import { - getConversionRate, - getCurrentCurrency, -} from '../send.selectors.js' -import AccountListItem from './account-list-item.component' - -export default connect(mapStateToProps)(AccountListItem) - -function mapStateToProps (state) { - return { - conversionRate: getConversionRate(state), - currentCurrency: getCurrentCurrency(state), - } -} diff --git a/ui/app/components/send_/account-list-item/account-list-item.scss b/ui/app/components/send_/account-list-item/account-list-item.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/account-list-item/index.js b/ui/app/components/send_/account-list-item/index.js deleted file mode 100644 index 907485cf7..000000000 --- a/ui/app/components/send_/account-list-item/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './account-list-item.container' diff --git a/ui/app/components/send_/account-list-item/tests/account-list-item-component.test.js b/ui/app/components/send_/account-list-item/tests/account-list-item-component.test.js deleted file mode 100644 index bb7f3776c..000000000 --- a/ui/app/components/send_/account-list-item/tests/account-list-item-component.test.js +++ /dev/null @@ -1,138 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -import proxyquire from 'proxyquire' -import Identicon from '../../../identicon' -import CurrencyDisplay from '../../../send/currency-display' - -const utilsMethodStubs = { - checksumAddress: sinon.stub().returns('mockCheckSumAddress'), -} - -const AccountListItem = proxyquire('../account-list-item.component.js', { - '../../../util': utilsMethodStubs, -}).default - - -const propsMethodSpies = { - handleClick: sinon.spy(), -} - -describe('AccountListItem Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow(} - />, { context: { t: str => str + '_t' } }) - }) - - afterEach(() => { - propsMethodSpies.handleClick.resetHistory() - }) - - describe('render', () => { - it('should render a div with the passed className', () => { - assert.equal(wrapper.find('.mockClassName').length, 1) - assert(wrapper.find('.mockClassName').is('div')) - assert(wrapper.find('.mockClassName').hasClass('account-list-item')) - }) - - it('should call handleClick with the expected props when the root div is clicked', () => { - const { onClick } = wrapper.find('.mockClassName').props() - assert.equal(propsMethodSpies.handleClick.callCount, 0) - onClick() - assert.equal(propsMethodSpies.handleClick.callCount, 1) - assert.deepEqual( - propsMethodSpies.handleClick.getCall(0).args, - [{ address: 'mockAddress', name: 'mockName', balance: 'mockBalance' }] - ) - }) - - it('should have a top row div', () => { - assert.equal(wrapper.find('.mockClassName > .account-list-item__top-row').length, 1) - assert(wrapper.find('.mockClassName > .account-list-item__top-row').is('div')) - }) - - it('should have an identicon, name and icon in the top row', () => { - const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') - assert.equal(topRow.find(Identicon).length, 1) - assert.equal(topRow.find('.account-list-item__account-name').length, 1) - assert.equal(topRow.find('.account-list-item__icon').length, 1) - }) - - it('should show the account name if it exists', () => { - const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') - assert.equal(topRow.find('.account-list-item__account-name').text(), 'mockName') - }) - - it('should show the account address if there is no name', () => { - wrapper.setProps({ account: { address: 'addressButNoName' } }) - const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') - assert.equal(topRow.find('.account-list-item__account-name').text(), 'addressButNoName') - }) - - it('should render the passed icon', () => { - const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') - assert(topRow.find('.account-list-item__icon').childAt(0).is('i')) - assert(topRow.find('.account-list-item__icon').childAt(0).hasClass('mockIcon')) - }) - - it('should not render an icon if none is passed', () => { - wrapper.setProps({ icon: null }) - const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') - assert.equal(topRow.find('.account-list-item__icon').length, 0) - }) - - it('should render the account address as a checksumAddress if displayAddress is true and name is provided', () => { - wrapper.setProps({ displayAddress: true }) - assert.equal(wrapper.find('.account-list-item__account-address').length, 1) - assert.equal(wrapper.find('.account-list-item__account-address').text(), 'mockCheckSumAddress') - assert.deepEqual( - utilsMethodStubs.checksumAddress.getCall(0).args, - ['mockAddress'] - ) - }) - - it('should not render the account address as a checksumAddress if displayAddress is false', () => { - wrapper.setProps({ displayAddress: false }) - assert.equal(wrapper.find('.account-list-item__account-address').length, 0) - }) - - it('should not render the account address as a checksumAddress if name is not provided', () => { - wrapper.setProps({ account: { address: 'someAddressButNoName' } }) - assert.equal(wrapper.find('.account-list-item__account-address').length, 0) - }) - - it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => { - wrapper.setProps({ displayBalance: true }) - assert.equal(wrapper.find(CurrencyDisplay).length, 1) - assert.deepEqual( - wrapper.find(CurrencyDisplay).props(), - { - className: 'account-list-item__account-balances', - conversionRate: 4, - convertedBalanceClassName: 'account-list-item__account-secondary-balance', - convertedCurrency: 'mockCurrentyCurrency', - primaryBalanceClassName: 'account-list-item__account-primary-balance', - primaryCurrency: 'ETH', - readOnly: true, - value: 'mockBalance', - } - ) - }) - - it('should not render a CurrencyDisplay if displayBalance is false', () => { - wrapper.setProps({ displayBalance: false }) - assert.equal(wrapper.find(CurrencyDisplay).length, 0) - }) - }) -}) diff --git a/ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js b/ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js deleted file mode 100644 index af0859117..000000000 --- a/ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' - -let mapStateToProps - -proxyquire('../account-list-item.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - return () => ({}) - }, - }, - '../send.selectors.js': { - getConversionRate: (s) => `mockConversionRate:${s}`, - getCurrentCurrency: (s) => `mockCurrentCurrency:${s}`, - }, -}) - -describe('account-list-item container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - conversionRate: 'mockConversionRate:mockState', - currentCurrency: 'mockCurrentCurrency:mockState', - }) - }) - - }) - -}) diff --git a/ui/app/components/send_/index.js b/ui/app/components/send_/index.js deleted file mode 100644 index b5114babc..000000000 --- a/ui/app/components/send_/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './send.container' diff --git a/ui/app/components/send_/send-content/index.js b/ui/app/components/send_/send-content/index.js deleted file mode 100644 index 891c17e6a..000000000 --- a/ui/app/components/send_/send-content/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './send-content.component' diff --git a/ui/app/components/send_/send-content/send-amount-row/README.md b/ui/app/components/send_/send-content/send-amount-row/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js deleted file mode 100644 index 4d0d36ab4..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js +++ /dev/null @@ -1,54 +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, - }) - } - - render () { - const { setMaxModeTo, maxModeOn } = this.props - - return ( -
{ - event.preventDefault() - setMaxModeTo(true) - this.setMaxAmount() - }} - > - {!maxModeOn ? this.context.t('max') : ''} -
- ) - } - -} diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js deleted file mode 100644 index 2d2ec42f7..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js +++ /dev/null @@ -1,40 +0,0 @@ -import { connect } from 'react-redux' -import { - getGasTotal, - getSelectedToken, - getSendFromBalance, - getTokenBalance, -} from '../../../send.selectors.js' -import { getMaxModeOn } from './amount-max-button.selectors.js' -import { calcMaxAmount } from './amount-max-button.utils.js' -import { - updateSendAmount, - setMaxModeTo, -} from '../../../../../actions' -import AmountMaxButton from './amount-max-button.component' -import { - updateSendErrors, -} from '../../../../../ducks/send.duck' - -export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton) - -function mapStateToProps (state) { - - return { - balance: getSendFromBalance(state), - gasTotal: getGasTotal(state), - maxModeOn: getMaxModeOn(state), - selectedToken: getSelectedToken(state), - tokenBalance: getTokenBalance(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - setAmountToMax: maxAmountDataObject => { - dispatch(updateSendErrors({ amount: null })) - dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject))) - }, - setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), - } -} diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js deleted file mode 100644 index 69fec1994..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js +++ /dev/null @@ -1,9 +0,0 @@ -const selectors = { - getMaxModeOn, -} - -module.exports = selectors - -function getMaxModeOn (state) { - return state.metamask.send.maxModeOn -} diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js deleted file mode 100644 index b490a7fd7..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js +++ /dev/null @@ -1,22 +0,0 @@ -const { - multiplyCurrencies, - subtractCurrencies, -} = require('../../../../../conversion-util') -const ethUtil = require('ethereumjs-util') - -function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) { - const { decimals } = selectedToken || {} - const multiplier = Math.pow(10, Number(decimals || 0)) - - return selectedToken - ? multiplyCurrencies(tokenBalance, multiplier, {toNumericBase: 'hex'}) - : subtractCurrencies( - ethUtil.addHexPrefix(balance), - ethUtil.addHexPrefix(gasTotal), - { toNumericBase: 'hex' } - ) -} - -module.exports = { - calcMaxAmount, -} diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js deleted file mode 100644 index ee8271494..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './amount-max-button.container' diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js deleted file mode 100644 index 86a05ff21..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -import AmountMaxButton from '../amount-max-button.component.js' - -const propsMethodSpies = { - setAmountToMax: sinon.spy(), - setMaxModeTo: sinon.spy(), -} - -const MOCK_EVENT = { preventDefault: () => {} } - -sinon.spy(AmountMaxButton.prototype, 'setMaxAmount') - -describe('AmountMaxButton Component', function () { - let wrapper - let instance - - beforeEach(() => { - wrapper = shallow(, { context: { t: str => str + '_t' } }) - instance = wrapper.instance() - }) - - afterEach(() => { - propsMethodSpies.setAmountToMax.resetHistory() - propsMethodSpies.setMaxModeTo.resetHistory() - AmountMaxButton.prototype.setMaxAmount.resetHistory() - }) - - describe('setMaxAmount', () => { - - it('should call setAmountToMax with the correct params', () => { - assert.equal(propsMethodSpies.setAmountToMax.callCount, 0) - instance.setMaxAmount() - assert.equal(propsMethodSpies.setAmountToMax.callCount, 1) - assert.deepEqual( - propsMethodSpies.setAmountToMax.getCall(0).args, - [{ - balance: 'mockBalance', - gasTotal: 'mockGasTotal', - selectedToken: { address: 'mockTokenAddress' }, - tokenBalance: 'mockTokenBalance', - }] - ) - }) - - }) - - describe('render', () => { - it('should render a div with a send-v2__amount-max class', () => { - assert.equal(wrapper.find('.send-v2__amount-max').length, 1) - assert(wrapper.find('.send-v2__amount-max').is('div')) - }) - - it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => { - const { - onClick, - } = wrapper.find('.send-v2__amount-max').props() - - assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 0) - assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0) - onClick(MOCK_EVENT) - assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 1) - assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1) - assert.deepEqual( - propsMethodSpies.setMaxModeTo.getCall(0).args, - [true] - ) - }) - - it('should not render text when maxModeOn is true', () => { - wrapper.setProps({ maxModeOn: true }) - assert.equal(wrapper.find('.send-v2__amount-max').text(), '') - }) - - 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/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js deleted file mode 100644 index 2cc00d6d6..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js +++ /dev/null @@ -1,91 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -let mapStateToProps -let mapDispatchToProps - -const actionSpies = { - setMaxModeTo: sinon.spy(), - updateSendAmount: sinon.spy(), -} -const duckActionSpies = { - updateSendErrors: sinon.spy(), -} - -proxyquire('../amount-max-button.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - }, - '../../../send.selectors.js': { - getGasTotal: (s) => `mockGasTotal:${s}`, - getSelectedToken: (s) => `mockSelectedToken:${s}`, - getSendFromBalance: (s) => `mockBalance:${s}`, - getTokenBalance: (s) => `mockTokenBalance:${s}`, - }, - './amount-max-button.selectors.js': { getMaxModeOn: (s) => `mockMaxModeOn:${s}` }, - './amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 }, - '../../../../../actions': actionSpies, - '../../../../../ducks/send.duck': duckActionSpies, -}) - -describe('amount-max-button container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - balance: 'mockBalance:mockState', - gasTotal: 'mockGasTotal:mockState', - maxModeOn: 'mockMaxModeOn:mockState', - selectedToken: 'mockSelectedToken:mockState', - tokenBalance: 'mockTokenBalance:mockState', - }) - }) - - }) - - describe('mapDispatchToProps()', () => { - let dispatchSpy - let mapDispatchToPropsObject - - beforeEach(() => { - dispatchSpy = sinon.spy() - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) - }) - - describe('setAmountToMax()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.setAmountToMax({ val: 11, foo: 'bar' }) - assert(dispatchSpy.calledTwice) - assert(duckActionSpies.updateSendErrors.calledOnce) - assert.deepEqual( - duckActionSpies.updateSendErrors.getCall(0).args[0], - { amount: null } - ) - assert(actionSpies.updateSendAmount.calledOnce) - assert.equal( - actionSpies.updateSendAmount.getCall(0).args[0], - 12 - ) - }) - }) - - describe('setMaxModeTo()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.setMaxModeTo('mockVal') - assert(dispatchSpy.calledOnce) - assert.equal( - actionSpies.setMaxModeTo.getCall(0).args[0], - 'mockVal' - ) - }) - }) - - }) - -}) diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js deleted file mode 100644 index 655fe1969..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import assert from 'assert' -import { - getMaxModeOn, -} from '../amount-max-button.selectors.js' - -describe('amount-max-button selectors', () => { - - describe('getMaxModeOn()', () => { - it('should', () => { - const state = { - metamask: { - send: { - maxModeOn: null, - }, - }, - } - - assert.equal(getMaxModeOn(state), null) - }) - }) - -}) diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js deleted file mode 100644 index 816df6a12..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import assert from 'assert' -import { - calcMaxAmount, -} from '../amount-max-button.utils.js' - -describe('amount-max-button utils', () => { - - describe('calcMaxAmount()', () => { - it('should calculate the correct amount when no selectedToken defined', () => { - assert.deepEqual(calcMaxAmount({ - balance: 'ffffff', - gasTotal: 'ff', - selectedToken: false, - }), 'ffff00') - }) - - it('should calculate the correct amount when a selectedToken is defined', () => { - assert.deepEqual(calcMaxAmount({ - selectedToken: { - decimals: 10, - }, - tokenBalance: 100, - }), 'e8d4a51000') - }) - }) - -}) diff --git a/ui/app/components/send_/send-content/send-amount-row/index.js b/ui/app/components/send_/send-content/send-amount-row/index.js deleted file mode 100644 index abc6852fe..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './send-amount-row.container' diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js deleted file mode 100644 index 6e30d29a4..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js +++ /dev/null @@ -1,123 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import SendRowWrapper from '../send-row-wrapper/' -import AmountMaxButton from './amount-max-button/' -import CurrencyDisplay from '../../../send/currency-display' - -export default class SendAmountRow extends Component { - - static propTypes = { - amount: PropTypes.string, - amountConversionRate: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), - balance: PropTypes.string, - conversionRate: PropTypes.number, - convertedCurrency: PropTypes.string, - gasTotal: PropTypes.string, - inError: PropTypes.bool, - primaryCurrency: PropTypes.string, - selectedToken: PropTypes.object, - setMaxModeTo: PropTypes.func, - tokenBalance: PropTypes.string, - updateGasFeeError: PropTypes.func, - updateSendAmount: PropTypes.func, - updateSendAmountError: PropTypes.func, - updateGas: PropTypes.func, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - validateAmount (amount) { - const { - amountConversionRate, - balance, - conversionRate, - gasTotal, - primaryCurrency, - selectedToken, - tokenBalance, - updateGasFeeError, - updateSendAmountError, - } = this.props - - updateSendAmountError({ - amount, - amountConversionRate, - balance, - conversionRate, - gasTotal, - primaryCurrency, - selectedToken, - tokenBalance, - }) - - if (selectedToken) { - updateGasFeeError({ - amount, - amountConversionRate, - balance, - conversionRate, - gasTotal, - primaryCurrency, - selectedToken, - tokenBalance, - }) - } - } - - updateAmount (amount) { - const { updateSendAmount, setMaxModeTo } = this.props - - setMaxModeTo(false) - updateSendAmount(amount) - } - - updateGas (amount) { - const { selectedToken, updateGas } = this.props - - if (selectedToken) { - updateGas({ amount }) - } - } - - render () { - const { - amount, - amountConversionRate, - convertedCurrency, - gasTotal, - inError, - primaryCurrency, - selectedToken, - } = this.props - - return ( - - {!inError && gasTotal && } - { - this.updateGas(newAmount) - this.updateAmount(newAmount) - }} - onChange={newAmount => this.validateAmount(newAmount)} - inError={inError} - primaryCurrency={primaryCurrency || 'ETH'} - selectedToken={selectedToken} - value={amount} - step="any" - /> - - ) - } - -} diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js deleted file mode 100644 index 3504d1b73..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js +++ /dev/null @@ -1,54 +0,0 @@ -import { connect } from 'react-redux' -import { - getAmountConversionRate, - getConversionRate, - getCurrentCurrency, - getGasTotal, - getPrimaryCurrency, - getSelectedToken, - getSendAmount, - getSendFromBalance, - getTokenBalance, -} from '../../send.selectors' -import { - sendAmountIsInError, -} from './send-amount-row.selectors' -import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils' -import { - setMaxModeTo, - updateSendAmount, -} from '../../../../actions' -import { - updateSendErrors, -} from '../../../../ducks/send.duck' -import SendAmountRow from './send-amount-row.component' - -export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow) - -function mapStateToProps (state) { - return { - amount: getSendAmount(state), - amountConversionRate: getAmountConversionRate(state), - balance: getSendFromBalance(state), - conversionRate: getConversionRate(state), - convertedCurrency: getCurrentCurrency(state), - gasTotal: getGasTotal(state), - inError: sendAmountIsInError(state), - primaryCurrency: getPrimaryCurrency(state), - selectedToken: getSelectedToken(state), - tokenBalance: getTokenBalance(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), - updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)), - updateGasFeeError: (amountDataObject) => { - dispatch(updateSendErrors(getGasFeeErrorObject(amountDataObject))) - }, - updateSendAmountError: (amountDataObject) => { - dispatch(updateSendErrors(getAmountErrorObject(amountDataObject))) - }, - } -} diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js deleted file mode 100644 index fb08c7ed7..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js +++ /dev/null @@ -1,9 +0,0 @@ -const selectors = { - sendAmountIsInError, -} - -module.exports = selectors - -function sendAmountIsInError (state) { - return Boolean(state.send.errors.amount) -} diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js deleted file mode 100644 index 95c000a34..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js +++ /dev/null @@ -1,202 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -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 CurrencyDisplay from '../../../../send/currency-display' - -const propsMethodSpies = { - setMaxModeTo: sinon.spy(), - updateSendAmount: sinon.spy(), - updateSendAmountError: sinon.spy(), - updateGas: sinon.spy(), - updateGasFeeError: sinon.spy(), -} - -sinon.spy(SendAmountRow.prototype, 'updateAmount') -sinon.spy(SendAmountRow.prototype, 'validateAmount') -sinon.spy(SendAmountRow.prototype, 'updateGas') - -describe('SendAmountRow Component', function () { - let wrapper - let instance - - beforeEach(() => { - wrapper = shallow(, { context: { t: str => str + '_t' } }) - instance = wrapper.instance() - }) - - afterEach(() => { - propsMethodSpies.setMaxModeTo.resetHistory() - propsMethodSpies.updateSendAmount.resetHistory() - propsMethodSpies.updateSendAmountError.resetHistory() - propsMethodSpies.updateGasFeeError.resetHistory() - SendAmountRow.prototype.validateAmount.resetHistory() - SendAmountRow.prototype.updateAmount.resetHistory() - }) - - describe('validateAmount', () => { - - it('should call updateSendAmountError with the correct params', () => { - assert.equal(propsMethodSpies.updateSendAmountError.callCount, 0) - instance.validateAmount('someAmount') - assert.equal(propsMethodSpies.updateSendAmountError.callCount, 1) - assert.deepEqual( - propsMethodSpies.updateSendAmountError.getCall(0).args, - [{ - amount: 'someAmount', - amountConversionRate: 'mockAmountConversionRate', - balance: 'mockBalance', - conversionRate: 7, - gasTotal: 'mockGasTotal', - primaryCurrency: 'mockPrimaryCurrency', - selectedToken: { address: 'mockTokenAddress' }, - tokenBalance: 'mockTokenBalance', - }] - ) - }) - - it('should call updateGasFeeError if selectedToken is truthy', () => { - assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) - instance.validateAmount('someAmount') - assert.equal(propsMethodSpies.updateGasFeeError.callCount, 1) - assert.deepEqual( - propsMethodSpies.updateGasFeeError.getCall(0).args, - [{ - amount: 'someAmount', - amountConversionRate: 'mockAmountConversionRate', - balance: 'mockBalance', - conversionRate: 7, - gasTotal: 'mockGasTotal', - primaryCurrency: 'mockPrimaryCurrency', - selectedToken: { address: 'mockTokenAddress' }, - tokenBalance: 'mockTokenBalance', - }] - ) - }) - - it('should call not updateGasFeeError if selectedToken is falsey', () => { - wrapper.setProps({ selectedToken: null }) - assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) - instance.validateAmount('someAmount') - assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) - }) - - }) - - describe('updateAmount', () => { - - it('should call setMaxModeTo', () => { - assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0) - instance.updateAmount('someAmount') - assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1) - assert.deepEqual( - propsMethodSpies.setMaxModeTo.getCall(0).args, - [false] - ) - }) - - it('should call updateSendAmount', () => { - assert.equal(propsMethodSpies.updateSendAmount.callCount, 0) - instance.updateAmount('someAmount') - assert.equal(propsMethodSpies.updateSendAmount.callCount, 1) - assert.deepEqual( - propsMethodSpies.updateSendAmount.getCall(0).args, - ['someAmount'] - ) - }) - - }) - - describe('render', () => { - it('should render a SendRowWrapper component', () => { - assert.equal(wrapper.find(SendRowWrapper).length, 1) - }) - - it('should pass the correct props to SendRowWrapper', () => { - const { - errorType, - label, - showError, - } = wrapper.find(SendRowWrapper).props() - - assert.equal(errorType, 'amount') - - assert.equal(label, 'amount_t:') - - assert.equal(showError, false) - }) - - it('should render an AmountMaxButton as the first child of the SendRowWrapper', () => { - assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton)) - }) - - it('should render a CurrencyDisplay as the second child of the SendRowWrapper', () => { - assert(wrapper.find(SendRowWrapper).childAt(1).is(CurrencyDisplay)) - }) - - it('should render the CurrencyDisplay with the correct props', () => { - const { - conversionRate, - convertedCurrency, - onBlur, - onChange, - inError, - primaryCurrency, - selectedToken, - value, - } = wrapper.find(SendRowWrapper).childAt(1).props() - assert.equal(conversionRate, 'mockAmountConversionRate') - assert.equal(convertedCurrency, 'mockConvertedCurrency') - assert.equal(inError, false) - assert.equal(primaryCurrency, 'mockPrimaryCurrency') - assert.deepEqual(selectedToken, { address: 'mockTokenAddress' }) - assert.equal(value, 'mockAmount') - assert.equal(SendAmountRow.prototype.updateGas.callCount, 0) - assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0) - onBlur('mockNewAmount') - assert.equal(SendAmountRow.prototype.updateGas.callCount, 1) - assert.deepEqual( - SendAmountRow.prototype.updateGas.getCall(0).args, - ['mockNewAmount'] - ) - assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1) - assert.deepEqual( - SendAmountRow.prototype.updateAmount.getCall(0).args, - ['mockNewAmount'] - ) - assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0) - onChange('mockNewAmount') - assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1) - assert.deepEqual( - SendAmountRow.prototype.validateAmount.getCall(0).args, - ['mockNewAmount'] - ) - }) - - it('should pass the default primaryCurrency to the CurrencyDisplay if primaryCurrency is falsy', () => { - wrapper.setProps({ primaryCurrency: null }) - const { primaryCurrency } = wrapper.find(SendRowWrapper).childAt(1).props() - assert.equal(primaryCurrency, 'ETH') - }) - }) -}) diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js deleted file mode 100644 index 52e351aee..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js +++ /dev/null @@ -1,125 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -let mapStateToProps -let mapDispatchToProps - -const actionSpies = { - setMaxModeTo: sinon.spy(), - updateSendAmount: sinon.spy(), -} -const duckActionSpies = { - updateSendErrors: sinon.spy(), -} - -proxyquire('../send-amount-row.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - }, - '../../send.selectors': { - getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, - getConversionRate: (s) => `mockConversionRate:${s}`, - getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, - getGasTotal: (s) => `mockGasTotal:${s}`, - getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`, - getSelectedToken: (s) => `mockSelectedToken:${s}`, - getSendAmount: (s) => `mockAmount:${s}`, - getSendFromBalance: (s) => `mockBalance:${s}`, - getTokenBalance: (s) => `mockTokenBalance:${s}`, - }, - './send-amount-row.selectors': { sendAmountIsInError: (s) => `mockInError:${s}` }, - '../../send.utils': { - getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }), - getGasFeeErrorObject: (mockDataObject) => ({ ...mockDataObject, mockGasFeeErrorChange: true }), - }, - '../../../../actions': actionSpies, - '../../../../ducks/send.duck': duckActionSpies, -}) - -describe('send-amount-row container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - amount: 'mockAmount:mockState', - amountConversionRate: 'mockAmountConversionRate:mockState', - balance: 'mockBalance:mockState', - conversionRate: 'mockConversionRate:mockState', - convertedCurrency: 'mockConvertedCurrency:mockState', - gasTotal: 'mockGasTotal:mockState', - inError: 'mockInError:mockState', - primaryCurrency: 'mockPrimaryCurrency:mockState', - selectedToken: 'mockSelectedToken:mockState', - tokenBalance: 'mockTokenBalance:mockState', - }) - }) - - }) - - describe('mapDispatchToProps()', () => { - let dispatchSpy - let mapDispatchToPropsObject - - beforeEach(() => { - dispatchSpy = sinon.spy() - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) - duckActionSpies.updateSendErrors.resetHistory() - }) - - describe('setMaxModeTo()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.setMaxModeTo('mockBool') - assert(dispatchSpy.calledOnce) - assert(actionSpies.setMaxModeTo.calledOnce) - assert.equal( - actionSpies.setMaxModeTo.getCall(0).args[0], - 'mockBool' - ) - }) - }) - - describe('updateSendAmount()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.updateSendAmount('mockAmount') - assert(dispatchSpy.calledOnce) - assert(actionSpies.updateSendAmount.calledOnce) - assert.equal( - actionSpies.updateSendAmount.getCall(0).args[0], - 'mockAmount' - ) - }) - }) - - describe('updateGasFeeError()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.updateGasFeeError({ some: 'data' }) - assert(dispatchSpy.calledOnce) - assert(duckActionSpies.updateSendErrors.calledOnce) - assert.deepEqual( - duckActionSpies.updateSendErrors.getCall(0).args[0], - { some: 'data', mockGasFeeErrorChange: true } - ) - }) - }) - - describe('updateSendAmountError()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.updateSendAmountError({ some: 'data' }) - assert(dispatchSpy.calledOnce) - assert(duckActionSpies.updateSendErrors.calledOnce) - assert.deepEqual( - duckActionSpies.updateSendErrors.getCall(0).args[0], - { some: 'data', mockChange: true } - ) - }) - }) - - }) - -}) diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js deleted file mode 100644 index 4672cb8a7..000000000 --- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js +++ /dev/null @@ -1,34 +0,0 @@ -import assert from 'assert' -import { - sendAmountIsInError, -} from '../send-amount-row.selectors.js' - -describe('send-amount-row selectors', () => { - - describe('sendAmountIsInError()', () => { - it('should return true if send.errors.amount is truthy', () => { - const state = { - send: { - errors: { - amount: 'abc', - }, - }, - } - - assert.equal(sendAmountIsInError(state), true) - }) - - it('should return false if send.errors.amount is falsy', () => { - const state = { - send: { - errors: { - amount: null, - }, - }, - } - - assert.equal(sendAmountIsInError(state), false) - }) - }) - -}) diff --git a/ui/app/components/send_/send-content/send-content-README.md b/ui/app/components/send_/send-content/send-content-README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-content.component.js b/ui/app/components/send_/send-content/send-content.component.js deleted file mode 100644 index adc114c0e..000000000 --- a/ui/app/components/send_/send-content/send-content.component.js +++ /dev/null @@ -1,28 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import PageContainerContent from '../../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 SendToRow from './send-to-row/' - -export default class SendContent extends Component { - - static propTypes = { - updateGas: PropTypes.func, - }; - - render () { - return ( - -
- - this.props.updateGas(updateData)} /> - this.props.updateGas(updateData)} /> - -
-
- ) - } - -} diff --git a/ui/app/components/send_/send-content/send-content.scss b/ui/app/components/send_/send-content/send-content.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-dropdown-list/index.js b/ui/app/components/send_/send-content/send-dropdown-list/index.js deleted file mode 100644 index 04af6536c..000000000 --- a/ui/app/components/send_/send-content/send-dropdown-list/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './send-dropdown-list.component' diff --git a/ui/app/components/send_/send-content/send-dropdown-list/send-dropdown-list.component.js b/ui/app/components/send_/send-content/send-dropdown-list/send-dropdown-list.component.js deleted file mode 100644 index bedac1259..000000000 --- a/ui/app/components/send_/send-content/send-dropdown-list/send-dropdown-list.component.js +++ /dev/null @@ -1,52 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import AccountListItem from '../../account-list-item/' - -export default class SendDropdownList extends Component { - - static propTypes = { - accounts: PropTypes.array, - closeDropdown: PropTypes.func, - onSelect: PropTypes.func, - activeAddress: PropTypes.string, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - getListItemIcon (accountAddress, activeAddress) { - return accountAddress === activeAddress - ? - : null - } - - render () { - const { - accounts, - closeDropdown, - onSelect, - activeAddress, - } = this.props - - return (
-
closeDropdown()} - /> -
- {accounts.map((account, index) => { - onSelect(account) - closeDropdown() - }} - icon={this.getListItemIcon(account.address, activeAddress)} - key={`send-dropdown-account-#${index}`} - />)} -
-
) - } - -} diff --git a/ui/app/components/send_/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js b/ui/app/components/send_/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js deleted file mode 100644 index b92dd4dfe..000000000 --- a/ui/app/components/send_/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -import SendDropdownList from '../send-dropdown-list.component.js' - -import AccountListItem from '../../../account-list-item/account-list-item.container' - -const propsMethodSpies = { - closeDropdown: sinon.spy(), - onSelect: sinon.spy(), -} - -sinon.spy(SendDropdownList.prototype, 'getListItemIcon') - -describe('SendDropdownList Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow(, { context: { t: str => str + '_t' } }) - }) - - afterEach(() => { - propsMethodSpies.closeDropdown.resetHistory() - propsMethodSpies.onSelect.resetHistory() - SendDropdownList.prototype.getListItemIcon.resetHistory() - }) - - describe('getListItemIcon', () => { - it('should return check icon if the passed addresses are the same', () => { - assert.deepEqual( - wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount0'), - - ) - }) - - it('should return null if the passed addresses are different', () => { - assert.equal( - wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount1'), - null - ) - }) - }) - - describe('render', () => { - it('should render a single div with two children', () => { - assert(wrapper.is('div')) - assert.equal(wrapper.children().length, 2) - }) - - it('should render the children with the correct classes', () => { - assert(wrapper.childAt(0).hasClass('send-v2__from-dropdown__close-area')) - assert(wrapper.childAt(1).hasClass('send-v2__from-dropdown__list')) - }) - - it('should call closeDropdown onClick of the send-v2__from-dropdown__close-area', () => { - assert.equal(propsMethodSpies.closeDropdown.callCount, 0) - wrapper.childAt(0).props().onClick() - assert.equal(propsMethodSpies.closeDropdown.callCount, 1) - }) - - it('should render an AccountListItem for each item in accounts', () => { - assert.equal(wrapper.childAt(1).children().length, 3) - assert(wrapper.childAt(1).children().every(AccountListItem)) - }) - - it('should pass the correct props to the AccountListItem', () => { - wrapper.childAt(1).children().forEach((accountListItem, index) => { - const { - account, - className, - handleClick, - } = accountListItem.props() - assert.deepEqual(account, { address: 'mockAccount' + index }) - assert.equal(className, 'account-list-item__dropdown') - assert.equal(propsMethodSpies.onSelect.callCount, 0) - handleClick() - assert.equal(propsMethodSpies.onSelect.callCount, 1) - assert.deepEqual(propsMethodSpies.onSelect.getCall(0).args[0], { address: 'mockAccount' + index }) - propsMethodSpies.onSelect.resetHistory() - propsMethodSpies.closeDropdown.resetHistory() - assert.equal(propsMethodSpies.closeDropdown.callCount, 0) - handleClick() - assert.equal(propsMethodSpies.closeDropdown.callCount, 1) - propsMethodSpies.onSelect.resetHistory() - propsMethodSpies.closeDropdown.resetHistory() - }) - }) - - it('should call this.getListItemIcon for each AccountListItem', () => { - assert.equal(SendDropdownList.prototype.getListItemIcon.callCount, 3) - const getListItemIconCalls = SendDropdownList.prototype.getListItemIcon.getCalls() - assert(getListItemIconCalls.every(({ args }, index) => args[0] === 'mockAccount' + index)) - }) - }) -}) diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js deleted file mode 100644 index 4f43a9d61..000000000 --- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js +++ /dev/null @@ -1,46 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import AccountListItem from '../../../account-list-item/' -import SendDropdownList from '../../send-dropdown-list/' - -export default class FromDropdown extends Component { - - static propTypes = { - accounts: PropTypes.array, - closeDropdown: PropTypes.func, - dropdownOpen: PropTypes.bool, - onSelect: PropTypes.func, - openDropdown: PropTypes.func, - selectedAccount: PropTypes.object, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - render () { - const { - accounts, - closeDropdown, - dropdownOpen, - openDropdown, - selectedAccount, - onSelect, - } = this.props - - return
- } - /> - {dropdownOpen && } -
- } - -} diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/index.js b/ui/app/components/send_/send-content/send-from-row/from-dropdown/index.js deleted file mode 100644 index 2314ef4e3..000000000 --- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './from-dropdown.component' diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js b/ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js deleted file mode 100644 index 84fcb281e..000000000 --- a/ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -import FromDropdown from '../from-dropdown.component.js' - -import AccountListItem from '../../../../account-list-item/account-list-item.container' -import SendDropdownList from '../../../send-dropdown-list/send-dropdown-list.component' - -const propsMethodSpies = { - closeDropdown: sinon.spy(), - openDropdown: sinon.spy(), - onSelect: sinon.spy(), -} - -describe('FromDropdown Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow(, { context: { t: str => str + '_t' } }) - }) - - afterEach(() => { - propsMethodSpies.closeDropdown.resetHistory() - propsMethodSpies.openDropdown.resetHistory() - propsMethodSpies.onSelect.resetHistory() - }) - - describe('render', () => { - it('should render a div with a .send-v2__from-dropdown class', () => { - assert.equal(wrapper.find('.send-v2__from-dropdown').length, 1) - }) - - it('should render an AccountListItem as the first child of the .send-v2__from-dropdown div', () => { - assert(wrapper.find('.send-v2__from-dropdown').childAt(0).is(AccountListItem)) - }) - - it('should pass the correct props to AccountListItem', () => { - const { - account, - handleClick, - icon, - } = wrapper.find('.send-v2__from-dropdown').childAt(0).props() - assert.deepEqual(account, { address: 'mockAddress' }) - assert.deepEqual( - icon, - - ) - assert.equal(propsMethodSpies.openDropdown.callCount, 0) - handleClick() - assert.equal(propsMethodSpies.openDropdown.callCount, 1) - }) - - it('should not render a SendDropdownList when dropdownOpen is false', () => { - assert.equal(wrapper.find(SendDropdownList).length, 0) - }) - - it('should render a SendDropdownList when dropdownOpen is true', () => { - wrapper.setProps({ dropdownOpen: true }) - assert(wrapper.find(SendDropdownList).length, 1) - }) - - it('should pass the correct props to the SendDropdownList]', () => { - wrapper.setProps({ dropdownOpen: true }) - const { - accounts, - closeDropdown, - onSelect, - activeAddress, - } = wrapper.find(SendDropdownList).props() - assert.deepEqual(accounts, ['mockAccount']) - assert.equal(activeAddress, 'mockAddress') - assert.equal(propsMethodSpies.closeDropdown.callCount, 0) - closeDropdown() - assert.equal(propsMethodSpies.closeDropdown.callCount, 1) - assert.equal(propsMethodSpies.onSelect.callCount, 0) - onSelect() - assert.equal(propsMethodSpies.onSelect.callCount, 1) - }) - }) -}) diff --git a/ui/app/components/send_/send-content/send-from-row/index.js b/ui/app/components/send_/send-content/send-from-row/index.js deleted file mode 100644 index 0a79726b2..000000000 --- a/ui/app/components/send_/send-content/send-from-row/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './send-from-row.container' diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row-README.md b/ui/app/components/send_/send-content/send-from-row/send-from-row-README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js deleted file mode 100644 index 3e0e0de22..000000000 --- a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import SendRowWrapper from '../send-row-wrapper/' -import FromDropdown from './from-dropdown/' - -export default class SendFromRow extends Component { - - static propTypes = { - closeFromDropdown: PropTypes.func, - conversionRate: PropTypes.number, - from: PropTypes.object, - fromAccounts: PropTypes.array, - fromDropdownOpen: PropTypes.bool, - openFromDropdown: PropTypes.func, - tokenContract: PropTypes.object, - updateSendFrom: PropTypes.func, - setSendTokenBalance: PropTypes.func, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - async handleFromChange (newFrom) { - const { - updateSendFrom, - tokenContract, - setSendTokenBalance, - } = this.props - - if (tokenContract) { - const usersToken = await tokenContract.balanceOf(newFrom.address) - setSendTokenBalance(usersToken) - } - updateSendFrom(newFrom) - } - - render () { - const { - closeFromDropdown, - conversionRate, - from, - fromAccounts, - fromDropdownOpen, - openFromDropdown, - } = this.props - - return ( - - closeFromDropdown()} - conversionRate={conversionRate} - dropdownOpen={fromDropdownOpen} - onSelect={newFrom => this.handleFromChange(newFrom)} - openDropdown={() => openFromDropdown()} - selectedAccount={from} - /> - - ) - } - -} diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js deleted file mode 100644 index 33cb63b43..000000000 --- a/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js +++ /dev/null @@ -1,46 +0,0 @@ -import { connect } from 'react-redux' -import { - accountsWithSendEtherInfoSelector, - getConversionRate, - getSelectedTokenContract, - getSendFromObject, -} from '../../send.selectors.js' -import { - getFromDropdownOpen, -} from './send-from-row.selectors.js' -import { calcTokenBalance } from '../../send.utils.js' -import { - updateSendFrom, - setSendTokenBalance, -} from '../../../../actions' -import { - closeFromDropdown, - openFromDropdown, -} from '../../../../ducks/send.duck' -import SendFromRow from './send-from-row.component' - -export default connect(mapStateToProps, mapDispatchToProps)(SendFromRow) - -function mapStateToProps (state) { - return { - conversionRate: getConversionRate(state), - from: getSendFromObject(state), - fromAccounts: accountsWithSendEtherInfoSelector(state), - fromDropdownOpen: getFromDropdownOpen(state), - tokenContract: getSelectedTokenContract(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - closeFromDropdown: () => dispatch(closeFromDropdown()), - openFromDropdown: () => dispatch(openFromDropdown()), - updateSendFrom: newFrom => dispatch(updateSendFrom(newFrom)), - setSendTokenBalance: (usersToken, selectedToken) => { - if (!usersToken) return - - const tokenBalance = calcTokenBalance({ usersToken, selectedToken }) - dispatch(setSendTokenBalance(tokenBalance)) - }, - } -} diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js deleted file mode 100644 index 03ef4806b..000000000 --- a/ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js +++ /dev/null @@ -1,9 +0,0 @@ -const selectors = { - getFromDropdownOpen, -} - -module.exports = selectors - -function getFromDropdownOpen (state) { - return state.send.fromDropdownOpen -} diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js deleted file mode 100644 index 9ba8d1739..000000000 --- a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -import SendFromRow from '../send-from-row.component.js' - -import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' -import FromDropdown from '../from-dropdown/from-dropdown.component' - -const propsMethodSpies = { - closeFromDropdown: sinon.spy(), - openFromDropdown: sinon.spy(), - updateSendFrom: sinon.spy(), - setSendTokenBalance: sinon.spy(), -} - -sinon.spy(SendFromRow.prototype, 'handleFromChange') - -describe('SendFromRow Component', function () { - let wrapper - let instance - - beforeEach(() => { - wrapper = shallow(, { context: { t: str => str + '_t' } }) - instance = wrapper.instance() - }) - - afterEach(() => { - propsMethodSpies.closeFromDropdown.resetHistory() - propsMethodSpies.openFromDropdown.resetHistory() - propsMethodSpies.updateSendFrom.resetHistory() - propsMethodSpies.setSendTokenBalance.resetHistory() - SendFromRow.prototype.handleFromChange.resetHistory() - }) - - describe('handleFromChange', () => { - - it('should call updateSendFrom', () => { - assert.equal(propsMethodSpies.updateSendFrom.callCount, 0) - instance.handleFromChange('mockFrom') - assert.equal(propsMethodSpies.updateSendFrom.callCount, 1) - assert.deepEqual( - propsMethodSpies.updateSendFrom.getCall(0).args, - ['mockFrom'] - ) - }) - - it('should call tokenContract.balanceOf and setSendTokenBalance if tokenContract is defined', async () => { - wrapper.setProps({ - tokenContract: { - balanceOf: () => new Promise((resolve) => resolve('mockUsersToken')), - }, - }) - assert.equal(propsMethodSpies.setSendTokenBalance.callCount, 0) - await instance.handleFromChange('mockFrom') - assert.equal(propsMethodSpies.setSendTokenBalance.callCount, 1) - assert.deepEqual( - propsMethodSpies.setSendTokenBalance.getCall(0).args, - ['mockUsersToken'] - ) - }) - - }) - - describe('render', () => { - it('should render a SendRowWrapper component', () => { - assert.equal(wrapper.find(SendRowWrapper).length, 1) - }) - - it('should pass the correct props to SendRowWrapper', () => { - const { - label, - } = wrapper.find(SendRowWrapper).props() - - assert.equal(label, 'from_t:') - }) - - it('should render an FromDropdown as a child of the SendRowWrapper', () => { - assert(wrapper.find(SendRowWrapper).childAt(0).is(FromDropdown)) - }) - - it('should render the FromDropdown with the correct props', () => { - const { - accounts, - closeDropdown, - conversionRate, - dropdownOpen, - onSelect, - openDropdown, - selectedAccount, - } = wrapper.find(SendRowWrapper).childAt(0).props() - assert.deepEqual(accounts, ['mockAccount']) - assert.equal(dropdownOpen, false) - assert.equal(conversionRate, 15) - assert.deepEqual(selectedAccount, { address: 'mockAddress' }) - assert.equal(propsMethodSpies.closeFromDropdown.callCount, 0) - closeDropdown() - assert.equal(propsMethodSpies.closeFromDropdown.callCount, 1) - assert.equal(propsMethodSpies.openFromDropdown.callCount, 0) - openDropdown() - assert.equal(propsMethodSpies.openFromDropdown.callCount, 1) - assert.equal(SendFromRow.prototype.handleFromChange.callCount, 0) - onSelect('mockNewFrom') - assert.equal(SendFromRow.prototype.handleFromChange.callCount, 1) - assert.deepEqual( - SendFromRow.prototype.handleFromChange.getCall(0).args, - ['mockNewFrom'] - ) - }) - }) -}) diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js deleted file mode 100644 index e080b2fe3..000000000 --- a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js +++ /dev/null @@ -1,110 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -let mapStateToProps -let mapDispatchToProps - -const actionSpies = { - updateSendFrom: sinon.spy(), - setSendTokenBalance: sinon.spy(), -} -const duckActionSpies = { - closeFromDropdown: sinon.spy(), - openFromDropdown: sinon.spy(), -} - -proxyquire('../send-from-row.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - }, - '../../send.selectors.js': { - accountsWithSendEtherInfoSelector: (s) => `mockFromAccounts:${s}`, - getConversionRate: (s) => `mockConversionRate:${s}`, - getSelectedTokenContract: (s) => `mockTokenContract:${s}`, - getSendFromObject: (s) => `mockFrom:${s}`, - }, - './send-from-row.selectors.js': { getFromDropdownOpen: (s) => `mockFromDropdownOpen:${s}` }, - '../../send.utils.js': { calcTokenBalance: ({ usersToken, selectedToken }) => usersToken + selectedToken }, - '../../../../actions': actionSpies, - '../../../../ducks/send.duck': duckActionSpies, -}) - -describe('send-from-row container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - conversionRate: 'mockConversionRate:mockState', - from: 'mockFrom:mockState', - fromAccounts: 'mockFromAccounts:mockState', - fromDropdownOpen: 'mockFromDropdownOpen:mockState', - tokenContract: 'mockTokenContract:mockState', - }) - }) - - }) - - describe('mapDispatchToProps()', () => { - let dispatchSpy - let mapDispatchToPropsObject - - beforeEach(() => { - dispatchSpy = sinon.spy() - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) - }) - - describe('closeFromDropdown()', () => { - it('should dispatch a closeFromDropdown action', () => { - mapDispatchToPropsObject.closeFromDropdown() - assert(dispatchSpy.calledOnce) - assert(duckActionSpies.closeFromDropdown.calledOnce) - assert.equal( - duckActionSpies.closeFromDropdown.getCall(0).args[0], - undefined - ) - }) - }) - - describe('openFromDropdown()', () => { - it('should dispatch a openFromDropdown action', () => { - mapDispatchToPropsObject.openFromDropdown() - assert(dispatchSpy.calledOnce) - assert(duckActionSpies.openFromDropdown.calledOnce) - assert.equal( - duckActionSpies.openFromDropdown.getCall(0).args[0], - undefined - ) - }) - }) - - describe('updateSendFrom()', () => { - it('should dispatch an updateSendFrom action', () => { - mapDispatchToPropsObject.updateSendFrom('mockFrom') - assert(dispatchSpy.calledOnce) - assert.equal( - actionSpies.updateSendFrom.getCall(0).args[0], - 'mockFrom' - ) - }) - }) - - describe('setSendTokenBalance()', () => { - it('should dispatch an setSendTokenBalance action', () => { - mapDispatchToPropsObject.setSendTokenBalance('mockUsersToken', 'mockSelectedToken') - assert(dispatchSpy.calledOnce) - assert.equal( - actionSpies.setSendTokenBalance.getCall(0).args[0], - 'mockUsersTokenmockSelectedToken' - ) - }) - }) - - }) - -}) diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js b/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js deleted file mode 100644 index ecb57bbc3..000000000 --- a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import assert from 'assert' -import { - getFromDropdownOpen, -} from '../send-from-row.selectors.js' - -describe('send-from-row selectors', () => { - - describe('getFromDropdownOpen()', () => { - it('should get send.fromDropdownOpen', () => { - const state = { - send: { - fromDropdownOpen: null, - }, - } - - assert.equal(getFromDropdownOpen(state), null) - }) - }) - -}) diff --git a/ui/app/components/send_/send-content/send-gas-row/README.md b/ui/app/components/send_/send-content/send-gas-row/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js deleted file mode 100644 index bb9a94428..000000000 --- a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js +++ /dev/null @@ -1,61 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import CurrencyDisplay from '../../../../send/currency-display' - - -export default class GasFeeDisplay extends Component { - - static propTypes = { - conversionRate: PropTypes.number, - primaryCurrency: PropTypes.string, - convertedCurrency: PropTypes.string, - gasLoadingError: PropTypes.bool, - gasTotal: PropTypes.string, - onClick: PropTypes.func, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - render () { - const { - conversionRate, - gasTotal, - onClick, - primaryCurrency = 'ETH', - convertedCurrency, - gasLoadingError, - } = this.props - - return ( -
- {gasTotal - ? - : gasLoadingError - ?
- {this.context.t('setGasPrice')} -
- :
- {this.context.t('loading')} -
- } - -
- ) - } -} diff --git a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/index.js b/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/index.js deleted file mode 100644 index dba0edb7b..000000000 --- a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './gas-fee-display.component' diff --git a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js deleted file mode 100644 index 7cbe8d0df..000000000 --- a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react' -import assert from 'assert' -import {shallow} from 'enzyme' -import GasFeeDisplay from '../gas-fee-display.component' -import CurrencyDisplay from '../../../../../send/currency-display' -import sinon from 'sinon' - - -const propsMethodSpies = { - showCustomizeGasModal: sinon.spy(), -} - -describe('SendGasRow Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow(, {context: {t: str => str + '_t'}}) - }) - - afterEach(() => { - propsMethodSpies.showCustomizeGasModal.resetHistory() - }) - - describe('render', () => { - it('should render a CurrencyDisplay component', () => { - assert.equal(wrapper.find(CurrencyDisplay).length, 1) - }) - - it('should render the CurrencyDisplay with the correct props', () => { - const { - conversionRate, - convertedCurrency, - value, - } = wrapper.find(CurrencyDisplay).props() - assert.equal(conversionRate, 20) - assert.equal(convertedCurrency, 'mockConvertedCurrency') - assert.equal(value, 'mockGasTotal') - }) - - it('should render the Button with the correct props', () => { - const { - onClick, - } = wrapper.find('button').props() - assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) - onClick() - assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) - }) - }) -}) diff --git a/ui/app/components/send_/send-content/send-gas-row/index.js b/ui/app/components/send_/send-content/send-gas-row/index.js deleted file mode 100644 index 3c7ff1d5f..000000000 --- a/ui/app/components/send_/send-content/send-gas-row/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './send-gas-row.container' diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js deleted file mode 100644 index 91b58cfd0..000000000 --- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js +++ /dev/null @@ -1,48 +0,0 @@ -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' - -export default class SendGasRow extends Component { - - static propTypes = { - conversionRate: PropTypes.number, - convertedCurrency: PropTypes.string, - gasFeeError: PropTypes.bool, - gasLoadingError: PropTypes.bool, - gasTotal: PropTypes.string, - showCustomizeGasModal: PropTypes.func, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - render () { - const { - conversionRate, - convertedCurrency, - gasLoadingError, - gasTotal, - gasFeeError, - showCustomizeGasModal, - } = this.props - - return ( - - showCustomizeGasModal()} - /> - - ) - } - -} diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js deleted file mode 100644 index 8f8e3e4dd..000000000 --- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js +++ /dev/null @@ -1,27 +0,0 @@ -import { connect } from 'react-redux' -import { - getConversionRate, - getCurrentCurrency, - getGasTotal, -} from '../../send.selectors.js' -import { getGasLoadingError, gasFeeIsInError } from './send-gas-row.selectors.js' -import { showModal } from '../../../../actions' -import SendGasRow from './send-gas-row.component' - -export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow) - -function mapStateToProps (state) { - return { - conversionRate: getConversionRate(state), - convertedCurrency: getCurrentCurrency(state), - gasTotal: getGasTotal(state), - gasFeeError: gasFeeIsInError(state), - gasLoadingError: getGasLoadingError(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS' })), - } -} diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js deleted file mode 100644 index 96f6293c2..000000000 --- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js +++ /dev/null @@ -1,14 +0,0 @@ -const selectors = { - gasFeeIsInError, - getGasLoadingError, -} - -module.exports = selectors - -function getGasLoadingError (state) { - return state.send.errors.gasLoading -} - -function gasFeeIsInError (state) { - return Boolean(state.send.errors.gasFee) -} diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js deleted file mode 100644 index 54a92bd2d..000000000 --- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -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' - -const propsMethodSpies = { - showCustomizeGasModal: sinon.spy(), -} - -describe('SendGasRow Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow(, { context: { t: str => str + '_t' } }) - }) - - afterEach(() => { - propsMethodSpies.showCustomizeGasModal.resetHistory() - }) - - describe('render', () => { - it('should render a SendRowWrapper component', () => { - assert.equal(wrapper.find(SendRowWrapper).length, 1) - }) - - it('should pass the correct props to SendRowWrapper', () => { - const { - label, - showError, - errorType, - } = wrapper.find(SendRowWrapper).props() - - assert.equal(label, 'gasFee_t:') - assert.equal(showError, 'mockGasFeeError') - assert.equal(errorType, 'gasFee') - }) - - it('should render a GasFeeDisplay as a child of the SendRowWrapper', () => { - assert(wrapper.find(SendRowWrapper).childAt(0).is(GasFeeDisplay)) - }) - - it('should render the GasFeeDisplay with the correct props', () => { - const { - conversionRate, - convertedCurrency, - gasLoadingError, - gasTotal, - onClick, - } = wrapper.find(SendRowWrapper).childAt(0).props() - assert.equal(conversionRate, 20) - assert.equal(convertedCurrency, 'mockConvertedCurrency') - assert.equal(gasLoadingError, false) - assert.equal(gasTotal, 'mockGasTotal') - assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) - onClick() - assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) - }) - }) -}) diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js deleted file mode 100644 index 2ce062505..000000000 --- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js +++ /dev/null @@ -1,70 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -let mapStateToProps -let mapDispatchToProps - -const actionSpies = { - showModal: sinon.spy(), -} - -proxyquire('../send-gas-row.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - }, - '../../send.selectors.js': { - getConversionRate: (s) => `mockConversionRate:${s}`, - getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, - getGasTotal: (s) => `mockGasTotal:${s}`, - }, - './send-gas-row.selectors.js': { - getGasLoadingError: (s) => `mockGasLoadingError:${s}`, - gasFeeIsInError: (s) => `mockGasFeeError:${s}`, - }, - '../../../../actions': actionSpies, -}) - -describe('send-gas-row container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - conversionRate: 'mockConversionRate:mockState', - convertedCurrency: 'mockConvertedCurrency:mockState', - gasTotal: 'mockGasTotal:mockState', - gasFeeError: 'mockGasFeeError:mockState', - gasLoadingError: 'mockGasLoadingError:mockState', - }) - }) - - }) - - describe('mapDispatchToProps()', () => { - let dispatchSpy - let mapDispatchToPropsObject - - beforeEach(() => { - dispatchSpy = sinon.spy() - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) - }) - - describe('showCustomizeGasModal()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.showCustomizeGasModal() - assert(dispatchSpy.calledOnce) - assert.deepEqual( - actionSpies.showModal.getCall(0).args[0], - { name: 'CUSTOMIZE_GAS' } - ) - }) - }) - - }) - -}) diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js deleted file mode 100644 index d46dd9d8b..000000000 --- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js +++ /dev/null @@ -1,49 +0,0 @@ -import assert from 'assert' -import { - gasFeeIsInError, - getGasLoadingError, -} from '../send-gas-row.selectors.js' - -describe('send-gas-row selectors', () => { - - describe('getGasLoadingError()', () => { - it('should return send.errors.gasLoading', () => { - const state = { - send: { - errors: { - gasLoading: 'abc', - }, - }, - } - - assert.equal(getGasLoadingError(state), 'abc') - }) - }) - - describe('gasFeeIsInError()', () => { - it('should return true if send.errors.gasFee is truthy', () => { - const state = { - send: { - errors: { - gasFee: 'def', - }, - }, - } - - assert.equal(gasFeeIsInError(state), true) - }) - - it('should return false send.errors.gasFee is falsely', () => { - const state = { - send: { - errors: { - gasFee: null, - }, - }, - } - - assert.equal(gasFeeIsInError(state), false) - }) - }) - -}) diff --git a/ui/app/components/send_/send-content/send-row-wrapper/index.js b/ui/app/components/send_/send-content/send-row-wrapper/index.js deleted file mode 100644 index d17545dcc..000000000 --- a/ui/app/components/send_/send-content/send-row-wrapper/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './send-row-wrapper.component' diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/index.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/index.js deleted file mode 100644 index c00617f83..000000000 --- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './send-row-error-message.container' diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js deleted file mode 100644 index 61bc7bab7..000000000 --- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js +++ /dev/null @@ -1,27 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' - -export default class SendRowErrorMessage extends Component { - - static propTypes = { - errors: PropTypes.object, - errorType: PropTypes.string, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - render () { - const { errors, errorType } = this.props - - const errorMessage = errors[errorType] - - return ( - errorMessage - ?
{this.context.t(errorMessage)}
- : null - ) - } - -} diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js deleted file mode 100644 index 59622047f..000000000 --- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from 'react-redux' -import { getSendErrors } from '../../../send.selectors' -import SendRowErrorMessage from './send-row-error-message.component' - -export default connect(mapStateToProps)(SendRowErrorMessage) - -function mapStateToProps (state, ownProps) { - return { - errors: getSendErrors(state), - errorType: ownProps.errorType, - } -} diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js deleted file mode 100644 index 2304a43d2..000000000 --- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import SendRowErrorMessage from '../send-row-error-message.component.js' - -describe('SendRowErrorMessage Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow(, { context: { t: str => str + '_t' } }) - }) - - describe('render', () => { - it('should render null if the passed errors do not contain an error of errorType', () => { - assert.equal(wrapper.find('.send-v2__error').length, 0) - assert.equal(wrapper.html(), null) - }) - - it('should render an error message if the passed errors contain an error of errorType', () => { - wrapper.setProps({ errors: { error1: 'abc', error2: 'def', error3: 'xyz' } }) - assert.equal(wrapper.find('.send-v2__error').length, 1) - assert.equal(wrapper.find('.send-v2__error').text(), 'xyz_t') - }) - }) -}) diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js deleted file mode 100644 index eecff165d..000000000 --- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' - -let mapStateToProps - -proxyquire('../send-row-error-message.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - return () => ({}) - }, - }, - '../../../send.selectors': { getSendErrors: (s) => `mockErrors:${s}` }, -}) - -describe('send-row-error-message container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState', { errorType: 'someType' }), { - errors: 'mockErrors:mockState', - errorType: 'someType' }) - }) - - }) - -}) diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js deleted file mode 100644 index b7528a15f..000000000 --- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js +++ /dev/null @@ -1,43 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import SendRowErrorMessage from './send-row-error-message/' - -export default class SendRowWrapper extends Component { - - static propTypes = { - children: PropTypes.node, - errorType: PropTypes.string, - label: PropTypes.string, - showError: PropTypes.bool, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - render () { - const { - children, - errorType = '', - label, - showError = false, - } = this.props - - const formField = Array.isArray(children) ? children[1] || children[0] : children - const customLabelContent = children.length > 1 ? children[0] : null - - return ( -
-
- {label} - {showError && } - {customLabelContent} -
-
- {formField} -
-
- ) - } - -} diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js b/ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js deleted file mode 100644 index 30280e1d0..000000000 --- a/ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import SendRowWrapper from '../send-row-wrapper.component.js' - -import SendRowErrorMessage from '../send-row-error-message/send-row-error-message.container' - -describe('SendContent Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow( - Mock Form Field - ) - }) - - describe('render', () => { - it('should render a div with a send-v2__form-row class', () => { - assert.equal(wrapper.find('div.send-v2__form-row').length, 1) - }) - - it('should render two children of the root div, with send-v2_form label and field classes', () => { - assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-label').length, 1) - assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-field').length, 1) - }) - - it('should render the label as a child of the send-v2__form-label', () => { - assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-label').childAt(0).text(), 'mockLabel') - }) - - it('should render its first child as a child of the send-v2__form-field', () => { - assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-field').childAt(0).text(), 'Mock Form Field') - }) - - it('should not render a SendRowErrorMessage if showError is false', () => { - assert.equal(wrapper.find(SendRowErrorMessage).length, 0) - }) - - it('should render a SendRowErrorMessage with and errorType props if showError is true', () => { - wrapper.setProps({showError: true}) - assert.equal(wrapper.find(SendRowErrorMessage).length, 1) - - const expectedSendRowErrorMessage = wrapper.find('.send-v2__form-row > .send-v2__form-label').childAt(1) - assert(expectedSendRowErrorMessage.is(SendRowErrorMessage)) - assert.deepEqual( - expectedSendRowErrorMessage.props(), - { errorType: 'mockErrorType' } - ) - }) - - it('should render its second child as a child of the send-v2__form-field, if it has two children', () => { - wrapper = shallow( - Mock Custom Label Content - Mock Form Field - ) - assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-field').childAt(0).text(), 'Mock Form Field') - }) - - it('should render its first child as the last child of the send-v2__form-label, if it has two children', () => { - wrapper = shallow( - Mock Custom Label Content - Mock Form Field - ) - assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-label').childAt(1).text(), 'Mock Custom Label Content') - }) - }) -}) diff --git a/ui/app/components/send_/send-content/send-to-row/index.js b/ui/app/components/send_/send-content/send-to-row/index.js deleted file mode 100644 index 121f15148..000000000 --- a/ui/app/components/send_/send-content/send-to-row/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './send-to-row.container' diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row-README.md b/ui/app/components/send_/send-content/send-to-row/send-to-row-README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js deleted file mode 100644 index 892ad5d67..000000000 --- a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js +++ /dev/null @@ -1,69 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import SendRowWrapper from '../send-row-wrapper/' -import EnsInput from '../../../ens-input' -import { getToErrorObject } from './send-to-row.utils.js' - -export default class SendToRow extends Component { - - static propTypes = { - closeToDropdown: PropTypes.func, - inError: PropTypes.bool, - network: PropTypes.string, - openToDropdown: PropTypes.func, - to: PropTypes.string, - toAccounts: PropTypes.array, - toDropdownOpen: PropTypes.bool, - updateGas: PropTypes.func, - updateSendTo: PropTypes.func, - updateSendToError: PropTypes.func, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - handleToChange (to, nickname = '', toError) { - const { updateSendTo, updateSendToError, updateGas } = this.props - const toErrorObject = getToErrorObject(to, toError) - updateSendTo(to, nickname) - updateSendToError(toErrorObject) - if (toErrorObject.to === null) { - updateGas({ to }) - } - } - - render () { - const { - closeToDropdown, - inError, - network, - openToDropdown, - to, - toAccounts, - toDropdownOpen, - } = this.props - - return ( - - closeToDropdown()} - dropdownOpen={toDropdownOpen} - inError={inError} - name={'address'} - network={network} - onChange={({ toAddress, nickname, toError }) => this.handleToChange(toAddress, nickname, toError)} - openDropdown={() => openToDropdown()} - placeholder={this.context.t('recipientAddress')} - to={to} - /> - - ) - } - -} diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js deleted file mode 100644 index 1c9c9d518..000000000 --- a/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js +++ /dev/null @@ -1,42 +0,0 @@ -import { connect } from 'react-redux' -import { - getCurrentNetwork, - getSendTo, - getSendToAccounts, -} from '../../send.selectors.js' -import { - getToDropdownOpen, - sendToIsInError, -} from './send-to-row.selectors.js' -import { - updateSendTo, -} from '../../../../actions' -import { - updateSendErrors, - openToDropdown, - closeToDropdown, -} from '../../../../ducks/send.duck' -import SendToRow from './send-to-row.component' - -export default connect(mapStateToProps, mapDispatchToProps)(SendToRow) - -function mapStateToProps (state) { - return { - inError: sendToIsInError(state), - network: getCurrentNetwork(state), - to: getSendTo(state), - toAccounts: getSendToAccounts(state), - toDropdownOpen: getToDropdownOpen(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - closeToDropdown: () => dispatch(closeToDropdown()), - openToDropdown: () => dispatch(openToDropdown()), - updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), - updateSendToError: (toErrorObject) => { - dispatch(updateSendErrors(toErrorObject)) - }, - } -} diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js deleted file mode 100644 index 8919014be..000000000 --- a/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js +++ /dev/null @@ -1,14 +0,0 @@ -const selectors = { - getToDropdownOpen, - sendToIsInError, -} - -module.exports = selectors - -function getToDropdownOpen (state) { - return state.send.toDropdownOpen -} - -function sendToIsInError (state) { - return Boolean(state.send.errors.to) -} diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js deleted file mode 100644 index 6b90a9f09..000000000 --- a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js +++ /dev/null @@ -1,19 +0,0 @@ -const { - REQUIRED_ERROR, - INVALID_RECIPIENT_ADDRESS_ERROR, -} = require('../../send.constants') -const { isValidAddress } = require('../../../../util') - -function getToErrorObject (to, toError = null) { - if (!to) { - toError = REQUIRED_ERROR - } else if (!isValidAddress(to) && !toError) { - toError = INVALID_RECIPIENT_ADDRESS_ERROR - } - - return { to: toError } -} - -module.exports = { - getToErrorObject, -} diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js deleted file mode 100644 index 781371004..000000000 --- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js +++ /dev/null @@ -1,149 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -import proxyquire from 'proxyquire' - -const SendToRow = proxyquire('../send-to-row.component.js', { - './send-to-row.utils.js': { - getToErrorObject: (to, toError) => ({ - to: to === false ? null : `mockToErrorObject:${to}${toError}`, - }), - }, -}).default - -import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' -import EnsInput from '../../../../ens-input' - -const propsMethodSpies = { - closeToDropdown: sinon.spy(), - openToDropdown: sinon.spy(), - updateGas: sinon.spy(), - updateSendTo: sinon.spy(), - updateSendToError: sinon.spy(), -} - -sinon.spy(SendToRow.prototype, 'handleToChange') - -describe('SendToRow Component', function () { - let wrapper - let instance - - beforeEach(() => { - wrapper = shallow(, { context: { t: str => str + '_t' } }) - instance = wrapper.instance() - }) - - afterEach(() => { - propsMethodSpies.closeToDropdown.resetHistory() - propsMethodSpies.openToDropdown.resetHistory() - propsMethodSpies.updateSendTo.resetHistory() - propsMethodSpies.updateSendToError.resetHistory() - SendToRow.prototype.handleToChange.resetHistory() - }) - - describe('handleToChange', () => { - - it('should call updateSendTo', () => { - assert.equal(propsMethodSpies.updateSendTo.callCount, 0) - instance.handleToChange('mockTo2', 'mockNickname') - assert.equal(propsMethodSpies.updateSendTo.callCount, 1) - assert.deepEqual( - propsMethodSpies.updateSendTo.getCall(0).args, - ['mockTo2', 'mockNickname'] - ) - }) - - it('should call updateSendToError', () => { - assert.equal(propsMethodSpies.updateSendToError.callCount, 0) - instance.handleToChange('mockTo2', '', 'mockToError') - assert.equal(propsMethodSpies.updateSendToError.callCount, 1) - assert.deepEqual( - propsMethodSpies.updateSendToError.getCall(0).args, - [{ to: 'mockToErrorObject:mockTo2mockToError' }] - ) - }) - - it('should not call updateGas if there is a to error', () => { - assert.equal(propsMethodSpies.updateGas.callCount, 0) - instance.handleToChange('mockTo2') - assert.equal(propsMethodSpies.updateGas.callCount, 0) - }) - - it('should call updateGas if there is no to error', () => { - assert.equal(propsMethodSpies.updateGas.callCount, 0) - instance.handleToChange(false) - assert.equal(propsMethodSpies.updateGas.callCount, 1) - }) - }) - - describe('render', () => { - it('should render a SendRowWrapper component', () => { - assert.equal(wrapper.find(SendRowWrapper).length, 1) - }) - - it('should pass the correct props to SendRowWrapper', () => { - const { - errorType, - label, - showError, - } = wrapper.find(SendRowWrapper).props() - - assert.equal(errorType, 'to') - - assert.equal(label, 'to_t') - - assert.equal(showError, false) - }) - - it('should render an EnsInput as a child of the SendRowWrapper', () => { - assert(wrapper.find(SendRowWrapper).childAt(0).is(EnsInput)) - }) - - it('should render the EnsInput with the correct props', () => { - const { - accounts, - closeDropdown, - dropdownOpen, - inError, - name, - network, - onChange, - openDropdown, - placeholder, - to, - } = wrapper.find(SendRowWrapper).childAt(0).props() - assert.deepEqual(accounts, ['mockAccount']) - assert.equal(dropdownOpen, false) - assert.equal(inError, false) - assert.equal(name, 'address') - assert.equal(network, 'mockNetwork') - assert.equal(placeholder, 'recipientAddress_t') - assert.equal(to, 'mockTo') - assert.equal(propsMethodSpies.closeToDropdown.callCount, 0) - closeDropdown() - assert.equal(propsMethodSpies.closeToDropdown.callCount, 1) - assert.equal(propsMethodSpies.openToDropdown.callCount, 0) - openDropdown() - assert.equal(propsMethodSpies.openToDropdown.callCount, 1) - assert.equal(SendToRow.prototype.handleToChange.callCount, 0) - onChange({ toAddress: 'mockNewTo', nickname: 'mockNewNickname', toError: 'mockToError' }) - assert.equal(SendToRow.prototype.handleToChange.callCount, 1) - assert.deepEqual( - SendToRow.prototype.handleToChange.getCall(0).args, - ['mockNewTo', 'mockNewNickname', 'mockToError'] - ) - }) - }) -}) diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js deleted file mode 100644 index 92355c00a..000000000 --- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js +++ /dev/null @@ -1,113 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -let mapStateToProps -let mapDispatchToProps - -const actionSpies = { - updateSendTo: sinon.spy(), -} -const duckActionSpies = { - closeToDropdown: sinon.spy(), - openToDropdown: sinon.spy(), - updateSendErrors: sinon.spy(), -} - -proxyquire('../send-to-row.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - }, - '../../send.selectors.js': { - getCurrentNetwork: (s) => `mockNetwork:${s}`, - getSendTo: (s) => `mockTo:${s}`, - getSendToAccounts: (s) => `mockToAccounts:${s}`, - }, - './send-to-row.selectors.js': { - getToDropdownOpen: (s) => `mockToDropdownOpen:${s}`, - sendToIsInError: (s) => `mockInError:${s}`, - }, - '../../../../actions': actionSpies, - '../../../../ducks/send.duck': duckActionSpies, -}) - -describe('send-to-row container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - inError: 'mockInError:mockState', - network: 'mockNetwork:mockState', - to: 'mockTo:mockState', - toAccounts: 'mockToAccounts:mockState', - toDropdownOpen: 'mockToDropdownOpen:mockState', - }) - }) - - }) - - describe('mapDispatchToProps()', () => { - let dispatchSpy - let mapDispatchToPropsObject - - beforeEach(() => { - dispatchSpy = sinon.spy() - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) - }) - - describe('closeToDropdown()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.closeToDropdown() - assert(dispatchSpy.calledOnce) - assert(duckActionSpies.closeToDropdown.calledOnce) - assert.equal( - duckActionSpies.closeToDropdown.getCall(0).args[0], - undefined - ) - }) - }) - - describe('openToDropdown()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.openToDropdown() - assert(dispatchSpy.calledOnce) - assert(duckActionSpies.openToDropdown.calledOnce) - assert.equal( - duckActionSpies.openToDropdown.getCall(0).args[0], - undefined - ) - }) - }) - - describe('updateSendTo()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.updateSendTo('mockTo', 'mockNickname') - assert(dispatchSpy.calledOnce) - assert(actionSpies.updateSendTo.calledOnce) - assert.deepEqual( - actionSpies.updateSendTo.getCall(0).args, - ['mockTo', 'mockNickname'] - ) - }) - }) - - describe('updateSendToError()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.updateSendToError('mockToErrorObject') - assert(dispatchSpy.calledOnce) - assert(duckActionSpies.updateSendErrors.calledOnce) - assert.equal( - duckActionSpies.updateSendErrors.getCall(0).args[0], - 'mockToErrorObject' - ) - }) - }) - - }) - -}) diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js deleted file mode 100644 index 122ad3265..000000000 --- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import assert from 'assert' -import { - getToDropdownOpen, - sendToIsInError, -} from '../send-to-row.selectors.js' - -describe('send-to-row selectors', () => { - - describe('getToDropdownOpen()', () => { - it('should return send.getToDropdownOpen', () => { - const state = { - send: { - toDropdownOpen: false, - }, - } - - assert.equal(getToDropdownOpen(state), false) - }) - }) - - describe('sendToIsInError()', () => { - it('should return true if send.errors.to is truthy', () => { - const state = { - send: { - errors: { - to: 'abc', - }, - }, - } - - assert.equal(sendToIsInError(state), true) - }) - - it('should return false if send.errors.to is falsy', () => { - const state = { - send: { - errors: { - to: null, - }, - }, - } - - assert.equal(sendToIsInError(state), false) - }) - }) - -}) diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js deleted file mode 100644 index 4d2447c32..000000000 --- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -import { - REQUIRED_ERROR, - INVALID_RECIPIENT_ADDRESS_ERROR, -} from '../../../send.constants' - -const stubs = { - isValidAddress: sinon.stub().callsFake(to => Boolean(to.match(/^[0xabcdef123456798]+$/))), -} - -const toRowUtils = proxyquire('../send-to-row.utils.js', { - '../../../../util': { - isValidAddress: stubs.isValidAddress, - }, -}) -const { - getToErrorObject, -} = toRowUtils - -describe('send-to-row utils', () => { - - describe('getToErrorObject()', () => { - it('should return a required error if to is falsy', () => { - assert.deepEqual(getToErrorObject(null), { - to: REQUIRED_ERROR, - }) - }) - - it('should return an invalid recipient error if to is truthy but invalid', () => { - assert.deepEqual(getToErrorObject('mockInvalidTo'), { - to: INVALID_RECIPIENT_ADDRESS_ERROR, - }) - }) - - it('should return null if to is truthy and valid', () => { - assert.deepEqual(getToErrorObject('0xabc123'), { - to: null, - }) - }) - - it('should return the passed error if to is truthy but invalid if to is truthy and valid', () => { - assert.deepEqual(getToErrorObject('invalid #$ 345878', 'someExplicitError'), { - to: 'someExplicitError', - }) - }) - }) - -}) diff --git a/ui/app/components/send_/send-content/tests/send-content-component.test.js b/ui/app/components/send_/send-content/tests/send-content-component.test.js deleted file mode 100644 index d5bb6693c..000000000 --- a/ui/app/components/send_/send-content/tests/send-content-component.test.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import SendContent from '../send-content.component.js' - -import PageContainerContent from '../../../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' - -describe('SendContent Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow() - }) - - describe('render', () => { - it('should render a PageContainerContent component', () => { - assert.equal(wrapper.find(PageContainerContent).length, 1) - }) - - it('should render a div with a .send-v2__form class as a child of PageContainerContent', () => { - const PageContainerContentChild = wrapper.find(PageContainerContent).children() - PageContainerContentChild.is('div') - PageContainerContentChild.is('.send-v2__form') - }) - - it('should render the correct row components as grandchildren of the PageContainerContent component', () => { - 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)) - }) - }) -}) diff --git a/ui/app/components/send_/send-footer/README.md b/ui/app/components/send_/send-footer/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-footer/index.js b/ui/app/components/send_/send-footer/index.js deleted file mode 100644 index 58e91d622..000000000 --- a/ui/app/components/send_/send-footer/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './send-footer.container' diff --git a/ui/app/components/send_/send-footer/send-footer.component.js b/ui/app/components/send_/send-footer/send-footer.component.js deleted file mode 100644 index 2085f1dce..000000000 --- a/ui/app/components/send_/send-footer/send-footer.component.js +++ /dev/null @@ -1,101 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import PageContainerFooter from '../../page-container/page-container-footer' -import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../routes' - -export default class SendFooter extends Component { - - static propTypes = { - addToAddressBookIfNew: PropTypes.func, - amount: PropTypes.string, - clearSend: PropTypes.func, - disabled: PropTypes.bool, - editingTransactionId: PropTypes.string, - errors: PropTypes.object, - from: PropTypes.object, - gasLimit: PropTypes.string, - gasPrice: PropTypes.string, - gasTotal: PropTypes.string, - history: PropTypes.object, - inError: PropTypes.bool, - selectedToken: PropTypes.object, - sign: PropTypes.func, - to: PropTypes.string, - toAccounts: PropTypes.array, - tokenBalance: PropTypes.string, - unapprovedTxs: PropTypes.object, - update: PropTypes.func, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - onCancel () { - this.props.clearSend() - this.props.history.push(DEFAULT_ROUTE) - } - - onSubmit (event) { - event.preventDefault() - const { - addToAddressBookIfNew, - amount, - editingTransactionId, - from: {address: from}, - gasLimit: gas, - gasPrice, - selectedToken, - sign, - to, - unapprovedTxs, - // updateTx, - update, - toAccounts, - history, - } = this.props - - // Should not be needed because submit should be disabled if there are errors. - // const noErrors = !amountError && toError === null - - // if (!noErrors) { - // return - // } - - // TODO: add nickname functionality - addToAddressBookIfNew(to, toAccounts) - - const promise = editingTransactionId - ? update({ - amount, - editingTransactionId, - from, - gas, - gasPrice, - selectedToken, - to, - unapprovedTxs, - }) - : sign({ selectedToken, to, amount, from, gas, gasPrice }) - - Promise.resolve(promise) - .then(() => history.push(CONFIRM_TRANSACTION_ROUTE)) - } - - formShouldBeDisabled () { - const { inError, selectedToken, tokenBalance, gasTotal, to } = this.props - const missingTokenBalance = selectedToken && !tokenBalance - return inError || !gasTotal || missingTokenBalance || !to - } - - render () { - return ( - this.onCancel()} - onSubmit={e => this.onSubmit(e)} - disabled={this.formShouldBeDisabled()} - /> - ) - } - -} diff --git a/ui/app/components/send_/send-footer/send-footer.container.js b/ui/app/components/send_/send-footer/send-footer.container.js deleted file mode 100644 index 0af6fcfa1..000000000 --- a/ui/app/components/send_/send-footer/send-footer.container.js +++ /dev/null @@ -1,100 +0,0 @@ -import { connect } from 'react-redux' -import ethUtil from 'ethereumjs-util' -import { - addToAddressBook, - clearSend, - signTokenTx, - signTx, - updateTransaction, -} from '../../../actions' -import SendFooter from './send-footer.component' -import { - getGasLimit, - getGasPrice, - getGasTotal, - getSelectedToken, - getSendAmount, - getSendEditingTransactionId, - getSendFromObject, - getSendTo, - getSendToAccounts, - getTokenBalance, - getUnapprovedTxs, -} from '../send.selectors' -import { - isSendFormInError, -} from './send-footer.selectors' -import { - addressIsNew, - constructTxParams, - constructUpdatedTx, -} from './send-footer.utils' - -export default connect(mapStateToProps, mapDispatchToProps)(SendFooter) - -function mapStateToProps (state) { - return { - amount: getSendAmount(state), - editingTransactionId: getSendEditingTransactionId(state), - from: getSendFromObject(state), - gasLimit: getGasLimit(state), - gasPrice: getGasPrice(state), - gasTotal: getGasTotal(state), - inError: isSendFormInError(state), - selectedToken: getSelectedToken(state), - to: getSendTo(state), - toAccounts: getSendToAccounts(state), - tokenBalance: getTokenBalance(state), - unapprovedTxs: getUnapprovedTxs(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - clearSend: () => dispatch(clearSend()), - sign: ({ selectedToken, to, amount, from, gas, gasPrice }) => { - const txParams = constructTxParams({ - amount, - from, - gas, - gasPrice, - selectedToken, - to, - }) - - selectedToken - ? dispatch(signTokenTx(selectedToken.address, to, amount, txParams)) - : dispatch(signTx(txParams)) - }, - update: ({ - amount, - editingTransactionId, - from, - gas, - gasPrice, - selectedToken, - to, - unapprovedTxs, - }) => { - const editingTx = constructUpdatedTx({ - amount, - editingTransactionId, - from, - gas, - gasPrice, - selectedToken, - to, - unapprovedTxs, - }) - - return dispatch(updateTransaction(editingTx)) - }, - addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => { - const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress) - if (addressIsNew(toAccounts)) { - // TODO: nickname, i.e. addToAddressBook(recipient, nickname) - dispatch(addToAddressBook(hexPrefixedAddress, nickname)) - } - }, - } -} diff --git a/ui/app/components/send_/send-footer/send-footer.scss b/ui/app/components/send_/send-footer/send-footer.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-footer/send-footer.selectors.js b/ui/app/components/send_/send-footer/send-footer.selectors.js deleted file mode 100644 index e20addfdc..000000000 --- a/ui/app/components/send_/send-footer/send-footer.selectors.js +++ /dev/null @@ -1,11 +0,0 @@ -const { getSendErrors } = require('../send.selectors') - -const selectors = { - isSendFormInError, -} - -module.exports = selectors - -function isSendFormInError (state) { - return Object.values(getSendErrors(state)).some(n => n) -} diff --git a/ui/app/components/send_/send-footer/send-footer.utils.js b/ui/app/components/send_/send-footer/send-footer.utils.js deleted file mode 100644 index 875e7d948..000000000 --- a/ui/app/components/send_/send-footer/send-footer.utils.js +++ /dev/null @@ -1,81 +0,0 @@ -const ethAbi = require('ethereumjs-abi') -const ethUtil = require('ethereumjs-util') -const { TOKEN_TRANSFER_FUNCTION_SIGNATURE } = require('../send.constants') - -function addHexPrefixToObjectValues (obj) { - return Object.keys(obj).reduce((newObj, key) => { - return { ...newObj, [key]: ethUtil.addHexPrefix(obj[key]) } - }, {}) -} - -function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice }) { - const txParams = { - from, - value: '0', - gas, - gasPrice, - } - - if (!selectedToken) { - txParams.value = amount - txParams.to = to - } - - const hexPrefixedTxParams = addHexPrefixToObjectValues(txParams) - - return hexPrefixedTxParams -} - -function constructUpdatedTx ({ - amount, - editingTransactionId, - from, - gas, - gasPrice, - selectedToken, - to, - unapprovedTxs, -}) { - const editingTx = { - ...unapprovedTxs[editingTransactionId], - txParams: addHexPrefixToObjectValues({ from, gas, gasPrice }), - } - - if (selectedToken) { - const data = TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( - ethAbi.rawEncode(['address', 'uint256'], [to, ethUtil.addHexPrefix(amount)]), - x => ('00' + x.toString(16)).slice(-2) - ).join('') - - Object.assign(editingTx.txParams, addHexPrefixToObjectValues({ - value: '0', - to: selectedToken.address, - data, - })) - } else { - const { data } = unapprovedTxs[editingTransactionId].txParams - - Object.assign(editingTx.txParams, addHexPrefixToObjectValues({ - value: amount, - to, - data, - })) - - if (typeof editingTx.txParams.data === 'undefined') { - delete editingTx.txParams.data - } - } - - return editingTx -} - -function addressIsNew (toAccounts, newAddress) { - return !toAccounts.find(({ address }) => newAddress === address) -} - -module.exports = { - addressIsNew, - constructTxParams, - constructUpdatedTx, - addHexPrefixToObjectValues, -} diff --git a/ui/app/components/send_/send-footer/tests/send-footer-component.test.js b/ui/app/components/send_/send-footer/tests/send-footer-component.test.js deleted file mode 100644 index 4b2cd327d..000000000 --- a/ui/app/components/send_/send-footer/tests/send-footer-component.test.js +++ /dev/null @@ -1,230 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../routes' -import SendFooter from '../send-footer.component.js' - -import PageContainerFooter from '../../../page-container/page-container-footer' - -const propsMethodSpies = { - addToAddressBookIfNew: sinon.spy(), - clearSend: sinon.spy(), - sign: sinon.spy(), - update: sinon.spy(), -} -const historySpies = { - push: sinon.spy(), -} -const MOCK_EVENT = { preventDefault: () => {} } - -sinon.spy(SendFooter.prototype, 'onCancel') -sinon.spy(SendFooter.prototype, 'onSubmit') - -describe('SendFooter Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow(, { context: { t: str => str } }) - }) - - afterEach(() => { - propsMethodSpies.clearSend.resetHistory() - propsMethodSpies.addToAddressBookIfNew.resetHistory() - propsMethodSpies.clearSend.resetHistory() - propsMethodSpies.sign.resetHistory() - propsMethodSpies.update.resetHistory() - historySpies.push.resetHistory() - SendFooter.prototype.onCancel.resetHistory() - SendFooter.prototype.onSubmit.resetHistory() - }) - - describe('onCancel', () => { - it('should call clearSend', () => { - assert.equal(propsMethodSpies.clearSend.callCount, 0) - wrapper.instance().onCancel() - assert.equal(propsMethodSpies.clearSend.callCount, 1) - }) - - it('should call history.push', () => { - assert.equal(historySpies.push.callCount, 0) - wrapper.instance().onCancel() - assert.equal(historySpies.push.callCount, 1) - assert.equal(historySpies.push.getCall(0).args[0], DEFAULT_ROUTE) - }) - }) - - - describe('formShouldBeDisabled()', () => { - const config = { - 'should return true if inError is truthy': { - inError: true, - expectedResult: true, - }, - 'should return true if gasTotal is falsy': { - inError: false, - gasTotal: false, - expectedResult: true, - }, - 'should return true if to is truthy': { - to: '0xsomevalidAddress', - inError: false, - gasTotal: false, - expectedResult: true, - }, - 'should return true if selectedToken is truthy and tokenBalance is falsy': { - selectedToken: true, - tokenBalance: null, - expectedResult: true, - }, - 'should return false if inError is false and all other params are truthy': { - inError: false, - gasTotal: '0x123', - selectedToken: true, - tokenBalance: 123, - expectedResult: false, - }, - } - Object.entries(config).map(([description, obj]) => { - it(description, () => { - wrapper.setProps(obj) - assert.equal(wrapper.instance().formShouldBeDisabled(), obj.expectedResult) - }) - }) - }) - - describe('onSubmit', () => { - it('should call addToAddressBookIfNew with the correct params', () => { - wrapper.instance().onSubmit(MOCK_EVENT) - assert(propsMethodSpies.addToAddressBookIfNew.calledOnce) - assert.deepEqual( - propsMethodSpies.addToAddressBookIfNew.getCall(0).args, - ['mockTo', ['mockAccount']] - ) - }) - - it('should call props.update if editingTransactionId is truthy', () => { - wrapper.instance().onSubmit(MOCK_EVENT) - assert(propsMethodSpies.update.calledOnce) - assert.deepEqual( - propsMethodSpies.update.getCall(0).args[0], - { - amount: 'mockAmount', - editingTransactionId: 'mockEditingTransactionId', - from: 'mockAddress', - gas: 'mockGasLimit', - gasPrice: 'mockGasPrice', - selectedToken: { mockProp: 'mockSelectedTokenProp' }, - to: 'mockTo', - unapprovedTxs: ['mockTx'], - } - ) - }) - - it('should not call props.sign if editingTransactionId is truthy', () => { - assert.equal(propsMethodSpies.sign.callCount, 0) - }) - - it('should call props.sign if editingTransactionId is falsy', () => { - wrapper.setProps({ editingTransactionId: null }) - wrapper.instance().onSubmit(MOCK_EVENT) - assert(propsMethodSpies.sign.calledOnce) - assert.deepEqual( - propsMethodSpies.sign.getCall(0).args[0], - { - amount: 'mockAmount', - from: 'mockAddress', - gas: 'mockGasLimit', - gasPrice: 'mockGasPrice', - selectedToken: { mockProp: 'mockSelectedTokenProp' }, - to: 'mockTo', - } - ) - }) - - it('should not call props.update if editingTransactionId is falsy', () => { - assert.equal(propsMethodSpies.update.callCount, 0) - }) - - it('should call history.push', done => { - Promise.resolve(wrapper.instance().onSubmit(MOCK_EVENT)) - .then(() => { - assert.equal(historySpies.push.callCount, 1) - assert.equal(historySpies.push.getCall(0).args[0], CONFIRM_TRANSACTION_ROUTE) - done() - }) - }) - }) - - describe('render', () => { - beforeEach(() => { - sinon.stub(SendFooter.prototype, 'formShouldBeDisabled').returns('formShouldBeDisabledReturn') - wrapper = shallow(, { context: { t: str => str } }) - }) - - afterEach(() => { - SendFooter.prototype.formShouldBeDisabled.restore() - }) - - it('should render a PageContainerFooter component', () => { - assert.equal(wrapper.find(PageContainerFooter).length, 1) - }) - - it('should pass the correct props to PageContainerFooter', () => { - const { - onCancel, - onSubmit, - disabled, - } = wrapper.find(PageContainerFooter).props() - assert.equal(disabled, 'formShouldBeDisabledReturn') - - assert.equal(SendFooter.prototype.onSubmit.callCount, 0) - onSubmit(MOCK_EVENT) - assert.equal(SendFooter.prototype.onSubmit.callCount, 1) - - assert.equal(SendFooter.prototype.onCancel.callCount, 0) - onCancel() - assert.equal(SendFooter.prototype.onCancel.callCount, 1) - }) - }) -}) diff --git a/ui/app/components/send_/send-footer/tests/send-footer-container.test.js b/ui/app/components/send_/send-footer/tests/send-footer-container.test.js deleted file mode 100644 index 39d6a7686..000000000 --- a/ui/app/components/send_/send-footer/tests/send-footer-container.test.js +++ /dev/null @@ -1,191 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -let mapStateToProps -let mapDispatchToProps - -const actionSpies = { - addToAddressBook: sinon.spy(), - clearSend: sinon.spy(), - signTokenTx: sinon.spy(), - signTx: sinon.spy(), - updateTransaction: sinon.spy(), -} -const utilsStubs = { - addressIsNew: sinon.stub().returns(true), - constructTxParams: sinon.stub().returns('mockConstructedTxParams'), - constructUpdatedTx: sinon.stub().returns('mockConstructedUpdatedTxParams'), -} - -proxyquire('../send-footer.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - }, - '../../../actions': actionSpies, - '../send.selectors': { - getGasLimit: (s) => `mockGasLimit:${s}`, - getGasPrice: (s) => `mockGasPrice:${s}`, - getGasTotal: (s) => `mockGasTotal:${s}`, - getSelectedToken: (s) => `mockSelectedToken:${s}`, - getSendAmount: (s) => `mockAmount:${s}`, - getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, - getSendFromObject: (s) => `mockFromObject:${s}`, - getSendTo: (s) => `mockTo:${s}`, - getSendToAccounts: (s) => `mockToAccounts:${s}`, - getTokenBalance: (s) => `mockTokenBalance:${s}`, - getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`, - }, - './send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` }, - './send-footer.utils': utilsStubs, -}) - -describe('send-footer container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - amount: 'mockAmount:mockState', - selectedToken: 'mockSelectedToken:mockState', - editingTransactionId: 'mockEditingTransactionId:mockState', - from: 'mockFromObject:mockState', - gasLimit: 'mockGasLimit:mockState', - gasPrice: 'mockGasPrice:mockState', - gasTotal: 'mockGasTotal:mockState', - inError: 'mockInError:mockState', - to: 'mockTo:mockState', - toAccounts: 'mockToAccounts:mockState', - tokenBalance: 'mockTokenBalance:mockState', - unapprovedTxs: 'mockUnapprovedTxs:mockState', - }) - }) - - }) - - describe('mapDispatchToProps()', () => { - let dispatchSpy - let mapDispatchToPropsObject - - beforeEach(() => { - dispatchSpy = sinon.spy() - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) - }) - - describe('clearSend()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.clearSend() - assert(dispatchSpy.calledOnce) - assert(actionSpies.clearSend.calledOnce) - }) - }) - - describe('sign()', () => { - it('should dispatch a signTokenTx action if selectedToken is defined', () => { - mapDispatchToPropsObject.sign({ - selectedToken: { - address: '0xabc', - }, - to: 'mockTo', - amount: 'mockAmount', - from: 'mockFrom', - gas: 'mockGas', - gasPrice: 'mockGasPrice', - }) - assert(dispatchSpy.calledOnce) - assert.deepEqual( - utilsStubs.constructTxParams.getCall(0).args[0], - { - selectedToken: { - address: '0xabc', - }, - to: 'mockTo', - amount: 'mockAmount', - from: 'mockFrom', - gas: 'mockGas', - gasPrice: 'mockGasPrice', - } - ) - assert.deepEqual( - actionSpies.signTokenTx.getCall(0).args, - [ '0xabc', 'mockTo', 'mockAmount', 'mockConstructedTxParams' ] - ) - }) - - it('should dispatch a sign action if selectedToken is not defined', () => { - utilsStubs.constructTxParams.resetHistory() - mapDispatchToPropsObject.sign({ - to: 'mockTo', - amount: 'mockAmount', - from: 'mockFrom', - gas: 'mockGas', - gasPrice: 'mockGasPrice', - }) - assert(dispatchSpy.calledOnce) - assert.deepEqual( - utilsStubs.constructTxParams.getCall(0).args[0], - { - selectedToken: undefined, - to: 'mockTo', - amount: 'mockAmount', - from: 'mockFrom', - gas: 'mockGas', - gasPrice: 'mockGasPrice', - } - ) - assert.deepEqual( - actionSpies.signTx.getCall(0).args, - [ 'mockConstructedTxParams' ] - ) - }) - }) - - describe('update()', () => { - it('should dispatch an updateTransaction action', () => { - mapDispatchToPropsObject.update({ - to: 'mockTo', - amount: 'mockAmount', - from: 'mockFrom', - gas: 'mockGas', - gasPrice: 'mockGasPrice', - editingTransactionId: 'mockEditingTransactionId', - selectedToken: 'mockSelectedToken', - unapprovedTxs: 'mockUnapprovedTxs', - }) - assert(dispatchSpy.calledOnce) - assert.deepEqual( - utilsStubs.constructUpdatedTx.getCall(0).args[0], - { - to: 'mockTo', - amount: 'mockAmount', - from: 'mockFrom', - gas: 'mockGas', - gasPrice: 'mockGasPrice', - editingTransactionId: 'mockEditingTransactionId', - selectedToken: 'mockSelectedToken', - unapprovedTxs: 'mockUnapprovedTxs', - } - ) - assert.equal(actionSpies.updateTransaction.getCall(0).args[0], 'mockConstructedUpdatedTxParams') - }) - }) - - describe('addToAddressBookIfNew()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.addToAddressBookIfNew('mockNewAddress', 'mockToAccounts', 'mockNickname') - assert(dispatchSpy.calledOnce) - assert.equal(utilsStubs.addressIsNew.getCall(0).args[0], 'mockToAccounts') - assert.deepEqual( - actionSpies.addToAddressBook.getCall(0).args, - [ '0xmockNewAddress', 'mockNickname' ] - ) - }) - }) - - }) - -}) diff --git a/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js b/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js deleted file mode 100644 index 8de032f57..000000000 --- a/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' - -const { - isSendFormInError, -} = proxyquire('../send-footer.selectors', { - '../send.selectors': { - getSendErrors: (mockState) => mockState.errors, - }, -}) - -describe('send-footer selectors', () => { - - describe('getTitleKey()', () => { - it('should return true if any of the values of the object returned by getSendErrors are truthy', () => { - assert.equal(isSendFormInError({ errors: { a: 'abc', b: false} }), true) - }) - - it('should return false if all of the values of the object returned by getSendErrors are falsy', () => { - assert.equal(isSendFormInError({ errors: { a: false, b: null} }), false) - }) - }) - -}) diff --git a/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js b/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js deleted file mode 100644 index 2d3135995..000000000 --- a/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js +++ /dev/null @@ -1,210 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' -const { TOKEN_TRANSFER_FUNCTION_SIGNATURE } = require('../../send.constants') - -const stubs = { - rawEncode: sinon.stub().callsFake((arr1, arr2) => { - return [ ...arr1, ...arr2 ] - }), -} - -const sendUtils = proxyquire('../send-footer.utils.js', { - 'ethereumjs-abi': { - rawEncode: stubs.rawEncode, - }, -}) -const { - addressIsNew, - constructTxParams, - constructUpdatedTx, - addHexPrefixToObjectValues, -} = sendUtils - -describe('send-footer utils', () => { - - describe('addHexPrefixToObjectValues()', () => { - it('should return a new object with the same properties with a 0x prefix', () => { - assert.deepEqual( - addHexPrefixToObjectValues({ - prop1: '0x123', - prop2: '456', - prop3: 'x', - }), - { - prop1: '0x123', - prop2: '0x456', - prop3: '0xx', - } - ) - }) - }) - - describe('addressIsNew()', () => { - it('should return false if the address exists in toAccounts', () => { - assert.equal( - addressIsNew([ - { address: '0xabc' }, - { address: '0xdef' }, - { address: '0xghi' }, - ], '0xdef'), - false - ) - }) - - it('should return true if the address does not exists in toAccounts', () => { - assert.equal( - addressIsNew([ - { address: '0xabc' }, - { address: '0xdef' }, - { address: '0xghi' }, - ], '0xxyz'), - true - ) - }) - }) - - describe('constructTxParams()', () => { - it('should return a new txParams object with value and to properties if there is no selectedToken', () => { - assert.deepEqual( - constructTxParams({ - selectedToken: false, - to: 'mockTo', - amount: 'mockAmount', - from: 'mockFrom', - gas: 'mockGas', - gasPrice: 'mockGasPrice', - }), - { - to: '0xmockTo', - value: '0xmockAmount', - from: '0xmockFrom', - gas: '0xmockGas', - gasPrice: '0xmockGasPrice', - } - ) - }) - - it('should return a new txParams object without a to property and a 0 value if there is a selectedToken', () => { - assert.deepEqual( - constructTxParams({ - selectedToken: true, - to: 'mockTo', - amount: 'mockAmount', - from: 'mockFrom', - gas: 'mockGas', - gasPrice: 'mockGasPrice', - }), - { - value: '0x0', - from: '0xmockFrom', - gas: '0xmockGas', - gasPrice: '0xmockGasPrice', - } - ) - }) - }) - - describe('constructUpdatedTx()', () => { - it('should return a new object with an updated txParams', () => { - const result = constructUpdatedTx({ - amount: 'mockAmount', - editingTransactionId: '0x456', - from: 'mockFrom', - gas: 'mockGas', - gasPrice: 'mockGasPrice', - selectedToken: false, - to: 'mockTo', - unapprovedTxs: { - '0x123': {}, - '0x456': { - unapprovedTxParam: 'someOtherParam', - txParams: { - data: 'someData', - }, - }, - }, - }) - - assert.deepEqual(result, { - unapprovedTxParam: 'someOtherParam', - txParams: { - from: '0xmockFrom', - gas: '0xmockGas', - gasPrice: '0xmockGasPrice', - value: '0xmockAmount', - to: '0xmockTo', - data: '0xsomeData', - }, - }) - }) - - it('should not have data property if there is non in the original tx', () => { - const result = constructUpdatedTx({ - amount: 'mockAmount', - editingTransactionId: '0x456', - from: 'mockFrom', - gas: 'mockGas', - gasPrice: 'mockGasPrice', - selectedToken: false, - to: 'mockTo', - unapprovedTxs: { - '0x123': {}, - '0x456': { - unapprovedTxParam: 'someOtherParam', - txParams: { - from: 'oldFrom', - gas: 'oldGas', - gasPrice: 'oldGasPrice', - }, - }, - }, - }) - - assert.deepEqual(result, { - unapprovedTxParam: 'someOtherParam', - txParams: { - from: '0xmockFrom', - gas: '0xmockGas', - gasPrice: '0xmockGasPrice', - value: '0xmockAmount', - to: '0xmockTo', - }, - }) - }) - - it('should have token property values if selectedToken is truthy', () => { - const result = constructUpdatedTx({ - amount: 'mockAmount', - editingTransactionId: '0x456', - from: 'mockFrom', - gas: 'mockGas', - gasPrice: 'mockGasPrice', - selectedToken: { - address: 'mockTokenAddress', - }, - to: 'mockTo', - unapprovedTxs: { - '0x123': {}, - '0x456': { - unapprovedTxParam: 'someOtherParam', - txParams: {}, - }, - }, - }) - - assert.deepEqual(result, { - unapprovedTxParam: 'someOtherParam', - txParams: { - from: '0xmockFrom', - gas: '0xmockGas', - gasPrice: '0xmockGasPrice', - value: '0x0', - to: '0xmockTokenAddress', - data: `${TOKEN_TRANSFER_FUNCTION_SIGNATURE}ss56Tont`, - }, - }) - }) - }) - -}) diff --git a/ui/app/components/send_/send-header/README.md b/ui/app/components/send_/send-header/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send-header/index.js b/ui/app/components/send_/send-header/index.js deleted file mode 100644 index 0b17f0b7d..000000000 --- a/ui/app/components/send_/send-header/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './send-header.container' diff --git a/ui/app/components/send_/send-header/send-header.component.js b/ui/app/components/send_/send-header/send-header.component.js deleted file mode 100644 index efc4bbf27..000000000 --- a/ui/app/components/send_/send-header/send-header.component.js +++ /dev/null @@ -1,34 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import PageContainerHeader from '../../page-container/page-container-header' -import { DEFAULT_ROUTE } from '../../../routes' - -export default class SendHeader extends Component { - - static propTypes = { - clearSend: PropTypes.func, - history: PropTypes.object, - titleKey: PropTypes.string, - subtitleParams: PropTypes.array, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - onClose () { - this.props.clearSend() - this.props.history.push(DEFAULT_ROUTE) - } - - render () { - return ( - this.onClose()} - subtitle={this.context.t(...this.props.subtitleParams)} - title={this.context.t(this.props.titleKey)} - /> - ) - } - -} diff --git a/ui/app/components/send_/send-header/send-header.container.js b/ui/app/components/send_/send-header/send-header.container.js deleted file mode 100644 index 4bcd0d1b6..000000000 --- a/ui/app/components/send_/send-header/send-header.container.js +++ /dev/null @@ -1,19 +0,0 @@ -import { connect } from 'react-redux' -import { clearSend } from '../../../actions' -import SendHeader from './send-header.component' -import { getSubtitleParams, getTitleKey } from './send-header.selectors' - -export default connect(mapStateToProps, mapDispatchToProps)(SendHeader) - -function mapStateToProps (state) { - return { - titleKey: getTitleKey(state), - subtitleParams: getSubtitleParams(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - clearSend: () => dispatch(clearSend()), - } -} diff --git a/ui/app/components/send_/send-header/send-header.selectors.js b/ui/app/components/send_/send-header/send-header.selectors.js deleted file mode 100644 index d7c9d3766..000000000 --- a/ui/app/components/send_/send-header/send-header.selectors.js +++ /dev/null @@ -1,37 +0,0 @@ -const { - getSelectedToken, - getSendEditingTransactionId, -} = require('../send.selectors.js') - -const selectors = { - getTitleKey, - getSubtitleParams, -} - -module.exports = selectors - -function getTitleKey (state) { - const isEditing = Boolean(getSendEditingTransactionId(state)) - const isToken = Boolean(getSelectedToken(state)) - - if (isEditing) { - return 'edit' - } else if (isToken) { - return 'sendTokens' - } else { - return 'sendETH' - } -} - -function getSubtitleParams (state) { - const isEditing = Boolean(getSendEditingTransactionId(state)) - const token = getSelectedToken(state) - - if (isEditing) { - return [ 'editingTransaction' ] - } else if (token) { - return [ 'onlySendTokensToAccountAddress', [ token.symbol ] ] - } else { - return [ 'onlySendToEtherAddress' ] - } -} diff --git a/ui/app/components/send_/send-header/tests/send-header-component.test.js b/ui/app/components/send_/send-header/tests/send-header-component.test.js deleted file mode 100644 index 930bfa387..000000000 --- a/ui/app/components/send_/send-header/tests/send-header-component.test.js +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -import { DEFAULT_ROUTE } from '../../../../routes' -import SendHeader from '../send-header.component.js' - -import PageContainerHeader from '../../../page-container/page-container-header' - -const propsMethodSpies = { - clearSend: sinon.spy(), -} -const historySpies = { - push: sinon.spy(), -} - -sinon.spy(SendHeader.prototype, 'onClose') - -describe('SendHeader Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow(, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) - }) - - afterEach(() => { - propsMethodSpies.clearSend.resetHistory() - historySpies.push.resetHistory() - SendHeader.prototype.onClose.resetHistory() - }) - - describe('onClose', () => { - it('should call clearSend', () => { - assert.equal(propsMethodSpies.clearSend.callCount, 0) - wrapper.instance().onClose() - assert.equal(propsMethodSpies.clearSend.callCount, 1) - }) - - it('should call history.push', () => { - assert.equal(historySpies.push.callCount, 0) - wrapper.instance().onClose() - assert.equal(historySpies.push.callCount, 1) - assert.equal(historySpies.push.getCall(0).args[0], DEFAULT_ROUTE) - }) - }) - - describe('render', () => { - it('should render a PageContainerHeader compenent', () => { - assert.equal(wrapper.find(PageContainerHeader).length, 1) - }) - - 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() - assert.equal(SendHeader.prototype.onClose.callCount, 1) - }) - }) -}) diff --git a/ui/app/components/send_/send-header/tests/send-header-container.test.js b/ui/app/components/send_/send-header/tests/send-header-container.test.js deleted file mode 100644 index 41a7e8a89..000000000 --- a/ui/app/components/send_/send-header/tests/send-header-container.test.js +++ /dev/null @@ -1,59 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -let mapStateToProps -let mapDispatchToProps - -const actionSpies = { - clearSend: sinon.spy(), -} - -proxyquire('../send-header.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - }, - '../../../actions': actionSpies, - './send-header.selectors': { - getTitleKey: (s) => `mockTitleKey:${s}`, - getSubtitleParams: (s) => `mockSubtitleParams:${s}`, - }, -}) - -describe('send-header container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - titleKey: 'mockTitleKey:mockState', - subtitleParams: 'mockSubtitleParams:mockState', - }) - }) - - }) - - describe('mapDispatchToProps()', () => { - let dispatchSpy - let mapDispatchToPropsObject - - beforeEach(() => { - dispatchSpy = sinon.spy() - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) - }) - - describe('clearSend()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.clearSend() - assert(dispatchSpy.calledOnce) - assert(actionSpies.clearSend.calledOnce) - }) - }) - - }) - -}) diff --git a/ui/app/components/send_/send-header/tests/send-header-selectors.test.js b/ui/app/components/send_/send-header/tests/send-header-selectors.test.js deleted file mode 100644 index e0c6a3ab3..000000000 --- a/ui/app/components/send_/send-header/tests/send-header-selectors.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' - -const { - getTitleKey, - getSubtitleParams, -} = proxyquire('../send-header.selectors', { - '../send.selectors': { - getSelectedToken: (mockState) => mockState.t, - getSendEditingTransactionId: (mockState) => mockState.e, - }, -}) - -describe('send-header selectors', () => { - - describe('getTitleKey()', () => { - it('should return the correct key when getSendEditingTransactionId is truthy', () => { - assert.equal(getTitleKey({ e: 1, t: true }), 'edit') - }) - - it('should return the correct key when getSendEditingTransactionId is falsy and getSelectedToken is truthy', () => { - assert.equal(getTitleKey({ e: null, t: 'abc' }), 'sendTokens') - }) - - it('should return the correct key when getSendEditingTransactionId is falsy and getSelectedToken is falsy', () => { - assert.equal(getTitleKey({ e: null }), 'sendETH') - }) - }) - - describe('getSubtitleParams()', () => { - it('should return the correct params when getSendEditingTransactionId is truthy', () => { - assert.deepEqual(getSubtitleParams({ e: 1, t: true }), [ 'editingTransaction' ]) - }) - - it('should return the correct params when getSendEditingTransactionId is falsy and getSelectedToken is truthy', () => { - assert.deepEqual( - getSubtitleParams({ e: null, t: { symbol: 'ABC' } }), - [ 'onlySendTokensToAccountAddress', [ 'ABC' ] ] - ) - }) - - it('should return the correct params when getSendEditingTransactionId is falsy and getSelectedToken is falsy', () => { - assert.deepEqual(getSubtitleParams({ e: null }), [ 'onlySendToEtherAddress' ]) - }) - }) - -}) diff --git a/ui/app/components/send_/send.component.js b/ui/app/components/send_/send.component.js deleted file mode 100644 index 6f1b20c55..000000000 --- a/ui/app/components/send_/send.component.js +++ /dev/null @@ -1,179 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import PersistentForm from '../../../lib/persistent-form' -import { - getAmountErrorObject, - getGasFeeErrorObject, - getToAddressForGasUpdate, - doesAmountErrorRequireUpdate, -} from './send.utils' - -import SendHeader from './send-header/' -import SendContent from './send-content/' -import SendFooter from './send-footer/' - -export default class SendTransactionScreen extends PersistentForm { - - static propTypes = { - amount: PropTypes.string, - amountConversionRate: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), - blockGasLimit: PropTypes.string, - conversionRate: PropTypes.number, - editingTransactionId: PropTypes.string, - from: PropTypes.object, - gasLimit: PropTypes.string, - gasPrice: PropTypes.string, - gasTotal: PropTypes.string, - history: PropTypes.object, - network: PropTypes.string, - primaryCurrency: PropTypes.string, - recentBlocks: PropTypes.array, - selectedAddress: PropTypes.string, - selectedToken: PropTypes.object, - tokenBalance: PropTypes.string, - tokenContract: PropTypes.object, - updateAndSetGasTotal: PropTypes.func, - updateSendErrors: PropTypes.func, - updateSendTokenBalance: PropTypes.func, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - updateGas ({ to: updatedToAddress, amount: value } = {}) { - const { - amount, - blockGasLimit, - editingTransactionId, - gasLimit, - gasPrice, - recentBlocks, - selectedAddress, - selectedToken = {}, - to: currentToAddress, - updateAndSetGasTotal, - } = this.props - - updateAndSetGasTotal({ - blockGasLimit, - editingTransactionId, - gasLimit, - gasPrice, - recentBlocks, - selectedAddress, - selectedToken, - to: getToAddressForGasUpdate(updatedToAddress, currentToAddress), - value: value || amount, - }) - } - - componentDidUpdate (prevProps) { - const { - amount, - amountConversionRate, - conversionRate, - from: { address, balance }, - gasTotal, - network, - primaryCurrency, - selectedToken, - tokenBalance, - updateSendErrors, - updateSendTokenBalance, - tokenContract, - } = this.props - - const { - from: { balance: prevBalance }, - gasTotal: prevGasTotal, - tokenBalance: prevTokenBalance, - network: prevNetwork, - } = prevProps - - const uninitialized = [prevBalance, prevGasTotal].every(n => n === null) - - const amountErrorRequiresUpdate = doesAmountErrorRequireUpdate({ - balance, - gasTotal, - prevBalance, - prevGasTotal, - prevTokenBalance, - selectedToken, - tokenBalance, - }) - - if (amountErrorRequiresUpdate) { - const amountErrorObject = getAmountErrorObject({ - amount, - amountConversionRate, - balance, - conversionRate, - gasTotal, - primaryCurrency, - selectedToken, - tokenBalance, - }) - const gasFeeErrorObject = selectedToken - ? getGasFeeErrorObject({ - amount, - amountConversionRate, - balance, - conversionRate, - gasTotal, - primaryCurrency, - selectedToken, - tokenBalance, - }) - : { gasFee: null } - updateSendErrors(Object.assign(amountErrorObject, gasFeeErrorObject)) - } - - if (!uninitialized) { - - if (network !== prevNetwork && network !== 'loading') { - updateSendTokenBalance({ - selectedToken, - tokenContract, - address, - }) - this.updateGas() - } - } - } - - componentWillMount () { - const { - from: { address }, - selectedToken, - tokenContract, - updateSendTokenBalance, - } = this.props - updateSendTokenBalance({ - selectedToken, - tokenContract, - address, - }) - this.updateGas() - } - - componentWillUnmount () { - this.props.resetSendState() - } - - render () { - const { history } = this.props - - return ( -
- - this.updateGas(updateData)}/> - -
- ) - } - -} diff --git a/ui/app/components/send_/send.constants.js b/ui/app/components/send_/send.constants.js deleted file mode 100644 index 8acdf0641..000000000 --- a/ui/app/components/send_/send.constants.js +++ /dev/null @@ -1,57 +0,0 @@ -const ethUtil = require('ethereumjs-util') -const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') - -const MIN_GAS_PRICE_DEC = '0' -const MIN_GAS_PRICE_HEX = (parseInt(MIN_GAS_PRICE_DEC)).toString(16) -const MIN_GAS_LIMIT_DEC = '21000' -const MIN_GAS_LIMIT_HEX = (parseInt(MIN_GAS_LIMIT_DEC)).toString(16) - -const MIN_GAS_PRICE_GWEI = ethUtil.addHexPrefix(conversionUtil(MIN_GAS_PRICE_HEX, { - fromDenomination: 'WEI', - toDenomination: 'GWEI', - fromNumericBase: 'hex', - toNumericBase: 'hex', - numberOfDecimals: 1, -})) - -const MIN_GAS_TOTAL = multiplyCurrencies(MIN_GAS_LIMIT_HEX, MIN_GAS_PRICE_HEX, { - toNumericBase: 'hex', - multiplicandBase: 16, - multiplierBase: 16, -}) - -const TOKEN_TRANSFER_FUNCTION_SIGNATURE = '0xa9059cbb' - -const INSUFFICIENT_FUNDS_ERROR = 'insufficientFunds' -const INSUFFICIENT_TOKENS_ERROR = 'insufficientTokens' -const NEGATIVE_ETH_ERROR = 'negativeETH' -const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient' -const REQUIRED_ERROR = 'required' - -const ONE_GWEI_IN_WEI_HEX = ethUtil.addHexPrefix(conversionUtil('0x1', { - fromDenomination: 'GWEI', - toDenomination: 'WEI', - fromNumericBase: 'hex', - toNumericBase: 'hex', -})) - -const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. -const BASE_TOKEN_GAS_COST = '0x186a0' // Hex for 100000, a base estimate for token transfers. - -module.exports = { - INSUFFICIENT_FUNDS_ERROR, - INSUFFICIENT_TOKENS_ERROR, - INVALID_RECIPIENT_ADDRESS_ERROR, - MIN_GAS_LIMIT_DEC, - MIN_GAS_LIMIT_HEX, - MIN_GAS_PRICE_DEC, - MIN_GAS_PRICE_GWEI, - MIN_GAS_PRICE_HEX, - MIN_GAS_TOTAL, - NEGATIVE_ETH_ERROR, - ONE_GWEI_IN_WEI_HEX, - REQUIRED_ERROR, - SIMPLE_GAS_COST, - TOKEN_TRANSFER_FUNCTION_SIGNATURE, - BASE_TOKEN_GAS_COST, -} diff --git a/ui/app/components/send_/send.container.js b/ui/app/components/send_/send.container.js deleted file mode 100644 index 44ebd2792..000000000 --- a/ui/app/components/send_/send.container.js +++ /dev/null @@ -1,93 +0,0 @@ -import { connect } from 'react-redux' -import SendEther from './send.component' -import { withRouter } from 'react-router-dom' -import { compose } from 'recompose' -import { - getAmountConversionRate, - getBlockGasLimit, - getConversionRate, - getCurrentNetwork, - getGasLimit, - getGasPrice, - getGasTotal, - getPrimaryCurrency, - getRecentBlocks, - getSelectedAddress, - getSelectedToken, - getSelectedTokenContract, - getSelectedTokenToFiatRate, - getSendAmount, - getSendEditingTransactionId, - getSendFromObject, - getSendTo, - getTokenBalance, -} from './send.selectors' -import { - updateSendTokenBalance, - updateGasData, - setGasTotal, -} from '../../actions' -import { - resetSendState, - updateSendErrors, -} from '../../ducks/send.duck' -import { - calcGasTotal, -} from './send.utils.js' - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(SendEther) - -function mapStateToProps (state) { - return { - amount: getSendAmount(state), - amountConversionRate: getAmountConversionRate(state), - blockGasLimit: getBlockGasLimit(state), - conversionRate: getConversionRate(state), - editingTransactionId: getSendEditingTransactionId(state), - from: getSendFromObject(state), - gasLimit: getGasLimit(state), - gasPrice: getGasPrice(state), - gasTotal: getGasTotal(state), - network: getCurrentNetwork(state), - primaryCurrency: getPrimaryCurrency(state), - recentBlocks: getRecentBlocks(state), - selectedAddress: getSelectedAddress(state), - selectedToken: getSelectedToken(state), - to: getSendTo(state), - tokenBalance: getTokenBalance(state), - tokenContract: getSelectedTokenContract(state), - tokenToFiatRate: getSelectedTokenToFiatRate(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - updateAndSetGasTotal: ({ - blockGasLimit, - editingTransactionId, - gasLimit, - gasPrice, - recentBlocks, - selectedAddress, - selectedToken, - to, - value, - }) => { - !editingTransactionId - ? dispatch(updateGasData({ recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value })) - : dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice))) - }, - updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => { - dispatch(updateSendTokenBalance({ - selectedToken, - tokenContract, - address, - })) - }, - updateSendErrors: newError => dispatch(updateSendErrors(newError)), - resetSendState: () => dispatch(resetSendState()), - } -} diff --git a/ui/app/components/send_/send.scss b/ui/app/components/send_/send.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/app/components/send_/send.selectors.js b/ui/app/components/send_/send.selectors.js deleted file mode 100644 index f910f7caf..000000000 --- a/ui/app/components/send_/send.selectors.js +++ /dev/null @@ -1,279 +0,0 @@ -const { valuesFor } = require('../../util') -const abi = require('human-standard-token-abi') -const { - multiplyCurrencies, -} = require('../../conversion-util') -const { - estimateGasPriceFromRecentBlocks, -} = require('./send.utils') - -const selectors = { - accountsWithSendEtherInfoSelector, - // autoAddToBetaUI, - getAddressBook, - getAmountConversionRate, - getBlockGasLimit, - getConversionRate, - getCurrentAccountWithSendEtherInfo, - getCurrentCurrency, - getCurrentNetwork, - getCurrentViewContext, - getForceGasMin, - getGasLimit, - getGasPrice, - getGasPriceFromRecentBlocks, - getGasTotal, - getPrimaryCurrency, - getRecentBlocks, - getSelectedAccount, - getSelectedAddress, - getSelectedIdentity, - getSelectedToken, - getSelectedTokenContract, - getSelectedTokenExchangeRate, - getSelectedTokenToFiatRate, - getSendAmount, - getSendEditingTransactionId, - getSendErrors, - getSendFrom, - getSendFromBalance, - getSendFromObject, - getSendMaxModeState, - getSendTo, - getSendToAccounts, - getTokenBalance, - getTokenExchangeRate, - getUnapprovedTxs, - transactionsSelector, -} - -module.exports = selectors - -function accountsWithSendEtherInfoSelector (state) { - const { - accounts, - identities, - } = state.metamask - - const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => { - return Object.assign({}, account, identities[key]) - }) - - return accountsWithSendEtherInfo -} - -// function autoAddToBetaUI (state) { -// const autoAddTransactionThreshold = 12 -// const autoAddAccountsThreshold = 2 -// const autoAddTokensThreshold = 1 - -// const numberOfTransactions = state.metamask.selectedAddressTxList.length -// const numberOfAccounts = Object.keys(state.metamask.accounts).length -// const numberOfTokensAdded = state.metamask.tokens.length - -// const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) && -// (numberOfAccounts > autoAddAccountsThreshold) && -// (numberOfTokensAdded > autoAddTokensThreshold) -// const userIsNotInBeta = !state.metamask.featureFlags.betaUI - -// return userIsNotInBeta && userPassesThreshold -// } - -function getAddressBook (state) { - return state.metamask.addressBook -} - -function getAmountConversionRate (state) { - return getSelectedToken(state) - ? getSelectedTokenToFiatRate(state) - : getConversionRate(state) -} - -function getBlockGasLimit (state) { - return state.metamask.currentBlockGasLimit -} - -function getConversionRate (state) { - return state.metamask.conversionRate -} - -function getCurrentAccountWithSendEtherInfo (state) { - const currentAddress = getSelectedAddress(state) - const accounts = accountsWithSendEtherInfoSelector(state) - - return accounts.find(({ address }) => address === currentAddress) -} - -function getCurrentCurrency (state) { - return state.metamask.currentCurrency -} - -function getCurrentNetwork (state) { - return state.metamask.network -} - -function getCurrentViewContext (state) { - const { currentView = {} } = state.appState - return currentView.context -} - -function getForceGasMin (state) { - return state.metamask.send.forceGasMin -} - -function getGasLimit (state) { - return state.metamask.send.gasLimit -} - -function getGasPrice (state) { - return state.metamask.send.gasPrice -} - -function getGasPriceFromRecentBlocks (state) { - return estimateGasPriceFromRecentBlocks(state.metamask.recentBlocks) -} - -function getGasTotal (state) { - return state.metamask.send.gasTotal -} - -function getPrimaryCurrency (state) { - const selectedToken = getSelectedToken(state) - return selectedToken && selectedToken.symbol -} - -function getRecentBlocks (state) { - return state.metamask.recentBlocks -} - -function getSelectedAccount (state) { - const accounts = state.metamask.accounts - const selectedAddress = getSelectedAddress(state) - - return accounts[selectedAddress] -} - -function getSelectedAddress (state) { - const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0] - - return selectedAddress -} - -function getSelectedIdentity (state) { - const selectedAddress = getSelectedAddress(state) - const identities = state.metamask.identities - - return identities[selectedAddress] -} - -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 - - return selectedToken || sendToken || null -} - -function getSelectedTokenContract (state) { - const selectedToken = getSelectedToken(state) - - return selectedToken - ? global.eth.contract(abi).at(selectedToken.address) - : null -} - -function getSelectedTokenExchangeRate (state) { - const tokenExchangeRates = state.metamask.tokenExchangeRates - const selectedToken = getSelectedToken(state) || {} - const { symbol = '' } = selectedToken - const pair = `${symbol.toLowerCase()}_eth` - const { rate: tokenExchangeRate = 0 } = tokenExchangeRates && tokenExchangeRates[pair] || {} - - return tokenExchangeRate -} - -function getSelectedTokenToFiatRate (state) { - const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state) - const conversionRate = getConversionRate(state) - - const tokenToFiatRate = multiplyCurrencies( - conversionRate, - selectedTokenExchangeRate, - { toNumericBase: 'dec' } - ) - - return tokenToFiatRate -} - -function getSendAmount (state) { - return state.metamask.send.amount -} - -function getSendEditingTransactionId (state) { - return state.metamask.send.editingTransactionId -} - -function getSendErrors (state) { - return state.send.errors -} - -function getSendFrom (state) { - return state.metamask.send.from -} - -function getSendFromBalance (state) { - const from = getSendFrom(state) || getSelectedAccount(state) - return from.balance -} - -function getSendFromObject (state) { - return getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state) -} - -function getSendMaxModeState (state) { - return state.metamask.send.maxModeOn -} - -function getSendTo (state) { - return state.metamask.send.to -} - -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) -} - -function getTokenBalance (state) { - return state.metamask.send.tokenBalance -} - -function getTokenExchangeRate (state, tokenSymbol) { - const pair = `${tokenSymbol.toLowerCase()}_eth` - const tokenExchangeRates = state.metamask.tokenExchangeRates - const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} - - return tokenExchangeRate -} - -function getUnapprovedTxs (state) { - return state.metamask.unapprovedTxs -} - -function transactionsSelector (state) { - const { network, selectedTokenAddress } = state.metamask - const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs) - const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined - const transactions = state.metamask.selectedAddressTxList || [] - const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) - - return selectedTokenAddress - ? txsToRender - .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress) - .sort((a, b) => b.time - a.time) - : txsToRender - .sort((a, b) => b.time - a.time) -} diff --git a/ui/app/components/send_/send.utils.js b/ui/app/components/send_/send.utils.js deleted file mode 100644 index aa255c3d4..000000000 --- a/ui/app/components/send_/send.utils.js +++ /dev/null @@ -1,312 +0,0 @@ -const { - addCurrencies, - conversionUtil, - conversionGTE, - multiplyCurrencies, - conversionGreaterThan, - conversionLessThan, -} = require('../../conversion-util') -const { - calcTokenAmount, -} = require('../../token-util') -const { - BASE_TOKEN_GAS_COST, - INSUFFICIENT_FUNDS_ERROR, - INSUFFICIENT_TOKENS_ERROR, - NEGATIVE_ETH_ERROR, - ONE_GWEI_IN_WEI_HEX, - SIMPLE_GAS_COST, - TOKEN_TRANSFER_FUNCTION_SIGNATURE, -} = require('./send.constants') -const abi = require('ethereumjs-abi') -const ethUtil = require('ethereumjs-util') - -module.exports = { - addGasBuffer, - calcGasTotal, - calcTokenBalance, - doesAmountErrorRequireUpdate, - estimateGas, - estimateGasPriceFromRecentBlocks, - generateTokenTransferData, - getAmountErrorObject, - getGasFeeErrorObject, - getToAddressForGasUpdate, - isBalanceSufficient, - isTokenBalanceSufficient, - removeLeadingZeroes, -} - -function calcGasTotal (gasLimit = '0', gasPrice = '0') { - return multiplyCurrencies(gasLimit, gasPrice, { - toNumericBase: 'hex', - multiplicandBase: 16, - multiplierBase: 16, - }) -} - -function isBalanceSufficient ({ - amount = '0x0', - amountConversionRate = 1, - balance = '0x0', - conversionRate = 1, - gasTotal = '0x0', - primaryCurrency, -}) { - const totalAmount = addCurrencies(amount, gasTotal, { - aBase: 16, - bBase: 16, - toNumericBase: 'hex', - }) - - const balanceIsSufficient = conversionGTE( - { - value: balance, - fromNumericBase: 'hex', - fromCurrency: primaryCurrency, - conversionRate, - }, - { - value: totalAmount, - fromNumericBase: 'hex', - conversionRate: Number(amountConversionRate) || conversionRate, - fromCurrency: primaryCurrency, - }, - ) - - return balanceIsSufficient -} - -function isTokenBalanceSufficient ({ - amount = '0x0', - tokenBalance, - decimals, -}) { - const amountInDec = conversionUtil(amount, { - fromNumericBase: 'hex', - }) - - const tokenBalanceIsSufficient = conversionGTE( - { - value: tokenBalance, - fromNumericBase: 'dec', - }, - { - value: calcTokenAmount(amountInDec, decimals), - fromNumericBase: 'dec', - }, - ) - - return tokenBalanceIsSufficient -} - -function getAmountErrorObject ({ - amount, - amountConversionRate, - balance, - conversionRate, - gasTotal, - primaryCurrency, - selectedToken, - tokenBalance, -}) { - let insufficientFunds = false - if (gasTotal && conversionRate && !selectedToken) { - insufficientFunds = !isBalanceSufficient({ - amount, - amountConversionRate, - balance, - conversionRate, - gasTotal, - primaryCurrency, - }) - } - - let inSufficientTokens = false - if (selectedToken && tokenBalance !== null) { - const { decimals } = selectedToken - inSufficientTokens = !isTokenBalanceSufficient({ - tokenBalance, - amount, - decimals, - }) - } - - const amountLessThanZero = conversionGreaterThan( - { value: 0, fromNumericBase: 'dec' }, - { value: amount, fromNumericBase: 'hex' }, - ) - - let amountError = null - - if (insufficientFunds) { - amountError = INSUFFICIENT_FUNDS_ERROR - } else if (inSufficientTokens) { - amountError = INSUFFICIENT_TOKENS_ERROR - } else if (amountLessThanZero) { - amountError = NEGATIVE_ETH_ERROR - } - - return { amount: amountError } -} - -function getGasFeeErrorObject ({ - amount, - amountConversionRate, - balance, - conversionRate, - gasTotal, - primaryCurrency, -}) { - let gasFeeError = null - - if (gasTotal && conversionRate) { - const insufficientFunds = !isBalanceSufficient({ - amount: '0x0', - amountConversionRate, - balance, - conversionRate, - gasTotal, - primaryCurrency, - }) - - if (insufficientFunds) { - gasFeeError = INSUFFICIENT_FUNDS_ERROR - } - } - - return { gasFee: gasFeeError } -} - -function calcTokenBalance ({ selectedToken, usersToken }) { - const { decimals } = selectedToken || {} - return calcTokenAmount(usersToken.balance.toString(), decimals) + '' -} - -function doesAmountErrorRequireUpdate ({ - balance, - gasTotal, - prevBalance, - prevGasTotal, - prevTokenBalance, - selectedToken, - tokenBalance, -}) { - const balanceHasChanged = balance !== prevBalance - const gasTotalHasChange = gasTotal !== prevGasTotal - const tokenBalanceHasChanged = selectedToken && tokenBalance !== prevTokenBalance - const amountErrorRequiresUpdate = balanceHasChanged || gasTotalHasChange || tokenBalanceHasChanged - - return amountErrorRequiresUpdate -} - -async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to, value, gasPrice, estimateGasMethod }) { - const paramsForGasEstimate = { from: selectedAddress, value, gasPrice } - - if (selectedToken) { - paramsForGasEstimate.value = '0x0' - paramsForGasEstimate.data = generateTokenTransferData({ toAddress: to, amount: value, selectedToken }) - } - - // if recipient has no code, gas is 21k max: - if (!selectedToken) { - const code = Boolean(to) && await global.eth.getCode(to) - if (!code || code === '0x') { - return SIMPLE_GAS_COST - } - } else if (selectedToken && !to) { - return BASE_TOKEN_GAS_COST - } - - paramsForGasEstimate.to = selectedToken ? selectedToken.address : to - - // if not, fall back to block gasLimit - paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit, 0.95, { - multiplicandBase: 16, - multiplierBase: 10, - roundDown: '0', - toNumericBase: 'hex', - })) - // run tx - return new Promise((resolve, reject) => { - return estimateGasMethod(paramsForGasEstimate, (err, estimatedGas) => { - if (err) { - const simulationFailed = ( - err.message.includes('Transaction execution error.') || - err.message.includes('gas required exceeds allowance or always failing transaction') - ) - if (simulationFailed) { - const estimateWithBuffer = addGasBuffer(paramsForGasEstimate.gas, blockGasLimit, 1.5) - return resolve(ethUtil.addHexPrefix(estimateWithBuffer)) - } else { - return reject(err) - } - } - const estimateWithBuffer = addGasBuffer(estimatedGas.toString(16), blockGasLimit, 1.5) - return resolve(ethUtil.addHexPrefix(estimateWithBuffer)) - }) - }) -} - -function addGasBuffer (initialGasLimitHex, blockGasLimitHex, bufferMultiplier = 1.5) { - const upperGasLimit = multiplyCurrencies(blockGasLimitHex, 0.9, { - toNumericBase: 'hex', - multiplicandBase: 16, - multiplierBase: 10, - numberOfDecimals: '0', - }) - const bufferedGasLimit = multiplyCurrencies(initialGasLimitHex, bufferMultiplier, { - toNumericBase: 'hex', - multiplicandBase: 16, - multiplierBase: 10, - numberOfDecimals: '0', - }) - - // if initialGasLimit is above blockGasLimit, dont modify it - if (conversionGreaterThan( - { value: initialGasLimitHex, fromNumericBase: 'hex' }, - { value: upperGasLimit, fromNumericBase: 'hex' }, - )) return initialGasLimitHex - // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit - if (conversionLessThan( - { value: bufferedGasLimit, fromNumericBase: 'hex' }, - { value: upperGasLimit, fromNumericBase: 'hex' }, - )) return bufferedGasLimit - // otherwise use blockGasLimit - return upperGasLimit -} - -function generateTokenTransferData ({ toAddress = '0x0', amount = '0x0', selectedToken }) { - if (!selectedToken) return - return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( - abi.rawEncode(['address', 'uint256'], [toAddress, ethUtil.addHexPrefix(amount)]), - x => ('00' + x.toString(16)).slice(-2) - ).join('') -} - -function estimateGasPriceFromRecentBlocks (recentBlocks) { - // Return 1 gwei if no blocks have been observed: - if (!recentBlocks || recentBlocks.length === 0) { - return ONE_GWEI_IN_WEI_HEX - } - - const lowestPrices = recentBlocks.map((block) => { - if (!block.gasPrices || block.gasPrices.length < 1) { - return ONE_GWEI_IN_WEI_HEX - } - return block.gasPrices.reduce((currentLowest, next) => { - return parseInt(next, 16) < parseInt(currentLowest, 16) ? next : currentLowest - }) - }) - .sort((a, b) => parseInt(a, 16) > parseInt(b, 16) ? 1 : -1) - - return lowestPrices[Math.floor(lowestPrices.length / 2)] -} - -function getToAddressForGasUpdate (...addresses) { - return [...addresses, ''].find(str => str !== undefined && str !== null).toLowerCase() -} - -function removeLeadingZeroes (str) { - return str.replace(/^0*(?=\d)/, '') -} diff --git a/ui/app/components/send_/tests/send-component.test.js b/ui/app/components/send_/tests/send-component.test.js deleted file mode 100644 index 6194ec508..000000000 --- a/ui/app/components/send_/tests/send-component.test.js +++ /dev/null @@ -1,332 +0,0 @@ -import React from 'react' -import assert from 'assert' -import proxyquire from 'proxyquire' -import { shallow } from 'enzyme' -import sinon from 'sinon' - -import SendHeader from '../send-header/send-header.container' -import SendContent from '../send-content/send-content.component' -import SendFooter from '../send-footer/send-footer.container' - -const propsMethodSpies = { - updateAndSetGasTotal: sinon.spy(), - updateSendErrors: sinon.spy(), - updateSendTokenBalance: sinon.spy(), - resetSendState: sinon.spy(), -} -const utilsMethodStubs = { - getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }), - getGasFeeErrorObject: sinon.stub().returns({ gasFee: 'mockGasFeeError' }), - doesAmountErrorRequireUpdate: sinon.stub().callsFake(obj => obj.balance !== obj.prevBalance), -} - -const SendTransactionScreen = proxyquire('../send.component.js', { - './send.utils': utilsMethodStubs, -}).default - -sinon.spy(SendTransactionScreen.prototype, 'componentDidMount') -sinon.spy(SendTransactionScreen.prototype, 'updateGas') - -describe('Send Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow() - }) - - afterEach(() => { - SendTransactionScreen.prototype.componentDidMount.resetHistory() - SendTransactionScreen.prototype.updateGas.resetHistory() - utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory() - utilsMethodStubs.getAmountErrorObject.resetHistory() - utilsMethodStubs.getGasFeeErrorObject.resetHistory() - propsMethodSpies.updateAndSetGasTotal.resetHistory() - propsMethodSpies.updateSendErrors.resetHistory() - propsMethodSpies.updateSendTokenBalance.resetHistory() - }) - - it('should call componentDidMount', () => { - assert(SendTransactionScreen.prototype.componentDidMount.calledOnce) - }) - - describe('componentWillMount', () => { - it('should call this.updateGas', () => { - SendTransactionScreen.prototype.updateGas.resetHistory() - propsMethodSpies.updateSendErrors.resetHistory() - assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0) - wrapper.instance().componentWillMount() - assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1) - }) - }) - - describe('componentWillUnmount', () => { - it('should call this.props.resetSendState', () => { - propsMethodSpies.resetSendState.resetHistory() - assert.equal(propsMethodSpies.resetSendState.callCount, 0) - wrapper.instance().componentWillUnmount() - assert.equal(propsMethodSpies.resetSendState.callCount, 1) - }) - }) - - describe('componentDidUpdate', () => { - it('should call doesAmountErrorRequireUpdate with the expected params', () => { - utilsMethodStubs.getAmountErrorObject.resetHistory() - wrapper.instance().componentDidUpdate({ - from: { - balance: '', - }, - }) - assert(utilsMethodStubs.doesAmountErrorRequireUpdate.calledOnce) - assert.deepEqual( - utilsMethodStubs.doesAmountErrorRequireUpdate.getCall(0).args[0], - { - balance: 'mockBalance', - gasTotal: 'mockGasTotal', - prevBalance: '', - prevGasTotal: undefined, - prevTokenBalance: undefined, - selectedToken: 'mockSelectedToken', - tokenBalance: 'mockTokenBalance', - } - ) - }) - - it('should not call getAmountErrorObject if doesAmountErrorRequireUpdate returns false', () => { - utilsMethodStubs.getAmountErrorObject.resetHistory() - wrapper.instance().componentDidUpdate({ - from: { - balance: 'mockBalance', - }, - }) - assert.equal(utilsMethodStubs.getAmountErrorObject.callCount, 0) - }) - - it('should call getAmountErrorObject if doesAmountErrorRequireUpdate returns true', () => { - utilsMethodStubs.getAmountErrorObject.resetHistory() - wrapper.instance().componentDidUpdate({ - from: { - balance: 'balanceChanged', - }, - }) - assert.equal(utilsMethodStubs.getAmountErrorObject.callCount, 1) - assert.deepEqual( - utilsMethodStubs.getAmountErrorObject.getCall(0).args[0], - { - amount: 'mockAmount', - amountConversionRate: 'mockAmountConversionRate', - balance: 'mockBalance', - conversionRate: 10, - gasTotal: 'mockGasTotal', - primaryCurrency: 'mockPrimaryCurrency', - selectedToken: 'mockSelectedToken', - tokenBalance: 'mockTokenBalance', - } - ) - }) - - it('should call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true and selectedToken is truthy', () => { - utilsMethodStubs.getGasFeeErrorObject.resetHistory() - wrapper.instance().componentDidUpdate({ - from: { - balance: 'balanceChanged', - }, - }) - assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 1) - assert.deepEqual( - utilsMethodStubs.getGasFeeErrorObject.getCall(0).args[0], - { - amount: 'mockAmount', - amountConversionRate: 'mockAmountConversionRate', - balance: 'mockBalance', - conversionRate: 10, - gasTotal: 'mockGasTotal', - primaryCurrency: 'mockPrimaryCurrency', - selectedToken: 'mockSelectedToken', - tokenBalance: 'mockTokenBalance', - } - ) - }) - - it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns false', () => { - utilsMethodStubs.getGasFeeErrorObject.resetHistory() - wrapper.instance().componentDidUpdate({ - from: { address: 'mockAddress', balance: 'mockBalance' }, - }) - assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0) - }) - - it('should not call getGasFeeErrorObject if doesAmountErrorRequireUpdate returns true but selectedToken is falsy', () => { - utilsMethodStubs.getGasFeeErrorObject.resetHistory() - wrapper.setProps({ selectedToken: null }) - wrapper.instance().componentDidUpdate({ - from: { - balance: 'balanceChanged', - }, - }) - assert.equal(utilsMethodStubs.getGasFeeErrorObject.callCount, 0) - }) - - it('should call updateSendErrors with the expected params if selectedToken is falsy', () => { - propsMethodSpies.updateSendErrors.resetHistory() - wrapper.setProps({ selectedToken: null }) - wrapper.instance().componentDidUpdate({ - from: { - balance: 'balanceChanged', - }, - }) - assert.equal(propsMethodSpies.updateSendErrors.callCount, 1) - assert.deepEqual( - propsMethodSpies.updateSendErrors.getCall(0).args[0], - { amount: 'mockAmountError', gasFee: null } - ) - }) - - it('should call updateSendErrors with the expected params if selectedToken is truthy', () => { - propsMethodSpies.updateSendErrors.resetHistory() - wrapper.setProps({ selectedToken: 'someToken' }) - wrapper.instance().componentDidUpdate({ - from: { - balance: 'balanceChanged', - }, - }) - assert.equal(propsMethodSpies.updateSendErrors.callCount, 1) - assert.deepEqual( - propsMethodSpies.updateSendErrors.getCall(0).args[0], - { amount: 'mockAmountError', gasFee: 'mockGasFeeError' } - ) - }) - - it('should not call updateSendTokenBalance or this.updateGas if network === prevNetwork', () => { - SendTransactionScreen.prototype.updateGas.resetHistory() - propsMethodSpies.updateSendTokenBalance.resetHistory() - wrapper.instance().componentDidUpdate({ - from: { - balance: 'balanceChanged', - }, - network: '3', - }) - assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 0) - assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0) - }) - - it('should not call updateSendTokenBalance or this.updateGas if network === loading', () => { - wrapper.setProps({ network: 'loading' }) - SendTransactionScreen.prototype.updateGas.resetHistory() - propsMethodSpies.updateSendTokenBalance.resetHistory() - wrapper.instance().componentDidUpdate({ - from: { - balance: 'balanceChanged', - }, - network: '3', - }) - assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 0) - assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0) - }) - - it('should call updateSendTokenBalance and this.updateGas with the correct params', () => { - SendTransactionScreen.prototype.updateGas.resetHistory() - propsMethodSpies.updateSendTokenBalance.resetHistory() - wrapper.instance().componentDidUpdate({ - from: { - balance: 'balanceChanged', - }, - network: '2', - }) - assert.equal(propsMethodSpies.updateSendTokenBalance.callCount, 1) - assert.deepEqual( - propsMethodSpies.updateSendTokenBalance.getCall(0).args[0], - { - selectedToken: 'mockSelectedToken', - tokenContract: 'mockTokenContract', - address: 'mockAddress', - } - ) - assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1) - assert.deepEqual( - SendTransactionScreen.prototype.updateGas.getCall(0).args, - [] - ) - }) - }) - - describe('updateGas', () => { - it('should call updateAndSetGasTotal with the correct params if no to prop is passed', () => { - propsMethodSpies.updateAndSetGasTotal.resetHistory() - wrapper.instance().updateGas() - assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1) - assert.deepEqual( - propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0], - { - blockGasLimit: 'mockBlockGasLimit', - editingTransactionId: 'mockEditingTransactionId', - gasLimit: 'mockGasLimit', - gasPrice: 'mockGasPrice', - recentBlocks: ['mockBlock'], - selectedAddress: 'mockSelectedAddress', - selectedToken: 'mockSelectedToken', - to: '', - value: 'mockAmount', - } - ) - }) - - it('should call updateAndSetGasTotal with the correct params if a to prop is passed', () => { - propsMethodSpies.updateAndSetGasTotal.resetHistory() - wrapper.setProps({ to: 'someAddress' }) - wrapper.instance().updateGas() - assert.equal( - propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, - 'someaddress', - ) - }) - - it('should call updateAndSetGasTotal with to set to lowercase if passed', () => { - propsMethodSpies.updateAndSetGasTotal.resetHistory() - wrapper.instance().updateGas({ to: '0xABC' }) - assert.equal(propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, '0xabc') - }) - }) - - describe('render', () => { - it('should render a page-container class', () => { - assert.equal(wrapper.find('.page-container').length, 1) - }) - - it('should render SendHeader, SendContent and SendFooter', () => { - assert.equal(wrapper.find(SendHeader).length, 1) - assert.equal(wrapper.find(SendContent).length, 1) - assert.equal(wrapper.find(SendFooter).length, 1) - }) - - it('should pass the history prop to SendHeader and SendFooter', () => { - assert.deepEqual( - wrapper.find(SendFooter).props(), - { - history: { mockProp: 'history-abc' }, - } - ) - }) - }) -}) diff --git a/ui/app/components/send_/tests/send-container.test.js b/ui/app/components/send_/tests/send-container.test.js deleted file mode 100644 index 7a9120d24..000000000 --- a/ui/app/components/send_/tests/send-container.test.js +++ /dev/null @@ -1,169 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -let mapStateToProps -let mapDispatchToProps - -const actionSpies = { - updateSendTokenBalance: sinon.spy(), - updateGasData: sinon.spy(), - setGasTotal: sinon.spy(), -} -const duckActionSpies = { - updateSendErrors: sinon.spy(), - resetSendState: sinon.spy(), -} - -proxyquire('../send.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - }, - 'react-router-dom': { withRouter: () => {} }, - 'recompose': { compose: (arg1, arg2) => () => arg2() }, - './send.selectors': { - getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, - getBlockGasLimit: (s) => `mockBlockGasLimit:${s}`, - getConversionRate: (s) => `mockConversionRate:${s}`, - getCurrentNetwork: (s) => `mockNetwork:${s}`, - getGasLimit: (s) => `mockGasLimit:${s}`, - getGasPrice: (s) => `mockGasPrice:${s}`, - 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}`, - getSendAmount: (s) => `mockAmount:${s}`, - getSendTo: (s) => `mockTo:${s}`, - getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, - getSendFromObject: (s) => `mockFrom:${s}`, - getTokenBalance: (s) => `mockTokenBalance:${s}`, - }, - '../../actions': actionSpies, - '../../ducks/send.duck': duckActionSpies, - './send.utils.js': { - calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice, - }, -}) - -describe('send container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - amount: 'mockAmount:mockState', - amountConversionRate: 'mockAmountConversionRate:mockState', - blockGasLimit: 'mockBlockGasLimit:mockState', - conversionRate: 'mockConversionRate:mockState', - editingTransactionId: 'mockEditingTransactionId:mockState', - from: 'mockFrom:mockState', - gasLimit: 'mockGasLimit:mockState', - gasPrice: 'mockGasPrice:mockState', - gasTotal: 'mockGasTotal:mockState', - network: 'mockNetwork:mockState', - primaryCurrency: 'mockPrimaryCurrency:mockState', - recentBlocks: 'mockRecentBlocks:mockState', - selectedAddress: 'mockSelectedAddress:mockState', - selectedToken: 'mockSelectedToken:mockState', - to: 'mockTo:mockState', - tokenBalance: 'mockTokenBalance:mockState', - tokenContract: 'mockTokenContract:mockState', - tokenToFiatRate: 'mockTokenToFiatRate:mockState', - }) - }) - - }) - - describe('mapDispatchToProps()', () => { - let dispatchSpy - let mapDispatchToPropsObject - - beforeEach(() => { - dispatchSpy = sinon.spy() - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) - }) - - describe('updateAndSetGasTotal()', () => { - const mockProps = { - blockGasLimit: 'mockBlockGasLimit', - editingTransactionId: '0x2', - gasLimit: '0x3', - gasPrice: '0x4', - recentBlocks: ['mockBlock'], - selectedAddress: '0x4', - selectedToken: { address: '0x1' }, - to: 'mockTo', - value: 'mockValue', - } - - it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => { - mapDispatchToPropsObject.updateAndSetGasTotal(mockProps) - assert(dispatchSpy.calledOnce) - assert.equal( - actionSpies.setGasTotal.getCall(0).args[0], - '0x30x4' - ) - }) - - it('should dispatch an updateGasData action when editingTransactionId is falsy', () => { - const { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value } = mockProps - mapDispatchToPropsObject.updateAndSetGasTotal( - Object.assign({}, mockProps, {editingTransactionId: false}) - ) - assert(dispatchSpy.calledOnce) - assert.deepEqual( - actionSpies.updateGasData.getCall(0).args[0], - { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value } - ) - }) - }) - - describe('updateSendTokenBalance()', () => { - const mockProps = { - address: '0x10', - tokenContract: '0x00a', - selectedToken: {address: '0x1'}, - } - - it('should dispatch an action', () => { - mapDispatchToPropsObject.updateSendTokenBalance(Object.assign({}, mockProps)) - assert(dispatchSpy.calledOnce) - assert.deepEqual( - actionSpies.updateSendTokenBalance.getCall(0).args[0], - mockProps - ) - }) - }) - - describe('updateSendErrors()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.updateSendErrors('mockError') - assert(dispatchSpy.calledOnce) - assert.equal( - duckActionSpies.updateSendErrors.getCall(0).args[0], - 'mockError' - ) - }) - }) - - describe('resetSendState()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.resetSendState() - assert(dispatchSpy.calledOnce) - assert.equal( - duckActionSpies.resetSendState.getCall(0).args.length, - 0 - ) - }) - }) - - }) - -}) diff --git a/ui/app/components/send_/tests/send-selectors-test-data.js b/ui/app/components/send_/tests/send-selectors-test-data.js deleted file mode 100644 index 8f9c19314..000000000 --- a/ui/app/components/send_/tests/send-selectors-test-data.js +++ /dev/null @@ -1,230 +0,0 @@ -module.exports = { - 'metamask': { - 'isInitialized': true, - 'isUnlocked': true, - 'featureFlags': {'betaUI': true}, - 'rpcTarget': 'https://rawtestrpc.metamask.io/', - 'identities': { - '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { - 'address': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', - 'name': 'Send Account 1', - }, - '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { - 'address': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - 'name': 'Send Account 2', - }, - '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { - 'address': '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', - 'name': 'Send Account 3', - }, - '0xd85a4b6a394794842887b8284293d69163007bbb': { - 'address': '0xd85a4b6a394794842887b8284293d69163007bbb', - 'name': 'Send Account 4', - }, - }, - 'currentBlockGasLimit': '0x4c1878', - 'currentCurrency': 'USD', - 'conversionRate': 1200.88200327, - 'conversionDate': 1489013762, - 'noActiveNotices': true, - 'frequentRpcList': [], - 'network': '3', - 'accounts': { - '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { - 'code': '0x', - 'balance': '0x47c9d71831c76efe', - 'nonce': '0x1b', - 'address': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', - }, - '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb': { - 'code': '0x', - 'balance': '0x37452b1315889f80', - 'nonce': '0xa', - 'address': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - }, - '0x2f8d4a878cfa04a6e60d46362f5644deab66572d': { - 'code': '0x', - 'balance': '0x30c9d71831c76efe', - 'nonce': '0x1c', - 'address': '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', - }, - '0xd85a4b6a394794842887b8284293d69163007bbb': { - 'code': '0x', - 'balance': '0x0', - 'nonce': '0x0', - 'address': '0xd85a4b6a394794842887b8284293d69163007bbb', - }, - }, - 'addressBook': [ - { - 'address': '0x06195827297c7a80a443b6894d3bdb8824b43896', - 'name': 'Address Book Account 1', - }, - ], - 'tokens': [ - { - 'address': '0x1a195821297c7a80a433b6894d3bdb8824b43896', - 'decimals': 18, - 'symbol': 'ABC', - }, - { - 'address': '0x8d6b81208414189a58339873ab429b6c47ab92d3', - 'decimals': 4, - 'symbol': 'DEF', - }, - { - 'address': '0xa42084c8d1d9a2198631988579bb36b48433a72b', - 'decimals': 18, - 'symbol': 'GHI', - }, - ], - 'tokenExchangeRates': { - 'def_eth': { - rate: 2.0, - }, - 'ghi_eth': { - rate: 31.01, - }, - }, - 'transactions': {}, - 'selectedAddressTxList': [ - { - 'id': 'mockTokenTx1', - 'txParams': { - 'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3', - }, - 'time': 1700000000000, - }, - { - 'id': 'mockTokenTx2', - 'txParams': { - 'to': '0xafaketokenaddress', - }, - 'time': 1600000000000, - }, - { - 'id': 'mockTokenTx3', - 'txParams': { - 'to': '0x8d6b81208414189a58339873ab429b6c47ab92d3', - }, - 'time': 1500000000000, - }, - { - 'id': 'mockEthTx1', - 'txParams': { - 'to': '0xd85a4b6a394794842887b8284293d69163007bbb', - }, - 'time': 1400000000000, - }, - ], - 'selectedTokenAddress': '0x8d6b81208414189a58339873ab429b6c47ab92d3', - 'unapprovedMsgs': { - '0xabc': { id: 'unapprovedMessage1', 'time': 1650000000000 }, - '0xdef': { id: 'unapprovedMessage2', 'time': 1550000000000 }, - '0xghi': { id: 'unapprovedMessage3', 'time': 1450000000000 }, - }, - 'unapprovedMsgCount': 0, - 'unapprovedPersonalMsgs': {}, - 'unapprovedPersonalMsgCount': 0, - 'keyringTypes': [ - 'Simple Key Pair', - 'HD Key Tree', - ], - 'keyrings': [ - { - 'type': 'HD Key Tree', - 'accounts': [ - 'fdea65c8e26263f6d9a1b5de9555d2931a33b825', - 'c5b8dbac4c1d3f152cdeb400e2313f309c410acb', - '2f8d4a878cfa04a6e60d46362f5644deab66572d', - ], - }, - { - 'type': 'Simple Key Pair', - 'accounts': [ - '0xd85a4b6a394794842887b8284293d69163007bbb', - ], - }, - ], - 'selectedAddress': '0xd85a4b6a394794842887b8284293d69163007bbb', - 'provider': { - 'type': 'testnet', - }, - 'shapeShiftTxList': [ - { id: 'shapeShiftTx1', 'time': 1675000000000 }, - { id: 'shapeShiftTx2', 'time': 1575000000000 }, - { id: 'shapeShiftTx3', 'time': 1475000000000 }, - ], - 'lostAccounts': [], - 'send': { - 'gasLimit': '0xFFFF', - 'gasPrice': '0xaa', - 'gasTotal': '0xb451dc41b578', - 'tokenBalance': 3434, - 'from': { - 'address': '0xabcdefg', - 'balance': '0x5f4e3d2c1', - }, - 'to': '0x987fedabc', - 'amount': '0x080', - 'memo': '', - 'errors': { - 'someError': null, - }, - 'maxModeOn': false, - 'editingTransactionId': 97531, - 'forceGasMin': true, - }, - 'unapprovedTxs': { - '4768706228115573': { - 'id': 4768706228115573, - 'time': 1487363153561, - 'status': 'unapproved', - 'gasMultiplier': 1, - 'metamaskNetworkId': '3', - 'txParams': { - 'from': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - 'to': '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761', - 'value': '0xde0b6b3a7640000', - 'metamaskId': 4768706228115573, - 'metamaskNetworkId': '3', - 'gas': '0x5209', - }, - 'gasLimitSpecified': false, - 'estimatedGas': '0x5209', - 'txFee': '17e0186e60800', - 'txValue': 'de0b6b3a7640000', - 'maxCost': 'de234b52e4a0800', - 'gasPrice': '4a817c800', - }, - }, - 'currentLocale': 'en', - recentBlocks: ['mockBlock1', 'mockBlock2', 'mockBlock3'], - }, - 'appState': { - 'menuOpen': false, - 'currentView': { - 'name': 'accountDetail', - 'detailView': null, - 'context': '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - }, - 'accountDetail': { - 'subview': 'transactions', - }, - 'modal': { - 'modalState': {}, - 'previousModalState': {}, - }, - 'transForward': true, - 'isLoading': false, - 'warning': null, - 'scrollToBottom': false, - 'forgottenPassword': null, - }, - 'identities': {}, - 'send': { - 'fromDropdownOpen': false, - 'toDropdownOpen': false, - 'errors': { 'someError': null }, - }, -} diff --git a/ui/app/components/send_/tests/send-selectors.test.js b/ui/app/components/send_/tests/send-selectors.test.js deleted file mode 100644 index 218da656b..000000000 --- a/ui/app/components/send_/tests/send-selectors.test.js +++ /dev/null @@ -1,685 +0,0 @@ -import assert from 'assert' -import sinon from 'sinon' -import selectors from '../send.selectors.js' -const { - accountsWithSendEtherInfoSelector, - // autoAddToBetaUI, - getAddressBook, - getBlockGasLimit, - getAmountConversionRate, - getConversionRate, - getCurrentAccountWithSendEtherInfo, - getCurrentCurrency, - getCurrentNetwork, - getCurrentViewContext, - getForceGasMin, - getGasLimit, - getGasPrice, - getGasTotal, - getPrimaryCurrency, - getRecentBlocks, - getSelectedAccount, - getSelectedAddress, - getSelectedIdentity, - getSelectedToken, - getSelectedTokenContract, - getSelectedTokenExchangeRate, - getSelectedTokenToFiatRate, - getSendAmount, - getSendEditingTransactionId, - getSendErrors, - getSendFrom, - getSendFromBalance, - getSendFromObject, - getSendMaxModeState, - getSendTo, - getSendToAccounts, - getTokenBalance, - getTokenExchangeRate, - getUnapprovedTxs, - transactionsSelector, -} = selectors -import mockState from './send-selectors-test-data' - -describe('send selectors', () => { - const tempGlobalEth = Object.assign({}, global.eth) - beforeEach(() => { - global.eth = { - contract: sinon.stub().returns({ - at: address => 'mockAt:' + address, - }), - } - }) - - afterEach(() => { - global.eth = tempGlobalEth - }) - - describe('accountsWithSendEtherInfoSelector()', () => { - it('should return an array of account objects with name info from identities', () => { - assert.deepEqual( - accountsWithSendEtherInfoSelector(mockState), - [ - { - code: '0x', - balance: '0x47c9d71831c76efe', - nonce: '0x1b', - address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', - name: 'Send Account 1', - }, - { - code: '0x', - balance: '0x37452b1315889f80', - nonce: '0xa', - address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - name: 'Send Account 2', - }, - { - code: '0x', - balance: '0x30c9d71831c76efe', - nonce: '0x1c', - address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', - name: 'Send Account 3', - }, - { - code: '0x', - balance: '0x0', - nonce: '0x0', - address: '0xd85a4b6a394794842887b8284293d69163007bbb', - name: 'Send Account 4', - }, - ] - ) - }) - }) - - // describe('autoAddToBetaUI()', () => { - // it('should', () => { - // assert.deepEqual( - // autoAddToBetaUI(mockState), - - // ) - // }) - // }) - - describe('getAddressBook()', () => { - it('should return the address book', () => { - assert.deepEqual( - getAddressBook(mockState), - [ - { - address: '0x06195827297c7a80a443b6894d3bdb8824b43896', - name: 'Address Book Account 1', - }, - ], - ) - }) - }) - - describe('getAmountConversionRate()', () => { - it('should return the token conversion rate if a token is selected', () => { - assert.equal( - getAmountConversionRate(mockState), - 2401.76400654 - ) - }) - - it('should return the eth conversion rate if no token is selected', () => { - const editedMockState = { - metamask: Object.assign({}, mockState.metamask, { selectedTokenAddress: null }), - } - assert.equal( - getAmountConversionRate(editedMockState), - 1200.88200327 - ) - }) - }) - - describe('getBlockGasLimit', () => { - it('should return the current block gas limit', () => { - assert.deepEqual( - getBlockGasLimit(mockState), - '0x4c1878' - ) - }) - }) - - describe('getConversionRate()', () => { - it('should return the eth conversion rate', () => { - assert.deepEqual( - getConversionRate(mockState), - 1200.88200327 - ) - }) - }) - - describe('getCurrentAccountWithSendEtherInfo()', () => { - it('should return the currently selected account with identity info', () => { - assert.deepEqual( - getCurrentAccountWithSendEtherInfo(mockState), - { - code: '0x', - balance: '0x0', - nonce: '0x0', - address: '0xd85a4b6a394794842887b8284293d69163007bbb', - name: 'Send Account 4', - } - ) - }) - }) - - describe('getCurrentCurrency()', () => { - it('should return the currently selected currency', () => { - assert.equal( - getCurrentCurrency(mockState), - 'USD' - ) - }) - }) - - describe('getCurrentNetwork()', () => { - it('should return the id of the currently selected network', () => { - assert.equal( - getCurrentNetwork(mockState), - '3' - ) - }) - }) - - describe('getCurrentViewContext()', () => { - it('should return the context of the current view', () => { - assert.equal( - getCurrentViewContext(mockState), - '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' - ) - }) - }) - - describe('getForceGasMin()', () => { - it('should get the send.forceGasMin property', () => { - assert.equal( - getForceGasMin(mockState), - true - ) - }) - }) - - describe('getGasLimit()', () => { - it('should return the send.gasLimit', () => { - assert.equal( - getGasLimit(mockState), - '0xFFFF' - ) - }) - }) - - describe('getGasPrice()', () => { - it('should return the send.gasPrice', () => { - assert.equal( - getGasPrice(mockState), - '0xaa' - ) - }) - }) - - describe('getGasTotal()', () => { - it('should return the send.gasTotal', () => { - assert.equal( - getGasTotal(mockState), - '0xb451dc41b578' - ) - }) - }) - - describe('getPrimaryCurrency()', () => { - it('should return the symbol of the selected token', () => { - assert.equal( - getPrimaryCurrency(mockState), - 'DEF' - ) - }) - }) - - describe('getRecentBlocks()', () => { - it('should return the recent blocks', () => { - assert.deepEqual( - getRecentBlocks(mockState), - ['mockBlock1', 'mockBlock2', 'mockBlock3'] - ) - }) - }) - - describe('getSelectedAccount()', () => { - it('should return the currently selected account', () => { - assert.deepEqual( - getSelectedAccount(mockState), - { - code: '0x', - balance: '0x0', - nonce: '0x0', - address: '0xd85a4b6a394794842887b8284293d69163007bbb', - } - ) - }) - }) - - describe('getSelectedAddress()', () => { - it('should', () => { - assert.equal( - getSelectedAddress(mockState), - '0xd85a4b6a394794842887b8284293d69163007bbb' - ) - }) - }) - - describe('getSelectedIdentity()', () => { - it('should return the identity object of the currently selected address', () => { - assert.deepEqual( - getSelectedIdentity(mockState), - { - address: '0xd85a4b6a394794842887b8284293d69163007bbb', - name: 'Send Account 4', - } - ) - }) - }) - - describe('getSelectedToken()', () => { - it('should return the currently selected token if selected', () => { - assert.deepEqual( - getSelectedToken(mockState), - { - address: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - decimals: 4, - symbol: 'DEF', - } - ) - }) - - it('should return the send token if none is currently selected, but a send token exists', () => { - const mockSendToken = { - address: '0x123456708414189a58339873ab429b6c47ab92d3', - decimals: 4, - symbol: 'JKL', - } - const editedMockState = { - metamask: Object.assign({}, mockState.metamask, { - selectedTokenAddress: null, - send: { - token: mockSendToken, - }, - }), - } - assert.deepEqual( - getSelectedToken(editedMockState), - Object.assign({}, mockSendToken) - ) - }) - }) - - describe('getSelectedTokenContract()', () => { - it('should return the contract at the selected token address', () => { - assert.equal( - getSelectedTokenContract(mockState), - 'mockAt:0x8d6b81208414189a58339873ab429b6c47ab92d3' - ) - }) - - it('should return null if no token is selected', () => { - const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false }) - assert.equal( - getSelectedTokenContract(Object.assign({}, mockState, { metamask: modifiedMetamaskState })), - null - ) - }) - }) - - describe('getSelectedTokenExchangeRate()', () => { - it('should return the exchange rate for the selected token', () => { - assert.equal( - getSelectedTokenExchangeRate(mockState), - 2.0 - ) - }) - }) - - describe('getSelectedTokenToFiatRate()', () => { - it('should return rate for converting the selected token to fiat', () => { - assert.equal( - getSelectedTokenToFiatRate(mockState), - 2401.76400654 - ) - }) - }) - - describe('getSendAmount()', () => { - it('should return the send.amount', () => { - assert.equal( - getSendAmount(mockState), - '0x080' - ) - }) - }) - - describe('getSendEditingTransactionId()', () => { - it('should return the send.editingTransactionId', () => { - assert.equal( - getSendEditingTransactionId(mockState), - 97531 - ) - }) - }) - - describe('getSendErrors()', () => { - it('should return the send.errors', () => { - assert.deepEqual( - getSendErrors(mockState), - { someError: null } - ) - }) - }) - - describe('getSendFrom()', () => { - it('should return the send.from', () => { - assert.deepEqual( - getSendFrom(mockState), - { - address: '0xabcdefg', - balance: '0x5f4e3d2c1', - } - ) - }) - }) - - describe('getSendFromBalance()', () => { - it('should get the send.from balance if it exists', () => { - assert.equal( - getSendFromBalance(mockState), - '0x5f4e3d2c1' - ) - }) - - it('should get the selected account balance if the send.from does not exist', () => { - const editedMockState = { - metamask: Object.assign({}, mockState.metamask, { - send: { - from: null, - }, - }), - } - assert.equal( - getSendFromBalance(editedMockState), - '0x0' - ) - }) - }) - - describe('getSendFromObject()', () => { - it('should return send.from if it exists', () => { - assert.deepEqual( - getSendFromObject(mockState), - { - address: '0xabcdefg', - balance: '0x5f4e3d2c1', - } - ) - }) - - it('should return the current account with send ether info if send.from does not exist', () => { - const editedMockState = { - metamask: Object.assign({}, mockState.metamask, { - send: { - from: null, - }, - }), - } - assert.deepEqual( - getSendFromObject(editedMockState), - { - code: '0x', - balance: '0x0', - nonce: '0x0', - address: '0xd85a4b6a394794842887b8284293d69163007bbb', - name: 'Send Account 4', - } - ) - }) - }) - - describe('getSendMaxModeState()', () => { - it('should return send.maxModeOn', () => { - assert.equal( - getSendMaxModeState(mockState), - false - ) - }) - }) - - describe('getSendTo()', () => { - it('should return send.to', () => { - assert.equal( - getSendTo(mockState), - '0x987fedabc' - ) - }) - }) - - describe('getSendToAccounts()', () => { - it('should return an array including all the users accounts and the address book', () => { - assert.deepEqual( - getSendToAccounts(mockState), - [ - { - code: '0x', - balance: '0x47c9d71831c76efe', - nonce: '0x1b', - address: '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825', - name: 'Send Account 1', - }, - { - code: '0x', - balance: '0x37452b1315889f80', - nonce: '0xa', - address: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - name: 'Send Account 2', - }, - { - code: '0x', - balance: '0x30c9d71831c76efe', - nonce: '0x1c', - address: '0x2f8d4a878cfa04a6e60d46362f5644deab66572d', - name: 'Send Account 3', - }, - { - code: '0x', - balance: '0x0', - nonce: '0x0', - address: '0xd85a4b6a394794842887b8284293d69163007bbb', - name: 'Send Account 4', - }, - { - address: '0x06195827297c7a80a443b6894d3bdb8824b43896', - name: 'Address Book Account 1', - }, - ] - ) - }) - }) - - describe('getTokenBalance()', () => { - it('should', () => { - assert.equal( - getTokenBalance(mockState), - 3434 - ) - }) - }) - - describe('getTokenExchangeRate()', () => { - it('should return the passed tokens exchange rates', () => { - assert.equal( - getTokenExchangeRate(mockState, 'GHI'), - 31.01 - ) - }) - }) - - describe('getUnapprovedTxs()', () => { - it('should return the unapproved txs', () => { - assert.deepEqual( - getUnapprovedTxs(mockState), - { - 4768706228115573: { - id: 4768706228115573, - time: 1487363153561, - status: 'unapproved', - gasMultiplier: 1, - metamaskNetworkId: '3', - txParams: { - from: '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - to: '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761', - value: '0xde0b6b3a7640000', - metamaskId: 4768706228115573, - metamaskNetworkId: '3', - gas: '0x5209', - }, - gasLimitSpecified: false, - estimatedGas: '0x5209', - txFee: '17e0186e60800', - txValue: 'de0b6b3a7640000', - maxCost: 'de234b52e4a0800', - gasPrice: '4a817c800', - }, - } - ) - }) - }) - - describe('transactionsSelector()', () => { - it('should return the selected addresses selected token transactions', () => { - assert.deepEqual( - transactionsSelector(mockState), - [ - { - id: 'mockTokenTx1', - txParams: { - to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - }, - time: 1700000000000, - }, - { - id: 'mockTokenTx3', - txParams: { - to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - }, - time: 1500000000000, - }, - ] - ) - }) - - it('should return all transactions if no token is selected', () => { - const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false }) - const modifiedState = Object.assign({}, mockState, { metamask: modifiedMetamaskState }) - assert.deepEqual( - transactionsSelector(modifiedState), - [ - { - id: 'mockTokenTx1', - time: 1700000000000, - txParams: { - to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - }, - }, - { - id: 'unapprovedMessage1', - time: 1650000000000, - }, - { - id: 'mockTokenTx2', - time: 1600000000000, - txParams: { - to: '0xafaketokenaddress', - }, - }, - { - id: 'unapprovedMessage2', - time: 1550000000000, - }, - { - id: 'mockTokenTx3', - time: 1500000000000, - txParams: { - to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - }, - }, - { - id: 'unapprovedMessage3', - time: 1450000000000, - }, - { - id: 'mockEthTx1', - time: 1400000000000, - txParams: { - to: '0xd85a4b6a394794842887b8284293d69163007bbb', - }, - }, - ] - ) - }) - - it('should return shapeshift transactions if current network is 1', () => { - const modifiedMetamaskState = Object.assign({}, mockState.metamask, { selectedTokenAddress: false, network: '1' }) - const modifiedState = Object.assign({}, mockState, { metamask: modifiedMetamaskState }) - assert.deepEqual( - transactionsSelector(modifiedState), - [ - { - id: 'mockTokenTx1', - time: 1700000000000, - txParams: { - to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - }, - }, - { id: 'shapeShiftTx1', 'time': 1675000000000 }, - { - id: 'unapprovedMessage1', - time: 1650000000000, - }, - { - id: 'mockTokenTx2', - time: 1600000000000, - txParams: { - to: '0xafaketokenaddress', - }, - }, - { id: 'shapeShiftTx2', 'time': 1575000000000 }, - { - id: 'unapprovedMessage2', - time: 1550000000000, - }, - { - id: 'mockTokenTx3', - time: 1500000000000, - txParams: { - to: '0x8d6b81208414189a58339873ab429b6c47ab92d3', - }, - }, - { id: 'shapeShiftTx3', 'time': 1475000000000 }, - { - id: 'unapprovedMessage3', - time: 1450000000000, - }, - { - id: 'mockEthTx1', - time: 1400000000000, - txParams: { - to: '0xd85a4b6a394794842887b8284293d69163007bbb', - }, - }, - ] - ) - }) - }) - -}) diff --git a/ui/app/components/send_/tests/send-utils.test.js b/ui/app/components/send_/tests/send-utils.test.js deleted file mode 100644 index b8579e0e4..000000000 --- a/ui/app/components/send_/tests/send-utils.test.js +++ /dev/null @@ -1,486 +0,0 @@ -import assert from 'assert' -import sinon from 'sinon' -import proxyquire from 'proxyquire' -import { - BASE_TOKEN_GAS_COST, - ONE_GWEI_IN_WEI_HEX, - SIMPLE_GAS_COST, -} from '../send.constants' -const { - addCurrencies, - subtractCurrencies, -} = require('../../../conversion-util') - -const { - INSUFFICIENT_FUNDS_ERROR, - INSUFFICIENT_TOKENS_ERROR, -} = require('../send.constants') - -const stubs = { - addCurrencies: sinon.stub().callsFake((a, b, obj) => { - if (String(a).match(/^0x.+/)) a = Number(String(a).slice(2)) - if (String(b).match(/^0x.+/)) b = Number(String(b).slice(2)) - return a + b - }), - conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)), - conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value), - multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`), - calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d), - rawEncode: sinon.stub().returns([16, 1100]), - conversionGreaterThan: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value), - conversionLessThan: sinon.stub().callsFake((obj1, obj2) => obj1.value < obj2.value), -} - -const sendUtils = proxyquire('../send.utils.js', { - '../../conversion-util': { - addCurrencies: stubs.addCurrencies, - conversionUtil: stubs.conversionUtil, - conversionGTE: stubs.conversionGTE, - multiplyCurrencies: stubs.multiplyCurrencies, - conversionGreaterThan: stubs.conversionGreaterThan, - conversionLessThan: stubs.conversionLessThan, - }, - '../../token-util': { calcTokenAmount: stubs.calcTokenAmount }, - 'ethereumjs-abi': { - rawEncode: stubs.rawEncode, - }, -}) - -const { - calcGasTotal, - estimateGas, - doesAmountErrorRequireUpdate, - estimateGasPriceFromRecentBlocks, - generateTokenTransferData, - getAmountErrorObject, - getGasFeeErrorObject, - getToAddressForGasUpdate, - calcTokenBalance, - isBalanceSufficient, - isTokenBalanceSufficient, -} = sendUtils - -describe('send utils', () => { - - describe('calcGasTotal()', () => { - it('should call multiplyCurrencies with the correct params and return the multiplyCurrencies return', () => { - const result = calcGasTotal(12, 15) - assert.equal(result, '12x15') - const call_ = stubs.multiplyCurrencies.getCall(0).args - assert.deepEqual( - call_, - [12, 15, { - toNumericBase: 'hex', - multiplicandBase: 16, - multiplierBase: 16, - } ] - ) - }) - }) - - describe('doesAmountErrorRequireUpdate()', () => { - const config = { - 'should return true if balances are different': { - balance: 0, - prevBalance: 1, - expectedResult: true, - }, - 'should return true if gasTotals are different': { - gasTotal: 0, - prevGasTotal: 1, - expectedResult: true, - }, - 'should return true if token balances are different': { - tokenBalance: 0, - prevTokenBalance: 1, - selectedToken: 'someToken', - expectedResult: true, - }, - 'should return false if they are all the same': { - balance: 1, - prevBalance: 1, - gasTotal: 1, - prevGasTotal: 1, - tokenBalance: 1, - prevTokenBalance: 1, - selectedToken: 'someToken', - expectedResult: false, - }, - } - Object.entries(config).map(([description, obj]) => { - it(description, () => { - assert.equal(doesAmountErrorRequireUpdate(obj), obj.expectedResult) - }) - }) - - }) - - describe('generateTokenTransferData()', () => { - it('should return undefined if not passed a selected token', () => { - assert.equal(generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: false}), undefined) - }) - - it('should call abi.rawEncode with the correct params', () => { - stubs.rawEncode.resetHistory() - generateTokenTransferData({ toAddress: 'mockAddress', amount: 'ab', selectedToken: true}) - assert.deepEqual( - stubs.rawEncode.getCall(0).args, - [['address', 'uint256'], ['mockAddress', '0xab']] - ) - }) - - it('should return encoded token transfer data', () => { - assert.equal( - generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: true}), - '0xa9059cbb104c' - ) - }) - }) - - describe('getAmountErrorObject()', () => { - const config = { - 'should return insufficientFunds error if isBalanceSufficient returns false': { - amount: 15, - amountConversionRate: 2, - balance: 1, - conversionRate: 3, - gasTotal: 17, - primaryCurrency: 'ABC', - expectedResult: { amount: INSUFFICIENT_FUNDS_ERROR }, - }, - 'should not return insufficientFunds error if selectedToken is truthy': { - amount: '0x0', - amountConversionRate: 2, - balance: 1, - conversionRate: 3, - gasTotal: 17, - primaryCurrency: 'ABC', - selectedToken: { symbole: 'DEF', decimals: 0 }, - decimals: 0, - tokenBalance: 'sometokenbalance', - expectedResult: { amount: null }, - }, - 'should return insufficientTokens error if token is selected and isTokenBalanceSufficient returns false': { - amount: '0x10', - amountConversionRate: 2, - balance: 100, - conversionRate: 3, - decimals: 10, - gasTotal: 17, - primaryCurrency: 'ABC', - selectedToken: 'someToken', - tokenBalance: 123, - expectedResult: { amount: INSUFFICIENT_TOKENS_ERROR }, - }, - } - Object.entries(config).map(([description, obj]) => { - it(description, () => { - assert.deepEqual(getAmountErrorObject(obj), obj.expectedResult) - }) - }) - }) - - describe('getGasFeeErrorObject()', () => { - const config = { - 'should return insufficientFunds error if isBalanceSufficient returns false': { - amountConversionRate: 2, - balance: 16, - conversionRate: 3, - gasTotal: 17, - primaryCurrency: 'ABC', - expectedResult: { gasFee: INSUFFICIENT_FUNDS_ERROR }, - }, - 'should return null error if isBalanceSufficient returns true': { - amountConversionRate: 2, - balance: 16, - conversionRate: 3, - gasTotal: 15, - primaryCurrency: 'ABC', - expectedResult: { gasFee: null }, - }, - } - Object.entries(config).map(([description, obj]) => { - it(description, () => { - assert.deepEqual(getGasFeeErrorObject(obj), obj.expectedResult) - }) - }) - }) - - describe('calcTokenBalance()', () => { - it('should return the calculated token blance', () => { - assert.equal(calcTokenBalance({ - selectedToken: { - decimals: 11, - }, - usersToken: { - balance: 20, - }, - }), 'calc:2011') - }) - }) - - describe('isBalanceSufficient()', () => { - it('should correctly call addCurrencies and return the result of calling conversionGTE', () => { - stubs.conversionGTE.resetHistory() - const result = isBalanceSufficient({ - amount: 15, - amountConversionRate: 2, - balance: 100, - conversionRate: 3, - gasTotal: 17, - primaryCurrency: 'ABC', - }) - assert.deepEqual( - stubs.addCurrencies.getCall(0).args, - [ - 15, 17, { - aBase: 16, - bBase: 16, - toNumericBase: 'hex', - }, - ] - ) - assert.deepEqual( - stubs.conversionGTE.getCall(0).args, - [ - { - value: 100, - fromNumericBase: 'hex', - fromCurrency: 'ABC', - conversionRate: 3, - }, - { - value: 32, - fromNumericBase: 'hex', - conversionRate: 2, - fromCurrency: 'ABC', - }, - ] - ) - - assert.equal(result, true) - }) - }) - - describe('isTokenBalanceSufficient()', () => { - it('should correctly call conversionUtil and return the result of calling conversionGTE', () => { - stubs.conversionGTE.resetHistory() - stubs.conversionUtil.resetHistory() - const result = isTokenBalanceSufficient({ - amount: '0x10', - tokenBalance: 123, - decimals: 10, - }) - assert.deepEqual( - stubs.conversionUtil.getCall(0).args, - [ - '0x10', { - fromNumericBase: 'hex', - }, - ] - ) - assert.deepEqual( - stubs.conversionGTE.getCall(0).args, - [ - { - value: 123, - fromNumericBase: 'dec', - }, - { - value: 'calc:1610', - fromNumericBase: 'dec', - }, - ] - ) - - assert.equal(result, false) - }) - }) - - describe('estimateGas', () => { - const baseMockParams = { - blockGasLimit: '0x64', - selectedAddress: 'mockAddress', - to: '0xisContract', - estimateGasMethod: sinon.stub().callsFake( - (data, cb) => cb( - data.to.match(/willFailBecauseOf:/) ? { message: data.to.match(/:(.+)$/)[1] } : null, - { toString: (n) => `0xabc${n}` } - ) - ), - } - const baseExpectedCall = { - from: 'mockAddress', - gas: '0x64x0.95', - to: '0xisContract', - } - - beforeEach(() => { - global.eth = { - getCode: sinon.stub().callsFake( - (address) => Promise.resolve(address.match(/isContract/) ? 'not-0x' : '0x') - ), - } - }) - - afterEach(() => { - baseMockParams.estimateGasMethod.resetHistory() - global.eth.getCode.resetHistory() - }) - - it('should call ethQuery.estimateGas with the expected params', async () => { - const result = await sendUtils.estimateGas(baseMockParams) - assert.equal(baseMockParams.estimateGasMethod.callCount, 1) - assert.deepEqual( - baseMockParams.estimateGasMethod.getCall(0).args[0], - Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall) - ) - assert.equal(result, '0xabc16') - }) - - it('should call ethQuery.estimateGas with the expected params when initialGasLimitHex is lower than the upperGasLimit', async () => { - const result = await estimateGas(Object.assign({}, baseMockParams, { blockGasLimit: '0xbcd' })) - assert.equal(baseMockParams.estimateGasMethod.callCount, 1) - assert.deepEqual( - baseMockParams.estimateGasMethod.getCall(0).args[0], - Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall, { gas: '0xbcdx0.95' }) - ) - assert.equal(result, '0xabc16x1.5') - }) - - it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => { - const result = await estimateGas(Object.assign({ data: 'mockData', selectedToken: { address: 'mockAddress' } }, baseMockParams)) - assert.equal(baseMockParams.estimateGasMethod.callCount, 1) - assert.deepEqual( - baseMockParams.estimateGasMethod.getCall(0).args[0], - Object.assign({}, baseExpectedCall, { - gasPrice: undefined, - value: '0x0', - data: '0xa9059cbb104c', - to: 'mockAddress', - }) - ) - assert.equal(result, '0xabc16') - }) - - it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => { - assert.equal(baseMockParams.estimateGasMethod.callCount, 0) - const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123' })) - assert.equal(result, SIMPLE_GAS_COST) - }) - - it(`should return ${SIMPLE_GAS_COST} if not passed a selectedToken or truthy to address`, async () => { - assert.equal(baseMockParams.estimateGasMethod.callCount, 0) - const result = await estimateGas(Object.assign({}, baseMockParams, { to: null })) - assert.equal(result, SIMPLE_GAS_COST) - }) - - it(`should not return ${SIMPLE_GAS_COST} if passed a selectedToken`, async () => { - assert.equal(baseMockParams.estimateGasMethod.callCount, 0) - const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } })) - assert.notEqual(result, SIMPLE_GAS_COST) - }) - - it(`should return ${BASE_TOKEN_GAS_COST} if passed a selectedToken but no to address`, async () => { - const result = await estimateGas(Object.assign({}, baseMockParams, { to: null, selectedToken: { address: '' } })) - assert.equal(result, BASE_TOKEN_GAS_COST) - }) - - it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => { - const result = await estimateGas(Object.assign({}, baseMockParams, { - to: 'isContract willFailBecauseOf:Transaction execution error.', - })) - assert.equal(result, '0x64x0.95') - }) - - it(`should return the adjusted blockGasLimit if it fails with a 'gas required exceeds allowance or always failing transaction.'`, async () => { - const result = await estimateGas(Object.assign({}, baseMockParams, { - to: 'isContract willFailBecauseOf:gas required exceeds allowance or always failing transaction.', - })) - assert.equal(result, '0x64x0.95') - }) - - it(`should reject other errors`, async () => { - try { - await estimateGas(Object.assign({}, baseMockParams, { - to: 'isContract willFailBecauseOf:some other error', - })) - } catch (err) { - assert.deepEqual(err, { message: 'some other error' }) - } - }) - }) - - describe('estimateGasPriceFromRecentBlocks', () => { - const ONE_GWEI_IN_WEI_HEX_PLUS_ONE = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', { - aBase: 16, - bBase: 16, - toNumericBase: 'hex', - }) - const ONE_GWEI_IN_WEI_HEX_PLUS_TWO = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x2', { - aBase: 16, - bBase: 16, - toNumericBase: 'hex', - }) - const ONE_GWEI_IN_WEI_HEX_MINUS_ONE = subtractCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', { - aBase: 16, - bBase: 16, - toNumericBase: 'hex', - }) - - it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is falsy`, () => { - assert.equal(estimateGasPriceFromRecentBlocks(), ONE_GWEI_IN_WEI_HEX) - }) - - it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is empty`, () => { - assert.equal(estimateGasPriceFromRecentBlocks([]), ONE_GWEI_IN_WEI_HEX) - }) - - it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has no gas prices`, () => { - const mockRecentBlocks = [ - { gasPrices: null }, - { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] }, - { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] }, - ] - assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX) - }) - - it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has empty gas prices`, () => { - const mockRecentBlocks = [ - { gasPrices: [] }, - { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] }, - { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] }, - ] - assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX) - }) - - it(`should return the middle value of all blocks lowest prices`, () => { - const mockRecentBlocks = [ - { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_TWO ] }, - { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] }, - { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] }, - ] - assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX_PLUS_ONE) - }) - - it(`should work if a block has multiple gas prices`, () => { - const mockRecentBlocks = [ - { gasPrices: [ '0x1', '0x2', '0x3', '0x4', '0x5' ] }, - { gasPrices: [ '0x101', '0x100', '0x103', '0x104', '0x102' ] }, - { gasPrices: [ '0x150', '0x50', '0x100', '0x200', '0x5' ] }, - ] - assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5') - }) - }) - - describe('getToAddressForGasUpdate()', () => { - it('should return empty string if all params are undefined or null', () => { - assert.equal(getToAddressForGasUpdate(undefined, null), '') - }) - - it('should return the first string that is not defined or null in lower case', () => { - assert.equal(getToAddressForGasUpdate('A', null), 'a') - assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b') - }) - }) -}) -- cgit From 2e0cbf210e4c7096f1b7843ac9b31dd92b312355 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 2 Jul 2018 22:01:57 -0230 Subject: Fix lint warnings in the send/ directory. --- ui/app/components/send/currency-display/index.js | 2 +- ui/app/components/send/tests/send-utils.test.js | 26 ++++++++++++++++++++ ui/app/components/send/to-autocomplete/index.js | 2 +- ui/app/components/send_/send.utils.test.js | 30 ------------------------ 4 files changed, 28 insertions(+), 32 deletions(-) delete mode 100644 ui/app/components/send_/send.utils.test.js (limited to 'ui') diff --git a/ui/app/components/send/currency-display/index.js b/ui/app/components/send/currency-display/index.js index 0185a19e9..5dc269c5a 100644 --- a/ui/app/components/send/currency-display/index.js +++ b/ui/app/components/send/currency-display/index.js @@ -1 +1 @@ -export { default } from './currency-display.js' \ No newline at end of file +export { default } from './currency-display.js' diff --git a/ui/app/components/send/tests/send-utils.test.js b/ui/app/components/send/tests/send-utils.test.js index b8579e0e4..18dde495a 100644 --- a/ui/app/components/send/tests/send-utils.test.js +++ b/ui/app/components/send/tests/send-utils.test.js @@ -58,6 +58,7 @@ const { calcTokenBalance, isBalanceSufficient, isTokenBalanceSufficient, + removeLeadingZeroes, } = sendUtils describe('send utils', () => { @@ -483,4 +484,29 @@ describe('send utils', () => { assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b') }) }) + + describe('removeLeadingZeroes()', () => { + it('should remove leading zeroes from int when user types', () => { + assert.equal(removeLeadingZeroes('0'), '0') + assert.equal(removeLeadingZeroes('1'), '1') + assert.equal(removeLeadingZeroes('00'), '0') + assert.equal(removeLeadingZeroes('01'), '1') + }) + + it('should remove leading zeroes from int when user copy/paste', () => { + assert.equal(removeLeadingZeroes('001'), '1') + }) + + it('should remove leading zeroes from float when user types', () => { + assert.equal(removeLeadingZeroes('0.'), '0.') + assert.equal(removeLeadingZeroes('0.0'), '0.0') + assert.equal(removeLeadingZeroes('0.00'), '0.00') + assert.equal(removeLeadingZeroes('0.001'), '0.001') + assert.equal(removeLeadingZeroes('0.10'), '0.10') + }) + + it('should remove leading zeroes from float when user copy/paste', () => { + assert.equal(removeLeadingZeroes('00.1'), '0.1') + }) + }) }) diff --git a/ui/app/components/send/to-autocomplete/index.js b/ui/app/components/send/to-autocomplete/index.js index afa2eb5a4..244d301d1 100644 --- a/ui/app/components/send/to-autocomplete/index.js +++ b/ui/app/components/send/to-autocomplete/index.js @@ -1 +1 @@ -export { default } from './to-autocomplete.js' \ No newline at end of file +export { default } from './to-autocomplete.js' diff --git a/ui/app/components/send_/send.utils.test.js b/ui/app/components/send_/send.utils.test.js deleted file mode 100644 index 36f3a5c10..000000000 --- a/ui/app/components/send_/send.utils.test.js +++ /dev/null @@ -1,30 +0,0 @@ -import assert from 'assert' -import { removeLeadingZeroes } from './send.utils' - - -describe('send utils', () => { - describe('removeLeadingZeroes()', () => { - it('should remove leading zeroes from int when user types', () => { - assert.equal(removeLeadingZeroes('0'), '0') - assert.equal(removeLeadingZeroes('1'), '1') - assert.equal(removeLeadingZeroes('00'), '0') - assert.equal(removeLeadingZeroes('01'), '1') - }) - - it('should remove leading zeroes from int when user copy/paste', () => { - assert.equal(removeLeadingZeroes('001'), '1') - }) - - it('should remove leading zeroes from float when user types', () => { - assert.equal(removeLeadingZeroes('0.'), '0.') - assert.equal(removeLeadingZeroes('0.0'), '0.0') - assert.equal(removeLeadingZeroes('0.00'), '0.00') - assert.equal(removeLeadingZeroes('0.001'), '0.001') - assert.equal(removeLeadingZeroes('0.10'), '0.10') - }) - - it('should remove leading zeroes from float when user copy/paste', () => { - assert.equal(removeLeadingZeroes('00.1'), '0.1') - }) - }) -}) -- cgit From 040b8c59c4302479d786287299007bbbc98144e1 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Mon, 16 Jul 2018 13:07:27 -0230 Subject: Fix send import path in ToAutoComplete --- ui/app/components/send/to-autocomplete.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui') diff --git a/ui/app/components/send/to-autocomplete.component.js b/ui/app/components/send/to-autocomplete.component.js index 19f534b94..9e270db75 100644 --- a/ui/app/components/send/to-autocomplete.component.js +++ b/ui/app/components/send/to-autocomplete.component.js @@ -1,7 +1,7 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import AccountListItem from '../send_/account-list-item/account-list-item.component' +import AccountListItem from '../send/account-list-item/account-list-item.component' export default class ToAutoComplete extends Component { -- cgit From d38405fb0e066267919fb68dc56c53b229a818d5 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Mon, 16 Jul 2018 13:06:21 -0230 Subject: Fix send import path in customize gas modal --- ui/app/components/modals/customize-gas/customize-gas.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui') diff --git a/ui/app/components/modals/customize-gas/customize-gas.component.js b/ui/app/components/modals/customize-gas/customize-gas.component.js index d17c290b6..0337c5413 100644 --- a/ui/app/components/modals/customize-gas/customize-gas.component.js +++ b/ui/app/components/modals/customize-gas/customize-gas.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import GasModalCard from '../../customize-gas-modal/gas-modal-card' -import { MIN_GAS_PRICE_GWEI } from '../../send_/send.constants' +import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants' import { getDecimalGasLimit, -- cgit From 5c3efe8bc47a9f8e7a7e0952589c1a2f42ec4eca Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Mon, 16 Jul 2018 13:06:46 -0230 Subject: Fix send import paths in confirm tx components --- .../confirm-transaction-base/confirm-transaction-base.component.js | 2 +- .../confirm-transaction-base/confirm-transaction-base.container.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js index 842b34d2e..5327b116f 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container' import { formatCurrency } from '../../../helpers/confirm-transaction/util' -import { isBalanceSufficient } from '../../send_/send.utils' +import { isBalanceSufficient } from '../../send/send.utils' import { DEFAULT_ROUTE } from '../../../routes' import { INSUFFICIENT_FUNDS_ERROR_KEY, diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js index 31108bbd0..7db39adec 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -13,9 +13,9 @@ import { GAS_LIMIT_TOO_LOW_ERROR_KEY, } from '../../../constants/error-keys' import { getHexGasTotal } from '../../../helpers/confirm-transaction/util' -import { isBalanceSufficient } from '../../send_/send.utils' +import { isBalanceSufficient } from '../../send/send.utils' import { conversionGreaterThan } from '../../../conversion-util' -import { MIN_GAS_LIMIT_DEC } from '../../send_/send.constants' +import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants' const mapStateToProps = (state, props) => { const { toAddress: propsToAddress } = props -- cgit From 8d8de0508ab3cb67870d0c00469ee39f3be06714 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Wed, 11 Jul 2018 15:00:14 -0400 Subject: Fixes conversion status for tokens without conversion rates --- .../send/currency-display/currency-display.js | 43 +++++++++++++++------- 1 file changed, 29 insertions(+), 14 deletions(-) (limited to 'ui') diff --git a/ui/app/components/send/currency-display/currency-display.js b/ui/app/components/send/currency-display/currency-display.js index 1b9f7738c..70fc9de70 100644 --- a/ui/app/components/send/currency-display/currency-display.js +++ b/ui/app/components/send/currency-display/currency-display.js @@ -82,16 +82,21 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu numberOfDecimals: 2, conversionRate, }) - convertedValue = Number(convertedValue).toFixed(2) - const upperCaseCurrencyCode = convertedCurrency.toUpperCase() - - return currencies.find(currency => currency.code === upperCaseCurrencyCode) - ? currencyFormatter.format(Number(convertedValue), { - code: upperCaseCurrencyCode, - }) - : convertedValue -} + if (conversionRate == 0 && nonFormattedValue != 0) { + convertedValue = null + return convertedValue + } + else { + convertedValue == Number(convertedValue).toFixed(2) + const upperCaseCurrencyCode = convertedCurrency.toUpperCase() + return currencies.find(currency => currency.code === upperCaseCurrencyCode) + ? currencyFormatter.format(Number(convertedValue), { + code: upperCaseCurrencyCode, + }) + : convertedValue + } + } CurrencyDisplay.prototype.handleChange = function (newVal) { this.setState({ valueToRender: removeLeadingZeroes(newVal) }) @@ -105,6 +110,7 @@ CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) { return (valueLength + decimalPointDeficit + 0.75) + 'ch' } + CurrencyDisplay.prototype.render = function () { const { className = 'currency-display', @@ -121,6 +127,19 @@ CurrencyDisplay.prototype.render = function () { const convertedValueToRender = this.getConvertedValueToRender(valueToRender) + function onlyRenderConversions() { + if (convertedValueToRender == null) { + return h('div', { + className: convertedBalanceClassName, + }, 'No Conversion Rate') + } + else { + return h('div', { + className: convertedBalanceClassName, + }, `${convertedValueToRender} ${convertedCurrency.toUpperCase()}`) + } + } + return h('div', { className, style: { @@ -157,11 +176,7 @@ CurrencyDisplay.prototype.render = function () { ]), - ]), - - h('div', { - className: convertedBalanceClassName, - }, `${convertedValueToRender} ${convertedCurrency.toUpperCase()}`), + ]), onlyRenderConversions(), ]) -- cgit From 4014b279d7f02dcf90a289d7ef5d3cd27d953ee4 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Fri, 13 Jul 2018 13:11:43 -0700 Subject: Update onlyRenderConversions function to method and account for edge cases --- .../send/currency-display/currency-display.js | 50 +++++++++++----------- 1 file changed, 24 insertions(+), 26 deletions(-) (limited to 'ui') diff --git a/ui/app/components/send/currency-display/currency-display.js b/ui/app/components/send/currency-display/currency-display.js index 70fc9de70..12ddc3d53 100644 --- a/ui/app/components/send/currency-display/currency-display.js +++ b/ui/app/components/send/currency-display/currency-display.js @@ -75,6 +75,10 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { const { primaryCurrency, convertedCurrency, conversionRate } = this.props + if (conversionRate == 0 || conversionRate == null || converstionRate == undefined && nonFormattedValue != 0) { + return null + } + let convertedValue = conversionUtil(nonFormattedValue, { fromNumericBase: 'dec', fromCurrency: primaryCurrency, @@ -83,19 +87,13 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu conversionRate, }) - if (conversionRate == 0 && nonFormattedValue != 0) { - convertedValue = null - return convertedValue - } - else { - convertedValue == Number(convertedValue).toFixed(2) - const upperCaseCurrencyCode = convertedCurrency.toUpperCase() - return currencies.find(currency => currency.code === upperCaseCurrencyCode) - ? currencyFormatter.format(Number(convertedValue), { - code: upperCaseCurrencyCode, - }) + convertedValue == Number(convertedValue).toFixed(2) + const upperCaseCurrencyCode = convertedCurrency.toUpperCase() + return currencies.find(currency => currency.code === upperCaseCurrencyCode) + ? currencyFormatter.format(Number(convertedValue), { + code: upperCaseCurrencyCode, + }) : convertedValue - } } CurrencyDisplay.prototype.handleChange = function (newVal) { @@ -110,6 +108,19 @@ CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) { return (valueLength + decimalPointDeficit + 0.75) + 'ch' } +CurrencyDisplay.prototype.onlyRenderConversions = function (convertedValueToRender) { + const{ + convertedBalanceClassName = 'currency-display__converted-value', + convertedCurrency, + } = this.props + + return h('div', { + className: convertedBalanceClassName, + }, convertedValueToRender == null + ? 'No Conversion Rate' + : `${convertedValueToRender} ${convertedCurrency.toUpperCase()}` +) + } CurrencyDisplay.prototype.render = function () { const { @@ -127,19 +138,6 @@ CurrencyDisplay.prototype.render = function () { const convertedValueToRender = this.getConvertedValueToRender(valueToRender) - function onlyRenderConversions() { - if (convertedValueToRender == null) { - return h('div', { - className: convertedBalanceClassName, - }, 'No Conversion Rate') - } - else { - return h('div', { - className: convertedBalanceClassName, - }, `${convertedValueToRender} ${convertedCurrency.toUpperCase()}`) - } - } - return h('div', { className, style: { @@ -176,7 +174,7 @@ CurrencyDisplay.prototype.render = function () { ]), - ]), onlyRenderConversions(), + ]), this.onlyRenderConversions(convertedValueToRender), ]) -- cgit From 684fc710ee6db33e3ca4e5c5777874e46ccef3b1 Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 16 Jul 2018 13:02:12 -0700 Subject: Fix edge cases and add translation compatibility --- .../send/currency-display/currency-display.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) (limited to 'ui') diff --git a/ui/app/components/send/currency-display/currency-display.js b/ui/app/components/send/currency-display/currency-display.js index 12ddc3d53..3bef25e29 100644 --- a/ui/app/components/send/currency-display/currency-display.js +++ b/ui/app/components/send/currency-display/currency-display.js @@ -6,6 +6,11 @@ const { removeLeadingZeroes } = require('../send.utils') const currencyFormatter = require('currency-formatter') const currencies = require('currency-formatter/currencies') const ethUtil = require('ethereumjs-util') +const PropTypes = require('prop-types') + +CurrencyDisplay.contextTypes = { + t: PropTypes.func, +} module.exports = CurrencyDisplay @@ -75,11 +80,13 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { const { primaryCurrency, convertedCurrency, conversionRate } = this.props - if (conversionRate == 0 || conversionRate == null || converstionRate == undefined && nonFormattedValue != 0) { - return null + if (conversionRate == 0 || conversionRate == null || conversionRate == undefined) { + if (nonFormattedValue != 0) { + return null + } } - let convertedValue = conversionUtil(nonFormattedValue, { + const convertedValue = conversionUtil(nonFormattedValue, { fromNumericBase: 'dec', fromCurrency: primaryCurrency, toCurrency: convertedCurrency, @@ -109,15 +116,14 @@ CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) { } CurrencyDisplay.prototype.onlyRenderConversions = function (convertedValueToRender) { - const{ + const { convertedBalanceClassName = 'currency-display__converted-value', convertedCurrency, } = this.props - return h('div', { className: convertedBalanceClassName, }, convertedValueToRender == null - ? 'No Conversion Rate' + ? this.context.t('noConversionRateAvailable') : `${convertedValueToRender} ${convertedCurrency.toUpperCase()}` ) } -- cgit From dcf8e0ed12de61bf58b4d27010b5b8b6d93bd7dd Mon Sep 17 00:00:00 2001 From: Sara Reynolds Date: Mon, 16 Jul 2018 16:50:08 -0700 Subject: lint fix --- ui/app/components/send/currency-display/currency-display.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'ui') diff --git a/ui/app/components/send/currency-display/currency-display.js b/ui/app/components/send/currency-display/currency-display.js index 3bef25e29..2b8eaa41f 100644 --- a/ui/app/components/send/currency-display/currency-display.js +++ b/ui/app/components/send/currency-display/currency-display.js @@ -80,13 +80,13 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { const { primaryCurrency, convertedCurrency, conversionRate } = this.props - if (conversionRate == 0 || conversionRate == null || conversionRate == undefined) { - if (nonFormattedValue != 0) { + if (conversionRate === 0 || conversionRate === null || conversionRate === undefined) { + if (nonFormattedValue !== 0) { return null } } - const convertedValue = conversionUtil(nonFormattedValue, { + let convertedValue = conversionUtil(nonFormattedValue, { fromNumericBase: 'dec', fromCurrency: primaryCurrency, toCurrency: convertedCurrency, @@ -94,7 +94,7 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu conversionRate, }) - convertedValue == Number(convertedValue).toFixed(2) + convertedValue = Number(convertedValue).toFixed(2) const upperCaseCurrencyCode = convertedCurrency.toUpperCase() return currencies.find(currency => currency.code === upperCaseCurrencyCode) ? currencyFormatter.format(Number(convertedValue), { @@ -132,9 +132,7 @@ CurrencyDisplay.prototype.render = function () { const { className = 'currency-display', primaryBalanceClassName = 'currency-display__input', - convertedBalanceClassName = 'currency-display__converted-value', primaryCurrency, - convertedCurrency, readOnly = false, inError = false, onBlur, -- cgit From 4737ea49c72b24a712b7c3215bed93383ce3ad81 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Mon, 16 Jul 2018 17:09:57 -0700 Subject: Increase clickable area and padding of Retry Transaction bar --- ui/app/components/tx-list-item.js | 24 ++++++++++------------- ui/app/css/itcss/components/transaction-list.scss | 10 ++++------ 2 files changed, 14 insertions(+), 20 deletions(-) (limited to 'ui') diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js index e539514ec..0d693b805 100644 --- a/ui/app/components/tx-list-item.js +++ b/ui/app/components/tx-list-item.js @@ -307,20 +307,16 @@ TxListItem.prototype.render = function () { ]), ]), - this.showRetryButton() && h('div.tx-list-item-retry-container', [ - - h('span.tx-list-item-retry-copy', 'Taking too long?'), - - h('span.tx-list-item-retry-link', { - onClick: (event) => { - event.stopPropagation() - if (isTokenTx) { - this.setSelectedToken(txParams.to) - } - this.resubmit() - }, - }, 'Increase the gas price on your transaction'), - + this.showRetryButton() && h('.tx-list-item-retry-container', { + onClick: (event) => { + event.stopPropagation() + if (isTokenTx) { + this.setSelectedToken(txParams.to) + } + this.resubmit() + }, + }, [ + h('span', 'Taking too long? Increase the gas price on your transaction'), ]), ]), // holding on icon from design diff --git a/ui/app/css/itcss/components/transaction-list.scss b/ui/app/css/itcss/components/transaction-list.scss index d03faf486..1d45ff13b 100644 --- a/ui/app/css/itcss/components/transaction-list.scss +++ b/ui/app/css/itcss/components/transaction-list.scss @@ -129,12 +129,14 @@ .tx-list-item-retry-container { background: #d1edff; width: 100%; - border-radius: 4px; - font-size: 0.8em; + border-radius: 12px; + font-size: .75rem; display: flex; justify-content: center; margin-left: 44px; width: calc(100% - 44px); + padding: 4px; + cursor: pointer; @media screen and (min-width: 576px) and (max-width: 679px) { flex-flow: column; @@ -151,10 +153,6 @@ } } -.tx-list-item-retry-copy { - font-family: Roboto; -} - .tx-list-item-retry-link { text-decoration: underline; margin-left: 6px; -- cgit From e9a8c24cc4d26e33380a33e87e80952918339ad7 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Sat, 14 Jul 2018 13:43:55 -0700 Subject: Remove unused confirm transaction files --- .../components/pages/confirm-send-token/index.scss | 19 --- ui/app/components/pages/index.scss | 2 - ui/app/components/pending-tx/index.js | 165 --------------------- ui/app/conf-tx.js | 136 +++-------------- 4 files changed, 23 insertions(+), 299 deletions(-) delete mode 100644 ui/app/components/pages/confirm-send-token/index.scss delete mode 100644 ui/app/components/pending-tx/index.js (limited to 'ui') diff --git a/ui/app/components/pages/confirm-send-token/index.scss b/ui/app/components/pages/confirm-send-token/index.scss deleted file mode 100644 index 0476749f6..000000000 --- a/ui/app/components/pages/confirm-send-token/index.scss +++ /dev/null @@ -1,19 +0,0 @@ -.confirm-send-token { - &__title { - padding: 4px 0; - display: flex; - align-items: center; - } - - &__identicon { - flex: 0 0 auto; - } - - &__title-text { - font-size: 2.25rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding-left: 8px; - } -} diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss index 8b333b6a8..b15c59863 100644 --- a/ui/app/components/pages/index.scss +++ b/ui/app/components/pages/index.scss @@ -3,5 +3,3 @@ @import './add-token/index'; @import './confirm-add-token/index'; - -@import './confirm-send-token/index'; diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js deleted file mode 100644 index 3f8cd8823..000000000 --- a/ui/app/components/pending-tx/index.js +++ /dev/null @@ -1,165 +0,0 @@ -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const PropTypes = require('prop-types') -const clone = require('clone') -const abi = require('human-standard-token-abi') -const abiDecoder = require('abi-decoder') -abiDecoder.addABI(abi) -const inherits = require('util').inherits -const actions = require('../../actions') -const { getSymbolAndDecimals } = require('../../token-util') -const ConfirmSendEther = require('./confirm-send-ether') -const ConfirmSendToken = require('./confirm-send-token') -const ConfirmDeployContract = require('./confirm-deploy-contract') -const Loading = require('../loading-screen') - -const TX_TYPES = { - DEPLOY_CONTRACT: 'deploy_contract', - SEND_ETHER: 'send_ether', - SEND_TOKEN: 'send_token', -} - -module.exports = connect(mapStateToProps, mapDispatchToProps)(PendingTx) - -function mapStateToProps (state) { - const { - conversionRate, - identities, - tokens: existingTokens, - } = state.metamask - const accounts = state.metamask.accounts - const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] - return { - conversionRate, - identities, - selectedAddress, - existingTokens, - } -} - -function mapDispatchToProps (dispatch) { - return { - backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), - cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), - } -} - -inherits(PendingTx, Component) -function PendingTx () { - Component.call(this) - this.state = { - isFetching: true, - transactionType: '', - tokenAddress: '', - tokenSymbol: '', - tokenDecimals: '', - } -} - -PendingTx.prototype.componentDidMount = function () { - this.setTokenData() -} - -PendingTx.prototype.componentDidUpdate = function (prevProps, prevState) { - if (prevState.isFetching) { - this.setTokenData() - } -} - -PendingTx.prototype.setTokenData = async function () { - const { existingTokens } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - if (txMeta.loadingDefaults) { - return - } - - if (!txParams.to) { - return this.setState({ - transactionType: TX_TYPES.DEPLOY_CONTRACT, - isFetching: false, - }) - } - - // inspect tx data for supported special confirmation screens - let isTokenTransaction = false - if (txParams.data) { - const tokenData = abiDecoder.decodeMethod(txParams.data) - const { name: tokenMethodName } = tokenData || {} - isTokenTransaction = (tokenMethodName === 'transfer') - } - - if (isTokenTransaction) { - const { symbol, decimals } = await getSymbolAndDecimals(txParams.to, existingTokens) - - this.setState({ - transactionType: TX_TYPES.SEND_TOKEN, - tokenAddress: txParams.to, - tokenSymbol: symbol, - tokenDecimals: decimals, - isFetching: false, - }) - } else { - this.setState({ - transactionType: TX_TYPES.SEND_ETHER, - isFetching: false, - }) - } -} - -PendingTx.prototype.gatherTxMeta = function () { - const props = this.props - const state = this.state - const txData = clone(state.txData) || clone(props.txData) - - return txData -} - -PendingTx.prototype.render = function () { - const { - isFetching, - transactionType, - tokenAddress, - tokenSymbol, - tokenDecimals, - } = this.state - - const { sendTransaction } = this.props - - if (isFetching) { - return h(Loading, { - loadingMessage: this.context.t('generatingTransaction'), - }) - } - - switch (transactionType) { - case TX_TYPES.SEND_ETHER: - return h(ConfirmSendEther, { - txData: this.gatherTxMeta(), - sendTransaction, - }) - case TX_TYPES.SEND_TOKEN: - return h(ConfirmSendToken, { - txData: this.gatherTxMeta(), - sendTransaction, - token: { - address: tokenAddress, - symbol: tokenSymbol, - decimals: tokenDecimals, - }, - }) - case TX_TYPES.DEPLOY_CONTRACT: - return h(ConfirmDeployContract, { - txData: this.gatherTxMeta(), - sendTransaction, - }) - default: - return h(Loading) - } -} - -PendingTx.contextTypes = { - t: PropTypes.func, -} diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 4e8aaa07d..112ea6bca 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -9,11 +9,7 @@ const txHelper = require('../lib/tx-helper') const log = require('loglevel') const R = require('ramda') -const PendingTx = require('./components/pending-tx') const SignatureRequest = require('./components/signature-request') -// const PendingMsg = require('./components/pending-msg') -// const PendingPersonalMsg = require('./components/pending-personal-msg') -// const PendingTypedMsg = require('./components/pending-typed-msg') const Loading = require('./components/loading-screen') const { DEFAULT_ROUTE } = require('./routes') @@ -151,101 +147,32 @@ ConfirmTxScreen.prototype.render = function () { currentCurrency, conversionRate, blockGasLimit, - // provider, - // computedBalances, } = props var txData = this.getTxData() || {} - var txParams = txData.params || {} - - // var isNotification = isPopupOrNotification() === 'notification' - /* - Client is using the flag above to render the following in conf screen - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }) : null, - h('h2.page-subtitle', 'Confirm Transaction'), - isNotification ? h(NetworkIndicator, { - network: network, - provider: provider, - }) : null, - ]), - */ - - return currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - accounts: props.accounts, - identities: props.identities, - conversionRate, - currentCurrency, - blockGasLimit, - // Actions - buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), - sendTransaction: this.sendTransaction.bind(this), - cancelTransaction: this.cancelTransaction.bind(this, txData), - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - signTypedMessage: this.signTypedMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - cancelTypedMessage: this.cancelTypedMessage.bind(this, txData), - }) -} - -function currentTxView (opts) { - log.info('rendering current tx view') - const { txData } = opts - const { txParams, msgParams } = txData - - if (txParams) { - log.debug('txParams detected, rendering pending tx') - return h(PendingTx, opts) - } else if (msgParams) { - log.debug('msgParams detected, rendering pending msg') - - return h(SignatureRequest, opts) - - // if (type === 'eth_sign') { - // log.debug('rendering eth_sign message') - // return h(PendingMsg, opts) - // } else if (type === 'personal_sign') { - // log.debug('rendering personal_sign message') - // return h(PendingPersonalMsg, opts) - // } else if (type === 'eth_signTypedData') { - // log.debug('rendering eth_signTypedData message') - // return h(PendingTypedMsg, opts) - // } - } - - return h(Loading) -} - -ConfirmTxScreen.prototype.buyEth = function (address, event) { - event.preventDefault() - this.props.dispatch(actions.buyEthView(address)) -} - -ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { - this.stopPropagation(event) - this.props.dispatch(actions.updateAndApproveTx(txData)) - .then(() => this.props.history.push(DEFAULT_ROUTE)) -} - -ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { - this.stopPropagation(event) - event.preventDefault() - this.props.dispatch(actions.cancelTx(txData)) -} - -ConfirmTxScreen.prototype.cancelAllTransactions = function (unconfTxList, event) { - this.stopPropagation(event) - event.preventDefault() - this.props.dispatch(actions.cancelAllTx(unconfTxList)) + const { msgParams } = txData + log.debug('msgParams detected, rendering pending msg') + + return msgParams + ? h(SignatureRequest, { + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + signTypedMessage: this.signTypedMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + cancelTypedMessage: this.cancelTypedMessage.bind(this, txData), + }) + : h(Loading) } ConfirmTxScreen.prototype.signMessage = function (msgData, event) { @@ -295,20 +222,3 @@ ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) { this.stopPropagation(event) return this.props.dispatch(actions.cancelTypedMsg(msgData)) } - -ConfirmTxScreen.prototype.goHome = function (event) { - this.stopPropagation(event) - this.props.dispatch(actions.goHome()) -} - -// function warningIfExists (warning) { -// if (warning && -// // Do not display user rejections on this screen: -// warning.indexOf('User denied transaction signature') === -1) { -// return h('.error', { -// style: { -// margin: 'auto', -// }, -// }, warning) -// } -// } -- cgit From d19c42fcaeea933793ed459ab5248b42811a0498 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Sat, 14 Jul 2018 13:47:07 -0700 Subject: Add fallback when no function found, fix network colors, add fiat values for tokens with contract exchange rates --- .../confirm-detail-row.component.js | 18 ++-- ui/app/components/network-display/index.scss | 6 +- .../confirm-approve/confirm-approve.component.js | 21 ++--- .../confirm-approve/confirm-approve.container.js | 19 +---- .../confirm-send-token.component.js | 20 ++--- .../confirm-send-token.container.js | 26 +----- .../confirm-token-transaction-base.component.js | 85 +++++++++++++++++++ .../confirm-token-transaction-base.container.js | 34 ++++++++ .../pages/confirm-token-transaction-base/index.js | 2 + .../confirm-transaction-base.component.js | 40 +++++---- .../confirm-transaction-switch.component.js | 20 +++-- .../confirm-transaction-switch.constants.js | 1 + .../confirm-transaction.component.js | 7 ++ ui/app/helpers/confirm-transaction/util.js | 17 ++++ ui/app/routes.js | 2 + ui/app/selectors/confirm-transaction.js | 99 ++++++++++++++++++++++ 16 files changed, 313 insertions(+), 104 deletions(-) create mode 100644 ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js create mode 100644 ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js create mode 100644 ui/app/components/pages/confirm-token-transaction-base/index.js (limited to 'ui') diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js index 631cf5803..f0703dde2 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js +++ b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js @@ -5,10 +5,10 @@ import classnames from 'classnames' const ConfirmDetailRow = props => { const { label, - fiatFee, - ethFee, + fiatText, + ethText, onHeaderClick, - fiatFeeColor, + fiatTextColor, headerText, headerTextClassName, } = props @@ -27,12 +27,12 @@ const ConfirmDetailRow = props => {
- { fiatFee } + { fiatText }
- { `\u2666 ${ethFee}` } + { ethText }
@@ -41,9 +41,9 @@ const ConfirmDetailRow = props => { ConfirmDetailRow.propTypes = { label: PropTypes.string, - fiatFee: PropTypes.string, - ethFee: PropTypes.string, - fiatFeeColor: PropTypes.string, + fiatText: PropTypes.string, + ethText: PropTypes.string, + fiatTextColor: PropTypes.string, onHeaderClick: PropTypes.func, headerText: PropTypes.string, headerTextClassName: PropTypes.string, diff --git a/ui/app/components/network-display/index.scss b/ui/app/components/network-display/index.scss index e82d0e70c..2085cff67 100644 --- a/ui/app/components/network-display/index.scss +++ b/ui/app/components/network-display/index.scss @@ -9,7 +9,7 @@ height: 25px; &--mainnet { - background-color: lighten($blue-lagoon, 45%); + background-color: lighten($blue-lagoon, 68%); } &--ropsten { @@ -17,11 +17,11 @@ } &--kovan { - background-color: lighten($purple, 45%); + background-color: lighten($purple, 65%); } &--rinkeby { - background-color: lighten($tulip-tree, 45%); + background-color: lighten($tulip-tree, 35%); } } diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.component.js b/ui/app/components/pages/confirm-approve/confirm-approve.component.js index d775b0362..10f06e565 100644 --- a/ui/app/components/pages/confirm-approve/confirm-approve.component.js +++ b/ui/app/components/pages/confirm-approve/confirm-approve.component.js @@ -1,29 +1,18 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import ConfirmTransactionBase from '../confirm-transaction-base' +import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' export default class ConfirmApprove extends Component { - static contextTypes = { - t: PropTypes.func, - } - static propTypes = { - tokenAddress: PropTypes.string, - toAddress: PropTypes.string, - tokenAmount: PropTypes.string, - tokenSymbol: PropTypes.string, + tokenAmount: PropTypes.number, } render () { - const { toAddress, tokenAddress, tokenAmount, tokenSymbol } = this.props + const { tokenAmount } = this.props return ( - ) } diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.container.js b/ui/app/components/pages/confirm-approve/confirm-approve.container.js index 040e499ae..249175e17 100644 --- a/ui/app/components/pages/confirm-approve/confirm-approve.container.js +++ b/ui/app/components/pages/confirm-approve/confirm-approve.container.js @@ -1,27 +1,12 @@ import { connect } from 'react-redux' import ConfirmApprove from './confirm-approve.component' +import { approveTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction' const mapStateToProps = state => { - const { confirmTransaction } = state - const { - tokenData = {}, - txData: { txParams: { to: tokenAddress } = {} } = {}, - tokenProps: { tokenSymbol } = {}, - } = confirmTransaction - const { params = [] } = tokenData - - let toAddress = '' - let tokenAmount = '' - - if (params && params.length === 2) { - [{ value: toAddress }, { value: tokenAmount }] = params - } + const { tokenAmount } = approveTokenAmountAndToAddressSelector(state) return { - toAddress, - tokenAddress, tokenAmount, - tokenSymbol, } } diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js index 46ad9ccab..cb39e3d7b 100644 --- a/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js +++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js @@ -1,20 +1,13 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import ConfirmTransactionBase from '../confirm-transaction-base' +import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' import { SEND_ROUTE } from '../../../routes' export default class ConfirmSendToken extends Component { - static contextTypes = { - t: PropTypes.func, - } - static propTypes = { history: PropTypes.object, - tokenAddress: PropTypes.string, - toAddress: PropTypes.string, - numberOfTokens: PropTypes.number, - tokenSymbol: PropTypes.string, editTransaction: PropTypes.func, + tokenAmount: PropTypes.number, } handleEdit (confirmTransactionData) { @@ -24,15 +17,12 @@ export default class ConfirmSendToken extends Component { } render () { - const { toAddress, tokenAddress, tokenSymbol, numberOfTokens } = this.props + const { tokenAmount } = this.props return ( - this.handleEdit(confirmTransactionData)} - hideSubtitle + tokenAmount={tokenAmount} /> ) } diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js index 2d7efeed6..d60911e59 100644 --- a/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js +++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js @@ -2,36 +2,16 @@ import { connect } from 'react-redux' import { compose } from 'recompose' import { withRouter } from 'react-router-dom' import ConfirmSendToken from './confirm-send-token.component' -import { calcTokenAmount } from '../../../token-util' import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck' import { setSelectedToken, updateSend, showSendTokenPage } from '../../../actions' import { conversionUtil } from '../../../conversion-util' +import { sendTokenTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction' const mapStateToProps = state => { - const { confirmTransaction } = state - const { - tokenData = {}, - tokenProps: { tokenSymbol, tokenDecimals } = {}, - txData: { txParams: { to: tokenAddress } = {} } = {}, - } = confirmTransaction - const { params = [] } = tokenData - - let toAddress = '' - let tokenAmount = '' - - if (params && params.length === 2) { - [{ value: toAddress }, { value: tokenAmount }] = params - } - - const numberOfTokens = tokenAmount && tokenDecimals - ? calcTokenAmount(tokenAmount, tokenDecimals) - : 0 + const { tokenAmount } = sendTokenTokenAmountAndToAddressSelector(state) return { - toAddress, - tokenAddress, - tokenSymbol, - numberOfTokens, + tokenAmount, } } diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js new file mode 100644 index 000000000..618ff123c --- /dev/null +++ b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js @@ -0,0 +1,85 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import ConfirmTransactionBase from '../confirm-transaction-base' +import { + formatCurrency, + convertTokenToFiat, + addFiat, +} from '../../../helpers/confirm-transaction/util' + +export default class ConfirmTokenTransactionBase extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + tokenAddress: PropTypes.string, + toAddress: PropTypes.string, + tokenAmount: PropTypes.number, + tokenSymbol: PropTypes.string, + fiatTransactionTotal: PropTypes.string, + ethTransactionTotal: PropTypes.string, + contractExchangeRate: PropTypes.number, + conversionRate: PropTypes.number, + currentCurrency: PropTypes.string, + } + + getFiatTransactionAmount () { + const { tokenAmount, currentCurrency, conversionRate, contractExchangeRate } = this.props + + return convertTokenToFiat({ + value: tokenAmount, + toCurrency: currentCurrency, + conversionRate, + contractExchangeRate, + }) + } + + getSubtitle () { + const { currentCurrency, contractExchangeRate } = this.props + + if (typeof contractExchangeRate === 'undefined') { + return this.context.t('noConversionRate') + } else { + const fiatTransactionAmount = this.getFiatTransactionAmount() + return formatCurrency(fiatTransactionAmount, currentCurrency) + } + } + + getFiatTotalTextOverride () { + const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props + + if (typeof contractExchangeRate === 'undefined') { + return formatCurrency(fiatTransactionTotal, currentCurrency) + } else { + const fiatTransactionAmount = this.getFiatTransactionAmount() + const fiatTotal = addFiat(fiatTransactionAmount, fiatTransactionTotal) + return formatCurrency(fiatTotal, currentCurrency) + } + } + + render () { + const { + toAddress, + tokenAddress, + tokenSymbol, + tokenAmount, + ethTransactionTotal, + ...restProps + } = this.props + + const tokensText = `${tokenAmount} ${tokenSymbol}` + + return ( + + ) + } +} diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js new file mode 100644 index 000000000..be38acdb0 --- /dev/null +++ b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js @@ -0,0 +1,34 @@ +import { connect } from 'react-redux' +import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component' +import { + tokenAmountAndToAddressSelector, + contractExchangeRateSelector, +} from '../../../selectors/confirm-transaction' + +const mapStateToProps = (state, ownProps) => { + const { tokenAmount: ownTokenAmount } = ownProps + const { confirmTransaction, metamask: { currentCurrency, conversionRate } } = state + const { + txData: { txParams: { to: tokenAddress } = {} } = {}, + tokenProps: { tokenSymbol } = {}, + fiatTransactionTotal, + ethTransactionTotal, + } = confirmTransaction + + const { tokenAmount, toAddress } = tokenAmountAndToAddressSelector(state) + const contractExchangeRate = contractExchangeRateSelector(state) + + return { + toAddress, + tokenAddress, + tokenAmount: typeof ownTokenAmount !== 'undefined' ? ownTokenAmount : tokenAmount, + tokenSymbol, + currentCurrency, + conversionRate, + contractExchangeRate, + fiatTransactionTotal, + ethTransactionTotal, + } +} + +export default connect(mapStateToProps)(ConfirmTokenTransactionBase) diff --git a/ui/app/components/pages/confirm-token-transaction-base/index.js b/ui/app/components/pages/confirm-token-transaction-base/index.js new file mode 100644 index 000000000..e15c5d56b --- /dev/null +++ b/ui/app/components/pages/confirm-token-transaction-base/index.js @@ -0,0 +1,2 @@ +export { default } from './confirm-token-transaction-base.container' +export { default as ConfirmTokenTransactionBase } from './confirm-token-transaction-base.component' diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js index 5327b116f..e1bf2210f 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -54,6 +54,8 @@ export default class ConfirmTransactionBase extends Component { detailsComponent: PropTypes.node, errorKey: PropTypes.string, errorMessage: PropTypes.string, + ethTotalTextOverride: PropTypes.string, + fiatTotalTextOverride: PropTypes.string, hideData: PropTypes.bool, hideDetails: PropTypes.bool, hideSubtitle: PropTypes.bool, @@ -146,6 +148,8 @@ export default class ConfirmTransactionBase extends Component { currentCurrency, fiatTransactionTotal, ethTransactionTotal, + fiatTotalTextOverride, + ethTotalTextOverride, hideDetails, } = this.props @@ -153,14 +157,16 @@ export default class ConfirmTransactionBase extends Component { return null } + const formattedCurrency = formatCurrency(fiatTransactionTotal, currentCurrency) + return ( detailsComponent || (
this.handleEditGas()} @@ -169,11 +175,11 @@ export default class ConfirmTransactionBase extends Component {
@@ -206,17 +212,21 @@ export default class ConfirmTransactionBase extends Component {
{`${t('functionType')}:`} - { name } + { name || t('notFound') }
-
-
- { `${t('parameters')}:` } -
-
-
{ JSON.stringify(params, null, 2) }
-
-
+ { + params && ( +
+
+ { `${t('parameters')}:` } +
+
+
{ JSON.stringify(params, null, 2) }
+
+
+ ) + }
{`${t('hexData')}:`}
@@ -297,7 +307,7 @@ export default class ConfirmTransactionBase extends Component { toName={toName} toAddress={toAddress} showEdit={onEdit && !isTxReprice} - action={action || name} + action={action || name || this.context.t('unknownFunction')} title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`} subtitle={subtitle || `\u2666 ${ethTransactionAmount}`} hideSubtitle={hideSubtitle} diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js index 25259b98c..0280f73c6 100644 --- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js +++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js @@ -8,11 +8,16 @@ import { CONFIRM_SEND_ETHER_PATH, CONFIRM_SEND_TOKEN_PATH, CONFIRM_APPROVE_PATH, + CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, } from '../../../routes' import { isConfirmDeployContract } from './confirm-transaction-switch.util' -import { TOKEN_METHOD_TRANSFER, TOKEN_METHOD_APPROVE } from './confirm-transaction-switch.constants' +import { + TOKEN_METHOD_TRANSFER, + TOKEN_METHOD_APPROVE, + TOKEN_METHOD_TRANSFER_FROM, +} from './confirm-transaction-switch.constants' export default class ConfirmTransactionSwitch extends Component { static propTypes = { @@ -27,8 +32,7 @@ export default class ConfirmTransactionSwitch extends Component { methodData: { name }, fetchingMethodData, } = this.props - const { id } = txData - + const { id, txParams: { data } = {} } = txData if (isConfirmDeployContract(txData)) { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}` @@ -39,10 +43,10 @@ export default class ConfirmTransactionSwitch extends Component { return } - if (name) { - const methodName = name.toLowerCase() + if (data) { + const methodName = name && name.toLowerCase() - switch (methodName.toLowerCase()) { + switch (methodName) { case TOKEN_METHOD_TRANSFER: { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}` return @@ -51,6 +55,10 @@ export default class ConfirmTransactionSwitch extends Component { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}` return } + case TOKEN_METHOD_TRANSFER_FROM: { + const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}` + return + } default: { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}` return diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js index 622d2a37a..9db4a2f96 100644 --- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js +++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js @@ -1,2 +1,3 @@ export const TOKEN_METHOD_TRANSFER = 'transfer' export const TOKEN_METHOD_APPROVE = 'approve' +export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom' diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js index 874a89fd2..3ac656d73 100644 --- a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js +++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js @@ -8,6 +8,7 @@ import ConfirmSendEther from '../confirm-send-ether' import ConfirmSendToken from '../confirm-send-token' import ConfirmDeployContract from '../confirm-deploy-contract' import ConfirmApprove from '../confirm-approve' +import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' import ConfTx from '../../../conf-tx' import { DEFAULT_ROUTE, @@ -16,6 +17,7 @@ import { CONFIRM_SEND_ETHER_PATH, CONFIRM_SEND_TOKEN_PATH, CONFIRM_APPROVE_PATH, + CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, } from '../../../routes' @@ -137,6 +139,11 @@ export default class ConfirmTransaction extends Component { path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`} component={ConfirmApprove} /> + state.metamask.unapprovedTxs const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs @@ -63,3 +64,101 @@ export const unconfirmedTransactionsHashSelector = createSelector( export const currentCurrencySelector = state => state.metamask.currentCurrency export const conversionRateSelector = state => state.metamask.conversionRate + +const txDataSelector = state => state.confirmTransaction.txData +const tokenDataSelector = state => state.confirmTransaction.tokenData +const tokenPropsSelector = state => state.confirmTransaction.tokenProps + +const contractExchangeRatesSelector = state => state.metamask.contractExchangeRates + +const tokenDecimalsSelector = createSelector( + tokenPropsSelector, + tokenProps => tokenProps && tokenProps.tokenDecimals +) + +const tokenDataParamsSelector = createSelector( + tokenDataSelector, + tokenData => tokenData && tokenData.params || [] +) + +const txParamsSelector = createSelector( + txDataSelector, + txData => txData && txData.txParams || {} +) + +export const tokenAddressSelector = createSelector( + txParamsSelector, + txParams => txParams && txParams.to +) + +const TOKEN_PARAM_SPENDER = '_spender' +const TOKEN_PARAM_TO = '_to' +const TOKEN_PARAM_VALUE = '_value' + +export const tokenAmountAndToAddressSelector = createSelector( + tokenDataParamsSelector, + params => { + let toAddress = '' + let tokenAmount = 0 + + if (params && params.length) { + const toParam = params.find(param => param.name === TOKEN_PARAM_TO) + const valueParam = params.find(param => param.name === TOKEN_PARAM_VALUE) + toAddress = toParam ? toParam.value : params[0].value + tokenAmount = valueParam ? +valueParam.value : +params[1].value + } + + return { + toAddress, + tokenAmount, + } + } +) + +export const approveTokenAmountAndToAddressSelector = createSelector( + tokenDataParamsSelector, + params => { + let toAddress = '' + let tokenAmount = 0 + + if (params && params.length) { + toAddress = params.find(param => param.name === TOKEN_PARAM_SPENDER).value + tokenAmount = +params.find(param => param.name === TOKEN_PARAM_VALUE).value + } + + return { + toAddress, + tokenAmount, + } + } +) + +export const sendTokenTokenAmountAndToAddressSelector = createSelector( + tokenDataParamsSelector, + tokenDecimalsSelector, + (params, tokenDecimals) => { + let toAddress = '' + let tokenAmount = 0 + + if (params && params.length) { + toAddress = params.find(param => param.name === TOKEN_PARAM_TO).value + tokenAmount = +params.find(param => param.name === TOKEN_PARAM_VALUE).value + + if (tokenDecimals) { + tokenAmount = calcTokenAmount(tokenAmount, tokenDecimals) + } + } + + return { + toAddress, + tokenAmount, + } + } +) + + +export const contractExchangeRateSelector = createSelector( + contractExchangeRatesSelector, + tokenAddressSelector, + (contractExchangeRates, tokenAddress) => contractExchangeRates[tokenAddress] +) -- cgit From e16f5ab54d1e69e59fe4689fde3fa34e61c88974 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Sat, 14 Jul 2018 23:36:42 -0700 Subject: Fix tests --- ui/app/components/pages/confirm-approve/confirm-approve.component.js | 4 +++- ui/app/components/pages/confirm-approve/confirm-approve.container.js | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'ui') diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.component.js b/ui/app/components/pages/confirm-approve/confirm-approve.component.js index 10f06e565..b71eaa1d4 100644 --- a/ui/app/components/pages/confirm-approve/confirm-approve.component.js +++ b/ui/app/components/pages/confirm-approve/confirm-approve.component.js @@ -5,14 +5,16 @@ import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' export default class ConfirmApprove extends Component { static propTypes = { tokenAmount: PropTypes.number, + tokenSymbol: PropTypes.string, } render () { - const { tokenAmount } = this.props + const { tokenAmount, tokenSymbol } = this.props return ( ) } diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.container.js b/ui/app/components/pages/confirm-approve/confirm-approve.container.js index 249175e17..4ef9f4ced 100644 --- a/ui/app/components/pages/confirm-approve/confirm-approve.container.js +++ b/ui/app/components/pages/confirm-approve/confirm-approve.container.js @@ -3,10 +3,12 @@ import ConfirmApprove from './confirm-approve.component' import { approveTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction' const mapStateToProps = state => { + const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state const { tokenAmount } = approveTokenAmountAndToAddressSelector(state) return { tokenAmount, + tokenSymbol, } } -- cgit From 279bdfc2e7e1ca6ad69e449af4eedcfa079496bf Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Mon, 16 Jul 2018 12:03:27 -0700 Subject: Merge develop --- .../pending-tx/confirm-deploy-contract.js | 358 ----------- ui/app/components/pending-tx/confirm-send-ether.js | 692 -------------------- ui/app/components/pending-tx/confirm-send-token.js | 696 --------------------- 3 files changed, 1746 deletions(-) delete mode 100644 ui/app/components/pending-tx/confirm-deploy-contract.js delete mode 100644 ui/app/components/pending-tx/confirm-send-ether.js delete mode 100644 ui/app/components/pending-tx/confirm-send-token.js (limited to 'ui') diff --git a/ui/app/components/pending-tx/confirm-deploy-contract.js b/ui/app/components/pending-tx/confirm-deploy-contract.js deleted file mode 100644 index a059d1a32..000000000 --- a/ui/app/components/pending-tx/confirm-deploy-contract.js +++ /dev/null @@ -1,358 +0,0 @@ -const { Component } = require('react') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const PropTypes = require('prop-types') -const actions = require('../../actions') -const clone = require('clone') -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') -const { conversionUtil } = require('../../conversion-util') -const SenderToRecipient = require('../sender-to-recipient') -const NetworkDisplay = require('../network-display') - -const { MIN_GAS_PRICE_HEX } = require('../send/send.constants') - -class ConfirmDeployContract extends Component { - constructor (props) { - super(props) - - this.state = { - valid: false, - submitting: false, - } - } - - onSubmit (event) { - event.preventDefault() - const txMeta = this.gatherTxMeta() - const valid = this.checkValidity() - this.setState({ valid, submitting: true }) - - if (valid && this.verifyGasParams()) { - this.props.sendTransaction(txMeta, event) - } else { - this.props.displayWarning(this.context.t('invalidGasParams')) - this.setState({ submitting: false }) - } - } - - cancel (event, txMeta) { - event.preventDefault() - this.props.cancelTransaction(txMeta) - } - - checkValidity () { - const form = this.getFormEl() - const valid = form.checkValidity() - return valid - } - - getFormEl () { - const form = document.querySelector('form#pending-tx-form') - // Stub out form for unit tests: - if (!form) { - return { checkValidity () { return true } } - } - return form - } - - // After a customizable state value has been updated, - gatherTxMeta () { - const props = this.props - const state = this.state - const txData = clone(state.txData) || clone(props.txData) - - // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) - return txData - } - - verifyGasParams () { - // We call this in case the gas has not been modified at all - if (!this.state) { return true } - return ( - this._notZeroOrEmptyString(this.state.gas) && - this._notZeroOrEmptyString(this.state.gasPrice) - ) - } - - _notZeroOrEmptyString (obj) { - return obj !== '' && obj !== '0x0' - } - - bnMultiplyByFraction (targetBN, numerator, denominator) { - const numBN = new BN(numerator) - const denomBN = new BN(denominator) - return targetBN.mul(numBN).div(denomBN) - } - - getData () { - const { identities } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - return { - from: { - address: txParams.from, - name: identities[txParams.from].name, - }, - memo: txParams.memo || '', - } - } - - getAmount () { - const { conversionRate, currentCurrency } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - const FIAT = conversionUtil(txParams.value, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: currentCurrency, - numberOfDecimals: 2, - fromDenomination: 'WEI', - conversionRate, - }) - const ETH = conversionUtil(txParams.value, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: 'ETH', - fromDenomination: 'WEI', - conversionRate, - numberOfDecimals: 6, - }) - - return { - fiat: Number(FIAT), - token: Number(ETH), - } - - } - - getGasFee () { - const { conversionRate, currentCurrency } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - // Gas - const gas = txParams.gas - const gasBn = hexToBn(gas) - - // Gas Price - const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX - const gasPriceBn = hexToBn(gasPrice) - - const txFeeBn = gasBn.mul(gasPriceBn) - - const FIAT = conversionUtil(txFeeBn, { - fromNumericBase: 'BN', - toNumericBase: 'dec', - fromDenomination: 'WEI', - fromCurrency: 'ETH', - toCurrency: currentCurrency, - numberOfDecimals: 2, - conversionRate, - }) - const ETH = conversionUtil(txFeeBn, { - fromNumericBase: 'BN', - toNumericBase: 'dec', - fromDenomination: 'WEI', - fromCurrency: 'ETH', - toCurrency: 'ETH', - numberOfDecimals: 6, - conversionRate, - }) - - return { - fiat: Number(FIAT), - eth: Number(ETH), - } - } - - renderGasFee () { - const { currentCurrency } = this.props - const { fiat: fiatGas, eth: ethGas } = this.getGasFee() - - return ( - h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]), - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `${fiatGas} ${currentCurrency.toUpperCase()}`), - - h( - 'div.confirm-screen-row-detail', - `${ethGas} ETH` - ), - ]), - ]) - ) - } - - renderHeroAmount () { - const { currentCurrency } = this.props - const { fiat: fiatAmount } = this.getAmount() - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - const { memo = '' } = txParams - - return ( - h('div.confirm-send-token__hero-amount-wrapper', [ - h('h3.flex-center.confirm-screen-send-amount', `${fiatAmount}`), - h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency.toUpperCase()), - h('div.flex-center.confirm-memo-wrapper', [ - h('h3.confirm-screen-send-memo', memo), - ]), - ]) - ) - } - - renderTotalPlusGas () { - const { currentCurrency } = this.props - const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() - const { fiat: fiatGas, eth: ethGas } = this.getGasFee() - - return ( - h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [ - h('div.confirm-screen-section-column', [ - h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]), - h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]), - ]), - - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `${fiatAmount + fiatGas} ${currentCurrency.toUpperCase()}`), - h('div.confirm-screen-row-detail', `${tokenAmount + ethGas} ETH`), - ]), - ]) - ) - } - - render () { - const { backToAccountDetail, selectedAddress } = this.props - const txMeta = this.gatherTxMeta() - - const { - from: { - address: fromAddress, - name: fromName, - }, - } = this.getData() - - this.inputs = [] - - return ( - h('.page-container', [ - h('.page-container__header', [ - h('.page-container__header-row', [ - h('span.page-container__back-button', { - onClick: () => backToAccountDetail(selectedAddress), - }, this.context.t('back')), - window.METAMASK_UI_TYPE === 'notification' && h(NetworkDisplay), - ]), - h('.page-container__title', this.context.t('confirmContract')), - h('.page-container__subtitle', this.context.t('pleaseReviewTransaction')), - ]), - // Main Send token Card - h('.page-container__content', [ - - h(SenderToRecipient, { - senderName: fromName, - senderAddress: fromAddress, - }), - - // h('h3.flex-center.confirm-screen-sending-to-message', { - // style: { - // textAlign: 'center', - // fontSize: '16px', - // }, - // }, [ - // `You're deploying a new contract.`, - // ]), - - this.renderHeroAmount(), - - h('div.confirm-screen-rows', [ - h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]), - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', fromName), - h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`), - ]), - ]), - - h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]), - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', this.context.t('newContract')), - ]), - ]), - - this.renderGasFee(), - - this.renderTotalPlusGas(), - - ]), - ]), - - h('form#pending-tx-form', { - onSubmit: event => this.onSubmit(event), - }, [ - h('.page-container__footer', [ - // Cancel Button - h('button.btn-cancel.page-container__footer-button.allcaps', { - onClick: event => this.cancel(event, txMeta), - }, this.context.t('cancel')), - - // Accept Button - h('button.btn-confirm.page-container__footer-button.allcaps', { - onClick: event => this.onSubmit(event), - }, this.context.t('confirm')), - ]), - ]), - ]) - ) - } -} - -ConfirmDeployContract.propTypes = { - sendTransaction: PropTypes.func, - cancelTransaction: PropTypes.func, - backToAccountDetail: PropTypes.func, - displayWarning: PropTypes.func, - identities: PropTypes.object, - conversionRate: PropTypes.number, - currentCurrency: PropTypes.string, - selectedAddress: PropTypes.string, - t: PropTypes.func, -} - -const mapStateToProps = state => { - const { - conversionRate, - identities, - currentCurrency, - } = state.metamask - const accounts = state.metamask.accounts - const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] - return { - currentCurrency, - conversionRate, - identities, - selectedAddress, - } -} - -const mapDispatchToProps = dispatch => { - return { - backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), - cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), - displayWarning: warning => actions.displayWarning(warning), - } -} - -ConfirmDeployContract.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmDeployContract) diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js deleted file mode 100644 index 67f54aa26..000000000 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ /dev/null @@ -1,692 +0,0 @@ -const Component = require('react').Component -const { withRouter } = require('react-router-dom') -const { compose } = require('recompose') -const PropTypes = require('prop-types') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const inherits = require('util').inherits -const actions = require('../../actions') -const clone = require('clone') -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') -const classnames = require('classnames') -const { - conversionUtil, - addCurrencies, - multiplyCurrencies, -} = require('../../conversion-util') -const { - calcGasTotal, - isBalanceSufficient, -} = require('../send/send.utils') -const GasFeeDisplay = require('../send/send-content/send-gas-row/gas-fee-display/').default -const SenderToRecipient = require('../sender-to-recipient') -const NetworkDisplay = require('../network-display') -const currencyFormatter = require('currency-formatter') -const currencies = require('currency-formatter/currencies') - -const { MIN_GAS_PRICE_HEX } = require('../send/send.constants') -const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') -const { - ENVIRONMENT_TYPE_POPUP, - ENVIRONMENT_TYPE_NOTIFICATION, -} = require('../../../../app/scripts/lib/enums') - -import { - updateSendErrors, -} from '../../ducks/send.duck' - -ConfirmSendEther.contextTypes = { - t: PropTypes.func, -} - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(ConfirmSendEther) - - -function mapStateToProps (state) { - const { - conversionRate, - identities, - currentCurrency, - send, - } = state.metamask - const accounts = state.metamask.accounts - const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] - const { balance } = accounts[selectedAddress] - return { - conversionRate, - identities, - selectedAddress, - currentCurrency, - send, - balance, - } -} - -function mapDispatchToProps (dispatch) { - return { - clearSend: () => dispatch(actions.clearSend()), - editTransaction: txMeta => { - const { id, txParams } = txMeta - const { - gas: gasLimit, - gasPrice, - to, - value: amount, - } = txParams - - dispatch(actions.updateSend({ - gasLimit, - gasPrice, - gasTotal: null, - to, - amount, - errors: { to: null, amount: null }, - editingTransactionId: id, - })) - }, - cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), - showCustomizeGasModal: (txMeta, sendGasLimit, sendGasPrice, sendGasTotal) => { - const { id, txParams, lastGasPrice } = txMeta - const { gas: txGasLimit, gasPrice: txGasPrice } = txParams - - let forceGasMin - if (lastGasPrice) { - forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, { - multiplicandBase: 16, - multiplierBase: 10, - toNumericBase: 'hex', - fromDenomination: 'WEI', - })) - } - - dispatch(actions.updateSend({ - gasLimit: sendGasLimit || txGasLimit, - gasPrice: sendGasPrice || txGasPrice, - editingTransactionId: id, - gasTotal: sendGasTotal, - forceGasMin, - })) - dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })) - }, - updateSendErrors: error => dispatch(updateSendErrors(error)), - } -} - -inherits(ConfirmSendEther, Component) -function ConfirmSendEther () { - Component.call(this) - this.state = {} - this.onSubmit = this.onSubmit.bind(this) -} - -ConfirmSendEther.prototype.updateComponentSendErrors = function (prevProps) { - const { - balance: oldBalance, - conversionRate: oldConversionRate, - } = prevProps - const { - updateSendErrors, - balance, - conversionRate, - send: { - errors: { - simulationFails, - }, - }, - } = this.props - const txMeta = this.gatherTxMeta() - - const shouldUpdateBalanceSendErrors = balance && [ - balance !== oldBalance, - conversionRate !== oldConversionRate, - ].some(x => Boolean(x)) - - if (shouldUpdateBalanceSendErrors) { - const balanceIsSufficient = this.isBalanceSufficient(txMeta) - updateSendErrors({ - insufficientFunds: balanceIsSufficient ? false : 'insufficientFunds', - }) - } - - const shouldUpdateSimulationSendError = Boolean(txMeta.simulationFails) !== Boolean(simulationFails) - - if (shouldUpdateSimulationSendError) { - updateSendErrors({ - simulationFails: !txMeta.simulationFails ? false : 'transactionError', - }) - } -} - -ConfirmSendEther.prototype.componentWillMount = function () { - this.updateComponentSendErrors({}) -} - -ConfirmSendEther.prototype.componentDidUpdate = function (prevProps) { - this.updateComponentSendErrors(prevProps) -} - -ConfirmSendEther.prototype.getAmount = function () { - const { conversionRate, currentCurrency } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - const FIAT = conversionUtil(txParams.value, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: currentCurrency, - numberOfDecimals: 2, - fromDenomination: 'WEI', - conversionRate, - }) - const ETH = conversionUtil(txParams.value, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: 'ETH', - fromDenomination: 'WEI', - conversionRate, - numberOfDecimals: 6, - }) - - return { - FIAT, - ETH, - } - -} - -ConfirmSendEther.prototype.getGasFee = function () { - const { conversionRate, currentCurrency } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - // Gas - const gas = txParams.gas - const gasBn = hexToBn(gas) - - // From latest master -// const gasLimit = new BN(parseInt(blockGasLimit)) -// const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20) -// const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20) -// const safeGasLimit = safeGasLimitBN.toString(10) - - // Gas Price - const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX - const gasPriceBn = hexToBn(gasPrice) - - const txFeeBn = gasBn.mul(gasPriceBn) - - const FIAT = conversionUtil(txFeeBn, { - fromNumericBase: 'BN', - toNumericBase: 'dec', - fromDenomination: 'WEI', - fromCurrency: 'ETH', - toCurrency: currentCurrency, - numberOfDecimals: 2, - conversionRate, - }) - const ETH = conversionUtil(txFeeBn, { - fromNumericBase: 'BN', - toNumericBase: 'dec', - fromDenomination: 'WEI', - fromCurrency: 'ETH', - toCurrency: 'ETH', - numberOfDecimals: 6, - conversionRate, - }) - - return { - FIAT, - ETH, - gasFeeInHex: txFeeBn.toString(16), - } -} - -ConfirmSendEther.prototype.getData = function () { - const { identities } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - const account = identities ? identities[txParams.from] || {} : {} - const { FIAT: gasFeeInFIAT, ETH: gasFeeInETH, gasFeeInHex } = this.getGasFee() - const { FIAT: amountInFIAT, ETH: amountInETH } = this.getAmount() - - const totalInFIAT = addCurrencies(gasFeeInFIAT, amountInFIAT, { - toNumericBase: 'dec', - numberOfDecimals: 2, - }) - const totalInETH = addCurrencies(gasFeeInETH, amountInETH, { - toNumericBase: 'dec', - numberOfDecimals: 6, - }) - - return { - from: { - address: txParams.from, - name: account.name, - }, - to: { - address: txParams.to, - name: identities[txParams.to] ? identities[txParams.to].name : this.context.t('newRecipient'), - }, - memo: txParams.memo || '', - gasFeeInFIAT, - gasFeeInETH, - amountInFIAT, - amountInETH, - totalInFIAT, - totalInETH, - gasFeeInHex, - } -} - -ConfirmSendEther.prototype.convertToRenderableCurrency = function (value, currencyCode) { - const upperCaseCurrencyCode = currencyCode.toUpperCase() - - return currencies.find(currency => currency.code === upperCaseCurrencyCode) - ? currencyFormatter.format(Number(value), { - code: upperCaseCurrencyCode, - }) - : value -} - -ConfirmSendEther.prototype.editTransaction = function () { - const { editTransaction, history } = this.props - const txMeta = this.gatherTxMeta() - editTransaction(txMeta) - history.push(SEND_ROUTE) -} - -ConfirmSendEther.prototype.renderHeaderRow = function (isTxReprice) { - const windowType = window.METAMASK_UI_TYPE - const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && - windowType !== ENVIRONMENT_TYPE_POPUP - - if (isTxReprice && isFullScreen) { - return null - } - - return ( - h('.page-container__header-row', [ - h('span.page-container__back-button', { - onClick: () => this.editTransaction(), - style: { - visibility: isTxReprice ? 'hidden' : 'initial', - }, - }, 'Edit'), - !isFullScreen && h(NetworkDisplay), - ]) - ) -} - -ConfirmSendEther.prototype.renderHeader = function (isTxReprice) { - const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm') - const subtitle = isTxReprice - ? this.context.t('speedUpSubtitle') - : this.context.t('pleaseReviewTransaction') - - return ( - h('.page-container__header', [ - this.renderHeaderRow(isTxReprice), - h('.page-container__title', title), - h('.page-container__subtitle', subtitle), - ]) - ) -} - -ConfirmSendEther.prototype.render = function () { - const { - currentCurrency, - clearSend, - conversionRate, - currentCurrency: convertedCurrency, - showCustomizeGasModal, - send: { - gasTotal, - gasLimit: sendGasLimit, - gasPrice: sendGasPrice, - errors, - }, - } = this.props - const txMeta = this.gatherTxMeta() - const isTxReprice = Boolean(txMeta.lastGasPrice) - const txParams = txMeta.txParams || {} - - const { - from: { - address: fromAddress, - name: fromName, - }, - to: { - address: toAddress, - name: toName, - }, - memo, - gasFeeInHex, - amountInFIAT, - totalInFIAT, - totalInETH, - } = this.getData() - - const convertedAmountInFiat = this.convertToRenderableCurrency(amountInFIAT, currentCurrency) - const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency) - - // This is from the latest master - // It handles some of the errors that we are not currently handling - // Leaving as comments fo reference - - // const balanceBn = hexToBn(balance) - // const insufficientBalance = balanceBn.lt(maxCost) - // const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting - // const showRejectAll = props.unconfTxListLength > 1 -// const dangerousGasLimit = gasBn.gte(saferGasLimitBN) -// const gasLimitSpecified = txMeta.gasLimitSpecified - - this.inputs = [] - - return ( - // Main Send token Card - h('.page-container', [ - this.renderHeader(isTxReprice), - h('.page-container__content', [ - h(SenderToRecipient, { - senderName: fromName, - senderAddress: fromAddress, - recipientName: toName, - recipientAddress: txParams.to, - }), - - // h('h3.flex-center.confirm-screen-sending-to-message', { - // style: { - // textAlign: 'center', - // fontSize: '16px', - // }, - // }, [ - // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, - // ]), - - h('h3.flex-center.confirm-screen-send-amount', [`${convertedAmountInFiat}`]), - h('h3.flex-center.confirm-screen-send-amount-currency', [ currentCurrency.toUpperCase() ]), - h('div.flex-center.confirm-memo-wrapper', [ - h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]), - ]), - - h('div.confirm-screen-rows', [ - h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]), - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', fromName), - h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`), - ]), - ]), - - h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]), - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', toName), - h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`), - ]), - ]), - - h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]), - h('div.confirm-screen-section-column', [ - h(GasFeeDisplay, { - gasTotal: gasTotal || gasFeeInHex, - conversionRate, - convertedCurrency, - onClick: () => showCustomizeGasModal(txMeta, sendGasLimit, sendGasPrice, gasTotal), - }), - ]), - ]), - - h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [ - h('div', { - className: classnames({ - 'confirm-screen-section-column--with-error': errors['insufficientFunds'], - 'confirm-screen-section-column': !errors['insufficientFunds'], - }), - }, [ - h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]), - h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]), - ]), - - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `${convertedTotalInFiat} ${currentCurrency.toUpperCase()}`), - h('div.confirm-screen-row-detail', `${totalInETH} ETH`), - ]), - - this.renderErrorMessage('insufficientFunds'), - ]), - ]), - -// These are latest errors handling from master -// Leaving as comments as reference when we start implementing error handling -// h('style', ` -// .conf-buttons button { -// margin-left: 10px; -// text-transform: uppercase; -// } -// `), - -// txMeta.simulationFails ? -// h('.error', { -// style: { -// marginLeft: 50, -// fontSize: '0.9em', -// }, -// }, 'Transaction Error. Exception thrown in contract code.') -// : null, - -// !isValidAddress ? -// h('.error', { -// style: { -// marginLeft: 50, -// fontSize: '0.9em', -// }, -// }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') -// : null, - -// insufficientBalance ? -// h('span.error', { -// style: { -// marginLeft: 50, -// fontSize: '0.9em', -// }, -// }, 'Insufficient balance for transaction') -// : null, - -// // send + cancel -// h('.flex-row.flex-space-around.conf-buttons', { -// style: { -// display: 'flex', -// justifyContent: 'flex-end', -// margin: '14px 25px', -// }, -// }, [ -// h('button', { -// onClick: (event) => { -// this.resetGasFields() -// event.preventDefault() -// }, -// }, 'Reset'), - -// // Accept Button or Buy Button -// insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') : -// h('input.confirm.btn-green', { -// type: 'submit', -// value: 'SUBMIT', -// style: { marginLeft: '10px' }, -// disabled: buyDisabled, -// }), - -// h('button.cancel.btn-red', { -// onClick: props.cancelTransaction, -// }, 'Reject'), -// ]), -// showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', { -// style: { -// display: 'flex', -// justifyContent: 'flex-end', -// margin: '14px 25px', -// }, -// }, [ -// h('button.cancel.btn-red', { -// onClick: props.cancelAllTransactions, -// }, 'Reject All'), -// ]) : null, -// ]), -// ]) -// ) -// } - ]), - - h('form#pending-tx-form', { - className: 'confirm-screen-form', - onSubmit: this.onSubmit, - }, [ - this.renderErrorMessage('simulationFails'), - h('.page-container__footer', [ - // Cancel Button - h('button.btn-cancel.page-container__footer-button.allcaps', { - onClick: (event) => { - clearSend() - this.cancel(event, txMeta) - }, - }, this.context.t('cancel')), - - // Accept Button - h('button.btn-confirm.page-container__footer-button.allcaps', { - onClick: event => this.onSubmit(event), - }, this.context.t('confirm')), - ]), - ]), - ]) - ) -} - -ConfirmSendEther.prototype.renderErrorMessage = function (message) { - const { send: { errors } } = this.props - - return errors[message] - ? h('div.confirm-screen-error', [ errors[message] ]) - : null -} - -ConfirmSendEther.prototype.onSubmit = function (event) { - event.preventDefault() - const { updateSendErrors } = this.props - const txMeta = this.gatherTxMeta() - const valid = this.checkValidity() - const balanceIsSufficient = this.isBalanceSufficient(txMeta) - this.setState({ valid, submitting: true }) - - if (valid && this.verifyGasParams() && balanceIsSufficient) { - this.props.sendTransaction(txMeta, event) - } else if (!balanceIsSufficient) { - updateSendErrors({ insufficientFunds: 'insufficientFunds' }) - } else { - updateSendErrors({ invalidGasParams: 'invalidGasParams' }) - this.setState({ submitting: false }) - } -} - -ConfirmSendEther.prototype.cancel = function (event, txMeta) { - event.preventDefault() - const { cancelTransaction } = this.props - - cancelTransaction(txMeta) - .then(() => this.props.history.push(DEFAULT_ROUTE)) -} - -ConfirmSendEther.prototype.isBalanceSufficient = function (txMeta) { - const { - balance, - conversionRate, - } = this.props - const { - txParams: { - gas, - gasPrice, - value: amount, - }, - } = txMeta - const gasTotal = calcGasTotal(gas, gasPrice) - - return isBalanceSufficient({ - amount, - gasTotal, - balance, - conversionRate, - }) -} - -ConfirmSendEther.prototype.checkValidity = function () { - const form = this.getFormEl() - const valid = form.checkValidity() - return valid -} - -ConfirmSendEther.prototype.getFormEl = function () { - const form = document.querySelector('form#pending-tx-form') - // Stub out form for unit tests: - if (!form) { - return { checkValidity () { return true } } - } - return form -} - -// After a customizable state value has been updated, -ConfirmSendEther.prototype.gatherTxMeta = function () { - const props = this.props - const state = this.state - const txData = clone(state.txData) || clone(props.txData) - - const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send - const { - lastGasPrice, - txParams: { - gasPrice: txGasPrice, - gas: txGasLimit, - }, - } = txData - - let forceGasMin - if (lastGasPrice) { - forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, { - multiplicandBase: 16, - multiplierBase: 10, - toNumericBase: 'hex', - })) - } - - txData.txParams.gasPrice = sendGasPrice || forceGasMin || txGasPrice - txData.txParams.gas = sendGasLimit || txGasLimit - - // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) - return txData -} - -ConfirmSendEther.prototype.verifyGasParams = function () { - // We call this in case the gas has not been modified at all - if (!this.state) { return true } - return ( - this._notZeroOrEmptyString(this.state.gas) && - this._notZeroOrEmptyString(this.state.gasPrice) - ) -} - -ConfirmSendEther.prototype._notZeroOrEmptyString = function (obj) { - return obj !== '' && obj !== '0x0' -} - -ConfirmSendEther.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { - const numBN = new BN(numerator) - const denomBN = new BN(denominator) - return targetBN.mul(numBN).div(denomBN) -} diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js deleted file mode 100644 index 818853882..000000000 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ /dev/null @@ -1,696 +0,0 @@ -const Component = require('react').Component -const { withRouter } = require('react-router-dom') -const { compose } = require('recompose') -const PropTypes = require('prop-types') -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const inherits = require('util').inherits -const tokenAbi = require('human-standard-token-abi') -const abiDecoder = require('abi-decoder') -abiDecoder.addABI(tokenAbi) -const actions = require('../../actions') -const clone = require('clone') -const Identicon = require('../identicon') -const GasFeeDisplay = require('../send/send-content/send-gas-row/gas-fee-display/').default -const NetworkDisplay = require('../network-display') -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const { - conversionUtil, - multiplyCurrencies, - addCurrencies, -} = require('../../conversion-util') -const { - calcGasTotal, - isBalanceSufficient, -} = require('../send/send.utils') -const { - calcTokenAmount, -} = require('../../token-util') -const classnames = require('classnames') -const currencyFormatter = require('currency-formatter') -const currencies = require('currency-formatter/currencies') - -const { MIN_GAS_PRICE_HEX } = require('../send/send.constants') - -const { - getTokenExchangeRate, - getSelectedAddress, - getSelectedTokenContract, -} = require('../../selectors') -const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') - -import { - updateSendErrors, -} from '../../ducks/send.duck' - -const { - ENVIRONMENT_TYPE_POPUP, - ENVIRONMENT_TYPE_NOTIFICATION, -} = require('../../../../app/scripts/lib/enums') - -ConfirmSendToken.contextTypes = { - t: PropTypes.func, -} - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(ConfirmSendToken) - - -function mapStateToProps (state, ownProps) { - const { token: { address }, txData } = ownProps - const { txParams } = txData || {} - const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data) - - const { - conversionRate, - identities, - currentCurrency, - } = state.metamask - const accounts = state.metamask.accounts - const selectedAddress = getSelectedAddress(state) - const tokenExchangeRate = getTokenExchangeRate(state, address) - const { balance } = accounts[selectedAddress] - return { - conversionRate, - identities, - selectedAddress, - tokenExchangeRate, - tokenData: tokenData || {}, - currentCurrency: currentCurrency.toUpperCase(), - send: state.metamask.send, - tokenContract: getSelectedTokenContract(state), - balance, - } -} - -function mapDispatchToProps (dispatch, ownProps) { - return { - backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)), - cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })), - editTransaction: txMeta => { - const { token: { address } } = ownProps - const { txParams = {}, id } = txMeta - const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data) || {} - const { params = [] } = tokenData - const { value: to } = params[0] || {} - const { value: tokenAmountInDec } = params[1] || {} - const tokenAmountInHex = conversionUtil(tokenAmountInDec, { - fromNumericBase: 'dec', - toNumericBase: 'hex', - }) - const { - gas: gasLimit, - gasPrice, - } = txParams - dispatch(actions.setSelectedToken(address)) - dispatch(actions.updateSend({ - gasLimit, - gasPrice, - gasTotal: null, - to, - amount: tokenAmountInHex, - errors: { to: null, amount: null }, - editingTransactionId: id && id.toString(), - token: ownProps.token, - })) - dispatch(actions.showSendTokenPage()) - }, - showCustomizeGasModal: (txMeta, sendGasLimit, sendGasPrice, sendGasTotal) => { - const { id, txParams, lastGasPrice } = txMeta - const { gas: txGasLimit, gasPrice: txGasPrice } = txParams - const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data) - const { params = [] } = tokenData - const { value: to } = params[0] || {} - const { value: tokenAmountInDec } = params[1] || {} - const tokenAmountInHex = conversionUtil(tokenAmountInDec, { - fromNumericBase: 'dec', - toNumericBase: 'hex', - }) - - let forceGasMin - if (lastGasPrice) { - forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, { - multiplicandBase: 16, - multiplierBase: 10, - toNumericBase: 'hex', - fromDenomination: 'WEI', - })) - } - - dispatch(actions.updateSend({ - gasLimit: sendGasLimit || txGasLimit, - gasPrice: sendGasPrice || txGasPrice, - editingTransactionId: id, - gasTotal: sendGasTotal, - to, - amount: tokenAmountInHex, - forceGasMin, - })) - dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })) - }, - updateSendErrors: error => dispatch(updateSendErrors(error)), - } -} - -inherits(ConfirmSendToken, Component) -function ConfirmSendToken () { - Component.call(this) - this.state = {} - this.onSubmit = this.onSubmit.bind(this) -} - -ConfirmSendToken.prototype.editTransaction = function (txMeta) { - const { editTransaction, history } = this.props - editTransaction(txMeta) - history.push(SEND_ROUTE) -} - -ConfirmSendToken.prototype.updateComponentSendErrors = function (prevProps) { - const { - balance: oldBalance, - conversionRate: oldConversionRate, - } = prevProps - const { - updateSendErrors, - balance, - conversionRate, - send: { - errors: { - simulationFails, - }, - }, - } = this.props - const txMeta = this.gatherTxMeta() - - const shouldUpdateBalanceSendErrors = balance && [ - balance !== oldBalance, - conversionRate !== oldConversionRate, - ].some(x => Boolean(x)) - - if (shouldUpdateBalanceSendErrors) { - const balanceIsSufficient = this.isBalanceSufficient(txMeta) - updateSendErrors({ - insufficientFunds: balanceIsSufficient ? false : this.context.t('insufficientFunds'), - }) - } - - const shouldUpdateSimulationSendError = Boolean(txMeta.simulationFails) !== Boolean(simulationFails) - - if (shouldUpdateSimulationSendError) { - updateSendErrors({ - simulationFails: !txMeta.simulationFails ? false : this.context.t('transactionError'), - }) - } -} - -ConfirmSendToken.prototype.componentWillMount = function () { - const { tokenContract, selectedAddress } = this.props - tokenContract && tokenContract - .balanceOf(selectedAddress) - .then(usersToken => { - }) - this.updateComponentSendErrors({}) -} - -ConfirmSendToken.prototype.componentDidUpdate = function (prevProps) { - this.updateComponentSendErrors(prevProps) -} - -ConfirmSendToken.prototype.getAmount = function () { - const { - conversionRate, - tokenExchangeRate, - token, - tokenData, - send: { amount, editingTransactionId }, - } = this.props - const { params = [] } = tokenData - let { value } = params[1] || {} - const { decimals } = token - - if (editingTransactionId) { - value = conversionUtil(amount, { - fromNumericBase: 'hex', - toNumericBase: 'dec', - }) - } - - const sendTokenAmount = calcTokenAmount(value, decimals) - - return { - fiat: tokenExchangeRate - ? +(sendTokenAmount * tokenExchangeRate * conversionRate).toFixed(2) - : null, - token: typeof value === 'undefined' - ? this.context.t('unknown') - : +sendTokenAmount.toFixed(decimals), - } - -} - -ConfirmSendToken.prototype.getGasFee = function () { - const { conversionRate, tokenExchangeRate, token, currentCurrency } = this.props - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - const { decimals } = token - - const gas = txParams.gas - const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX - const gasTotal = multiplyCurrencies(gas, gasPrice, { - multiplicandBase: 16, - multiplierBase: 16, - }) - - const FIAT = conversionUtil(gasTotal, { - fromNumericBase: 'BN', - toNumericBase: 'dec', - fromDenomination: 'WEI', - fromCurrency: 'ETH', - toCurrency: currentCurrency, - numberOfDecimals: 2, - conversionRate, - }) - const ETH = conversionUtil(gasTotal, { - fromNumericBase: 'BN', - toNumericBase: 'dec', - fromDenomination: 'WEI', - fromCurrency: 'ETH', - toCurrency: 'ETH', - numberOfDecimals: 6, - conversionRate, - }) - const tokenGas = multiplyCurrencies(gas, gasPrice, { - toNumericBase: 'dec', - multiplicandBase: 16, - multiplierBase: 16, - toCurrency: 'BAT', - conversionRate: tokenExchangeRate, - invertConversionRate: true, - fromDenomination: 'WEI', - numberOfDecimals: decimals || 4, - }) - - return { - fiat: +Number(FIAT).toFixed(2), - eth: ETH, - token: tokenExchangeRate - ? tokenGas - : null, - gasFeeInHex: gasTotal.toString(16), - } -} - -ConfirmSendToken.prototype.getData = function () { - const { identities, tokenData } = this.props - const { params = [] } = tokenData - const { value } = params[0] || {} - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - - return { - from: { - address: txParams.from, - name: identities[txParams.from].name, - }, - to: { - address: value, - name: identities[value] ? identities[value].name : this.context.t('newRecipient'), - }, - memo: txParams.memo || '', - } -} - -ConfirmSendToken.prototype.renderHeroAmount = function () { - const { token: { symbol }, currentCurrency } = this.props - const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() - const txMeta = this.gatherTxMeta() - const txParams = txMeta.txParams || {} - const { memo = '' } = txParams - - const convertedAmountInFiat = this.convertToRenderableCurrency(fiatAmount, currentCurrency) - - return fiatAmount - ? ( - h('div.confirm-send-token__hero-amount-wrapper', [ - h('h3.flex-center.confirm-screen-send-amount', `${convertedAmountInFiat}`), - h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency), - h('div.flex-center.confirm-memo-wrapper', [ - h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]), - ]), - ]) - ) - : ( - h('div.confirm-send-token__hero-amount-wrapper', [ - h('h3.flex-center.confirm-screen-send-amount', tokenAmount), - h('h3.flex-center.confirm-screen-send-amount-currency', symbol), - h('div.flex-center.confirm-memo-wrapper', [ - h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]), - ]), - ]) - ) -} - -ConfirmSendToken.prototype.renderGasFee = function () { - const { - currentCurrency: convertedCurrency, - conversionRate, - send: { gasTotal, gasLimit: sendGasLimit, gasPrice: sendGasPrice }, - showCustomizeGasModal, - } = this.props - const txMeta = this.gatherTxMeta() - const { gasFeeInHex } = this.getGasFee() - - return ( - h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]), - h('div.confirm-screen-section-column', [ - h(GasFeeDisplay, { - gasTotal: gasTotal || gasFeeInHex, - conversionRate, - convertedCurrency, - onClick: () => showCustomizeGasModal(txMeta, sendGasLimit, sendGasPrice, gasTotal), - }), - ]), - ]) - ) -} - -ConfirmSendToken.prototype.renderTotalPlusGas = function () { - const { token: { symbol }, currentCurrency, send: { errors } } = this.props - const { fiat: fiatAmount, token: tokenAmount } = this.getAmount() - const { fiat: fiatGas, token: tokenGas } = this.getGasFee() - - const totalInFIAT = fiatAmount && fiatGas && addCurrencies(fiatAmount, fiatGas) - const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency) - - return fiatAmount && fiatGas - ? ( - h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [ - h('div.confirm-screen-section-column', [ - h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]), - h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]), - ]), - - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `${convertedTotalInFiat} ${currentCurrency}`), - h('div.confirm-screen-row-detail', `${addCurrencies(tokenAmount, tokenGas || '0')} ${symbol}`), - ]), - ]) - ) - : ( - h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [ - h('div', { - className: classnames({ - 'confirm-screen-section-column--with-error': errors['insufficientFunds'], - 'confirm-screen-section-column': !errors['insufficientFunds'], - }), - }, [ - h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]), - h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]), - ]), - - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', `${tokenAmount} ${symbol}`), - h('div.confirm-screen-row-detail', `+ ${fiatGas} ${currentCurrency} ${this.context.t('gas')}`), - ]), - - this.renderErrorMessage('insufficientFunds'), - ]) - ) -} - -ConfirmSendToken.prototype.renderErrorMessage = function (message) { - const { send: { errors } } = this.props - - return errors[message] - ? h('div.confirm-screen-error', [ errors[message] ]) - : null -} - -ConfirmSendToken.prototype.convertToRenderableCurrency = function (value, currencyCode) { - const upperCaseCurrencyCode = currencyCode.toUpperCase() - - return currencies.find(currency => currency.code === upperCaseCurrencyCode) - ? currencyFormatter.format(Number(value), { - code: upperCaseCurrencyCode, - }) - : value -} - -ConfirmSendToken.prototype.renderHeaderRow = function (isTxReprice) { - const windowType = window.METAMASK_UI_TYPE - const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && - windowType !== ENVIRONMENT_TYPE_POPUP - - if (isTxReprice && isFullScreen) { - return null - } - - return ( - h('.page-container__header-row', [ - h('span.page-container__back-button', { - onClick: () => this.editTransaction(), - style: { - visibility: isTxReprice ? 'hidden' : 'initial', - }, - }, 'Edit'), - !isFullScreen && h(NetworkDisplay), - ]) - ) -} - -ConfirmSendToken.prototype.renderHeader = function (isTxReprice) { - const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm') - const subtitle = isTxReprice - ? this.context.t('speedUpSubtitle') - : this.context.t('pleaseReviewTransaction') - - return ( - h('.page-container__header', [ - this.renderHeaderRow(isTxReprice), - h('.page-container__title', title), - h('.page-container__subtitle', subtitle), - ]) - ) -} - -ConfirmSendToken.prototype.render = function () { - const txMeta = this.gatherTxMeta() - const { - from: { - address: fromAddress, - name: fromName, - }, - to: { - address: toAddress, - name: toName, - }, - } = this.getData() - - const isTxReprice = Boolean(txMeta.lastGasPrice) - - return ( - h('div.confirm-screen-container.confirm-send-token', [ - // Main Send token Card - h('div.page-container', [ - this.renderHeader(isTxReprice), - h('.page-container__content', [ - h('div.flex-row.flex-center.confirm-screen-identicons', [ - h('div.confirm-screen-account-wrapper', [ - h( - Identicon, - { - address: fromAddress, - diameter: 60, - }, - ), - h('span.confirm-screen-account-name', fromName), - // h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)), - ]), - h('i.fa.fa-arrow-right.fa-lg'), - h('div.confirm-screen-account-wrapper', [ - h( - Identicon, - { - address: toAddress, - diameter: 60, - }, - ), - h('span.confirm-screen-account-name', toName), - // h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)), - ]), - ]), - - // h('h3.flex-center.confirm-screen-sending-to-message', { - // style: { - // textAlign: 'center', - // fontSize: '16px', - // }, - // }, [ - // `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`, - // ]), - - this.renderHeroAmount(), - - h('div.confirm-screen-rows', [ - h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]), - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', fromName), - h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`), - ]), - ]), - - toAddress && h('section.flex-row.flex-center.confirm-screen-row', [ - h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]), - h('div.confirm-screen-section-column', [ - h('div.confirm-screen-row-info', toName), - h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`), - ]), - ]), - - this.renderGasFee(), - - this.renderTotalPlusGas(), - - ]), - - ]), - - h('form#pending-tx-form', { - className: 'confirm-screen-form', - onSubmit: this.onSubmit, - }, [ - this.renderErrorMessage('simulationFails'), - h('.page-container__footer', [ - // Cancel Button - h('button.btn-cancel.page-container__footer-button.allcaps', { - onClick: (event) => this.cancel(event, txMeta), - }, this.context.t('cancel')), - - // Accept Button - h('button.btn-confirm.page-container__footer-button.allcaps', { - onClick: event => this.onSubmit(event), - }, [this.context.t('confirm')]), - ]), - ]), - ]), - ]) - ) -} - -ConfirmSendToken.prototype.onSubmit = function (event) { - event.preventDefault() - const { updateSendErrors } = this.props - const txMeta = this.gatherTxMeta() - const valid = this.checkValidity() - const balanceIsSufficient = this.isBalanceSufficient(txMeta) - this.setState({ valid, submitting: true }) - - if (valid && this.verifyGasParams() && balanceIsSufficient) { - this.props.sendTransaction(txMeta, event) - } else if (!balanceIsSufficient) { - updateSendErrors({ insufficientFunds: 'insufficientFunds' }) - } else { - updateSendErrors({ invalidGasParams: 'invalidGasParams' }) - this.setState({ submitting: false }) - } -} - -ConfirmSendToken.prototype.isBalanceSufficient = function (txMeta) { - const { - balance, - conversionRate, - } = this.props - const { - txParams: { - gas, - gasPrice, - }, - } = txMeta - const gasTotal = calcGasTotal(gas, gasPrice) - - return isBalanceSufficient({ - amount: '0', - gasTotal, - balance, - conversionRate, - }) -} - - -ConfirmSendToken.prototype.cancel = function (event, txMeta) { - event.preventDefault() - const { cancelTransaction } = this.props - - cancelTransaction(txMeta) - .then(() => this.props.history.push(DEFAULT_ROUTE)) -} - -ConfirmSendToken.prototype.checkValidity = function () { - const form = this.getFormEl() - const valid = form.checkValidity() - return valid -} - -ConfirmSendToken.prototype.getFormEl = function () { - const form = document.querySelector('form#pending-tx-form') - // Stub out form for unit tests: - if (!form) { - return { checkValidity () { return true } } - } - return form -} - -// After a customizable state value has been updated, -ConfirmSendToken.prototype.gatherTxMeta = function () { - const props = this.props - const state = this.state - const txData = clone(state.txData) || clone(props.txData) - - const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send - const { - lastGasPrice, - txParams: { - gasPrice: txGasPrice, - gas: txGasLimit, - }, - } = txData - - let forceGasMin - if (lastGasPrice) { - forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, { - multiplicandBase: 16, - multiplierBase: 10, - toNumericBase: 'hex', - })) - } - - txData.txParams.gasPrice = sendGasPrice || forceGasMin || txGasPrice - txData.txParams.gas = sendGasLimit || txGasLimit - - // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) - return txData -} - -ConfirmSendToken.prototype.verifyGasParams = function () { - // We call this in case the gas has not been modified at all - if (!this.state) { return true } - return ( - this._notZeroOrEmptyString(this.state.gas) && - this._notZeroOrEmptyString(this.state.gasPrice) - ) -} - -ConfirmSendToken.prototype._notZeroOrEmptyString = function (obj) { - return obj !== '' && obj !== '0x0' -} - -ConfirmSendToken.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { - const numBN = new BN(numerator) - const denomBN = new BN(denominator) - return targetBN.mul(numBN).div(denomBN) -} -- cgit From 301ae305b88d0a7e3a3cbca9a500f4b39753dc52 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Tue, 17 Jul 2018 13:43:15 -0700 Subject: Use Number constructor for number conversion. Use existing noConversionRateAvailable message --- .../confirm-token-transaction-base.component.js | 2 +- ui/app/selectors/confirm-transaction.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js index 618ff123c..365ae216e 100644 --- a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js +++ b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js @@ -39,7 +39,7 @@ export default class ConfirmTokenTransactionBase extends Component { const { currentCurrency, contractExchangeRate } = this.props if (typeof contractExchangeRate === 'undefined') { - return this.context.t('noConversionRate') + return this.context.t('noConversionRateAvailable') } else { const fiatTransactionAmount = this.getFiatTransactionAmount() return formatCurrency(fiatTransactionAmount, currentCurrency) diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js index 5f1ae225c..54016a30e 100644 --- a/ui/app/selectors/confirm-transaction.js +++ b/ui/app/selectors/confirm-transaction.js @@ -105,7 +105,7 @@ export const tokenAmountAndToAddressSelector = createSelector( const toParam = params.find(param => param.name === TOKEN_PARAM_TO) const valueParam = params.find(param => param.name === TOKEN_PARAM_VALUE) toAddress = toParam ? toParam.value : params[0].value - tokenAmount = valueParam ? +valueParam.value : +params[1].value + tokenAmount = valueParam ? Number(valueParam.value) : Number(params[1].value) } return { @@ -123,7 +123,7 @@ export const approveTokenAmountAndToAddressSelector = createSelector( if (params && params.length) { toAddress = params.find(param => param.name === TOKEN_PARAM_SPENDER).value - tokenAmount = +params.find(param => param.name === TOKEN_PARAM_VALUE).value + tokenAmount = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value) } return { @@ -142,7 +142,7 @@ export const sendTokenTokenAmountAndToAddressSelector = createSelector( if (params && params.length) { toAddress = params.find(param => param.name === TOKEN_PARAM_TO).value - tokenAmount = +params.find(param => param.name === TOKEN_PARAM_VALUE).value + tokenAmount = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value) if (tokenDecimals) { tokenAmount = calcTokenAmount(tokenAmount, tokenDecimals) -- cgit From cb97517b26a7732cbb7c4a9f30f85b5fa596e608 Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 17 Jul 2018 18:53:37 -0400 Subject: updated account list based on new designs --- .../connect-hardware/account-list.js | 38 ++++++++---- .../connect-hardware/connect-screen.js | 2 +- .../pages/create-account/connect-hardware/index.js | 2 +- ui/app/css/itcss/components/new-account.scss | 68 ++++++++++++++++++---- 4 files changed, 85 insertions(+), 25 deletions(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js index 3bd6a00a7..06102e16f 100644 --- a/ui/app/components/pages/create-account/connect-hardware/account-list.js +++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js @@ -8,16 +8,20 @@ class AccountList extends Component { super(props) } + renderHeader () { + return ( + h('div.hw-connect', [ + h('h3.hw-connect__title', {}, this.context.t('selectAnAccount')), + h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')), + ]) + ) + } + renderAccounts () { return h('div.hw-account-list', [ - h('div.hw-account-list__title_wrapper', [ - h('div.hw-account-list__title', {}, [this.context.t('selectAnAddress')]), - h('div.hw-account-list__device', {}, ['Trezor - ETH']), - ]), this.props.accounts.map((a, i) => { return h('div.hw-account-list__item', { key: a.address }, [ - h('span.hw-account-list__item__index', a.index + 1), h('div.hw-account-list__item__radio', [ h('input', { type: 'radio', @@ -32,10 +36,12 @@ class AccountList extends Component { { htmlFor: `address-${i}`, }, - `${a.address.slice(0, 4)}...${a.address.slice(-4)}` - ), + [ + h('span.hw-account-list__item__index', a.index + 1), + `${a.address.slice(0, 4)}...${a.address.slice(-4)}`, + h('span.hw-account-list__item__balance', `${a.balance}`), + ]), ]), - h('span.hw-account-list__item__balance', `${a.balance}`), h( 'a.hw-account-list__item__link', { @@ -71,9 +77,15 @@ class AccountList extends Component { } renderButtons () { - return h('div.new-account-create-form__buttons', {}, [ + const disabled = this.props.selectedAccount === null + const buttonProps = {} + if (disabled) { + buttonProps.disabled = true + } + + return h('div.new-account-connect-form__buttons', {}, [ h( - 'button.btn-default.btn--large.new-account-create-form__button', + 'button.btn-default.btn--large.new-account-connect-form__button', { onClick: this.props.onCancel.bind(this), }, @@ -81,9 +93,10 @@ class AccountList extends Component { ), h( - `button.btn-primary.btn--large.new-account-create-form__button ${this.props.selectedAccount === null ? '.btn-primary--disabled' : ''}`, + `button.btn-primary.btn--large.new-account-connect-form__button ${disabled ? '.btn-primary--disabled' : ''}`, { onClick: this.props.onUnlockAccount.bind(this), + ...buttonProps, }, [this.context.t('unlock')] ), @@ -99,7 +112,8 @@ class AccountList extends Component { } render () { - return h('div', {}, [ + return h('div.new-account-connect-form', {}, [ + this.renderHeader(), this.renderAccounts(), this.renderPagination(), this.renderButtons(), diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js index 8d9980b10..7fb36b511 100644 --- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -29,7 +29,7 @@ class ConnectScreen extends Component { renderConnectScreen () { return ( - h('div', {}, [ + h('div.new-account-connect-form', {}, [ h('div.hw-connect', [ h('h3.hw-connect__title', {}, this.context.t('trezorHardwareWallet')), h('p.hw-connect__msg', {}, this.context.t('connectToTrezorHelp')), diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index dc9907f31..da42ddead 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -150,7 +150,7 @@ class ConnectHardwareForm extends Component { } render () { - return h('div.new-account-create-form', [ + return h('div', [ this.renderError(), this.renderContent(), ]) diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index a44fab3be..0eacf4615 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -157,13 +157,14 @@ &__title { padding-top: 10px; font-weight: 500; + font-size: 18px; } &__msg { font-size: 14px; color: #9b9b9b; - margin-top: 15px; - margin-bottom: 15px; + margin-top: 10px; + margin-bottom: 20px; } &__link { @@ -242,12 +243,16 @@ &__item__index { display: flex; - width: 28px; + width: 24px; } &__item__radio { display: flex; flex: 1; + + input { + margin-right: 9px; + } } &__item__label { @@ -264,6 +269,7 @@ &__item__link { display: flex; + margin-top: 3px; } &__item__link img { @@ -278,11 +284,55 @@ margin-top: 10px; &__button { - height: 25px; - flex: initial; - min-width: 90px; - font-size: 12px; + height: 19px; + display: flex; + width: 47px; + color: #33a4e7; + font-size: 14px; + line-height: 19px; + border: none; + min-width: 46px; + margin-right: 0px; + margin-left: 16px; + padding: 0px; + } +} + +.new-account-connect-form { + display: flex; + flex-flow: column; + align-items: center; + padding: 15px 30px 0; + + &__buttons { + margin-top: 39px; + display: flex; + width: 100%; + justify-content: space-between; + } + + &__button { + width: 150px; + min-width: initial; + } + + &__button.btn-primary { + background-color: #259DE5; + } + + &__button.btn-primary { + background-color: #259DE5; + color: #FFFFFF; } + + &__button.btn-primary--disabled { + cursor: not-allowed; + opacity: .5; + } + + + + } .hw-forget-device-container { @@ -337,8 +387,4 @@ width: 150px; min-width: initial; } - - &__button.btn-primary--disabled { - cursor: 'not-allowed'; - } } -- cgit From cbb14f1d5e50c10865838a98452ecfb4b6cb8d6a Mon Sep 17 00:00:00 2001 From: brunobar79 Date: Tue, 17 Jul 2018 21:57:19 -0400 Subject: fix browser not supported screen --- .../components/pages/create-account/connect-hardware/connect-screen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui') diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js index 7fb36b511..d4c479a58 100644 --- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -9,7 +9,7 @@ class ConnectScreen extends Component { renderUnsupportedBrowser () { return ( - h('div', {}, [ + h('div.new-account-connect-form', {}, [ h('div.hw-connect', [ h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')), h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')), -- cgit From 3f9c3d76b6edb70f91363c1e0787707ed1440c41 Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Mon, 16 Jul 2018 18:21:02 -0230 Subject: Add hex data row to send screen --- .../send/send-content/send-content.component.js | 2 ++ .../send/send-content/send-hex-data-row/index.js | 1 + .../send-hex-data-row.component.js | 38 ++++++++++++++++++++++ .../send-hex-data-row.container.js | 0 ui/app/css/itcss/components/send.scss | 4 +-- 5 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 ui/app/components/send/send-content/send-hex-data-row/index.js create mode 100644 ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js create mode 100644 ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js (limited to 'ui') diff --git a/ui/app/components/send/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js index adc114c0e..7a0b1a18e 100644 --- a/ui/app/components/send/send-content/send-content.component.js +++ b/ui/app/components/send/send-content/send-content.component.js @@ -4,6 +4,7 @@ import PageContainerContent from '../../page-container/page-container-content.co 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/' export default class SendContent extends Component { @@ -20,6 +21,7 @@ export default class SendContent extends Component { this.props.updateGas(updateData)} /> this.props.updateGas(updateData)} /> +
) diff --git a/ui/app/components/send/send-content/send-hex-data-row/index.js b/ui/app/components/send/send-content/send-hex-data-row/index.js new file mode 100644 index 000000000..4371ef83d --- /dev/null +++ b/ui/app/components/send/send-content/send-hex-data-row/index.js @@ -0,0 +1 @@ +export { default } from './send-hex-data-row.component' diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js new file mode 100644 index 000000000..eaffce359 --- /dev/null +++ b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js @@ -0,0 +1,38 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper' + +export default class SendHexDataRow extends Component { + static propTypes = { + data: PropTypes.string, + inError: PropTypes.bool, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + onInput = (event) => { + event.target.value = event.target.value.replace(/\n/g, '') + } + + render () { + const { + inError, + } = this.props + + return ( + +