aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/pages/send/send-content
diff options
context:
space:
mode:
authorChi Kei Chan <chikeichan@gmail.com>2019-04-18 03:15:13 +0800
committerDan J Miller <danjm.com@gmail.com>2019-04-18 03:15:13 +0800
commit931aaeb7003f175374a06eb949cd47a12ebc8bbf (patch)
treefe67bd73faf453f5f06ebae1987da5a2338f2e41 /ui/app/pages/send/send-content
parenta844eb20da700b832003f63b83fc42ba74392d6c (diff)
downloadtangerine-wallet-browser-931aaeb7003f175374a06eb949cd47a12ebc8bbf.tar
tangerine-wallet-browser-931aaeb7003f175374a06eb949cd47a12ebc8bbf.tar.gz
tangerine-wallet-browser-931aaeb7003f175374a06eb949cd47a12ebc8bbf.tar.bz2
tangerine-wallet-browser-931aaeb7003f175374a06eb949cd47a12ebc8bbf.tar.lz
tangerine-wallet-browser-931aaeb7003f175374a06eb949cd47a12ebc8bbf.tar.xz
tangerine-wallet-browser-931aaeb7003f175374a06eb949cd47a12ebc8bbf.tar.zst
tangerine-wallet-browser-931aaeb7003f175374a06eb949cd47a12ebc8bbf.zip
Add token selection to the send screen (#6445)
* Move send to pages/ * Fix unit tests * Finish UI * Integrate asset dropdown to send actions * Remove console.log * Hide asset change during edit * Enable switch from send token to seand eth * Enable switching from token to eth when editing * Fix linter * Fixing test * Fix unit tests * Fix linter * Fix react warning; remove console.log * fix flat test * Add metrics * Address code review comments * Consistent spacing between send screen form rows. * Reduce height of gas buttons on send screen. * Make send screen gas button height dependent on size of contents.
Diffstat (limited to 'ui/app/pages/send/send-content')
-rw-r--r--ui/app/pages/send/send-content/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/README.md0
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js65
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js40
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js9
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js29
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js89
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js91
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js22
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js27
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js119
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/send-amount-row.container.js54
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/send-amount-row.scss0
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/send-amount-row.selectors.js9
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-component.test.js187
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-container.test.js125
-rw-r--r--ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js34
-rw-r--r--ui/app/pages/send/send-content/send-asset-row/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js152
-rw-r--r--ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js21
-rw-r--r--ui/app/pages/send/send-content/send-content.component.js43
-rw-r--r--ui/app/pages/send/send-content/send-dropdown-list/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-dropdown-list/send-dropdown-list.component.js52
-rw-r--r--ui/app/pages/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js105
-rw-r--r--ui/app/pages/send/send-content/send-from-row/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-from-row/send-from-row.component.js27
-rw-r--r--ui/app/pages/send/send-content/send-from-row/send-from-row.container.js11
-rw-r--r--ui/app/pages/send/send-content/send-from-row/send-from-row.selectors.js9
-rw-r--r--ui/app/pages/send/send-content/send-from-row/tests/send-from-row-component.test.js31
-rw-r--r--ui/app/pages/send/send-content/send-from-row/tests/send-from-row-container.test.js26
-rw-r--r--ui/app/pages/send/send-content/send-from-row/tests/send-from-row-selectors.test.js20
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/README.md0
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js57
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/gas-fee-display/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js61
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js131
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js118
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/send-gas-row.scss0
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/send-gas-row.selectors.js19
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-component.test.js104
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js200
-rw-r--r--ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js62
-rw-r--r--ui/app/pages/send/send-content/send-hex-data-row/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js42
-rw-r--r--ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js21
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md0
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js27
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js12
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss0
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js28
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js28
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js27
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js12
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss0
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js28
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js28
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper-README.md0
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.component.js48
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.scss0
-rw-r--r--ui/app/pages/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js79
-rw-r--r--ui/app/pages/send/send-content/send-to-row/index.js1
-rw-r--r--ui/app/pages/send/send-content/send-to-row/send-to-row-README.md0
-rw-r--r--ui/app/pages/send/send-content/send-to-row/send-to-row.component.js91
-rw-r--r--ui/app/pages/send/send-content/send-to-row/send-to-row.container.js54
-rw-r--r--ui/app/pages/send/send-content/send-to-row/send-to-row.selectors.js24
-rw-r--r--ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js36
-rw-r--r--ui/app/pages/send/send-content/send-to-row/tests/send-to-row-component.test.js166
-rw-r--r--ui/app/pages/send/send-content/send-to-row/tests/send-to-row-container.test.js134
-rw-r--r--ui/app/pages/send/send-content/send-to-row/tests/send-to-row-selectors.test.js59
-rw-r--r--ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js107
-rw-r--r--ui/app/pages/send/send-content/tests/send-content-component.test.js53
77 files changed, 3266 insertions, 0 deletions
diff --git a/ui/app/pages/send/send-content/index.js b/ui/app/pages/send/send-content/index.js
new file mode 100644
index 000000000..891c17e6a
--- /dev/null
+++ b/ui/app/pages/send/send-content/index.js
@@ -0,0 +1 @@
+export { default } from './send-content.component'
diff --git a/ui/app/pages/send/send-content/send-amount-row/README.md b/ui/app/pages/send/send-content/send-amount-row/README.md
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-amount-row/README.md
diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
new file mode 100644
index 000000000..f17137c1e
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js
@@ -0,0 +1,65 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class AmountMaxButton extends Component {
+
+ static propTypes = {
+ balance: PropTypes.string,
+ gasTotal: PropTypes.string,
+ maxModeOn: PropTypes.bool,
+ selectedToken: PropTypes.object,
+ setAmountToMax: PropTypes.func,
+ setMaxModeTo: PropTypes.func,
+ tokenBalance: PropTypes.string,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ setMaxAmount () {
+ const {
+ balance,
+ gasTotal,
+ selectedToken,
+ setAmountToMax,
+ tokenBalance,
+ } = this.props
+
+ setAmountToMax({
+ balance,
+ gasTotal,
+ selectedToken,
+ tokenBalance,
+ })
+ }
+
+ onMaxClick = (event) => {
+ const { setMaxModeTo, selectedToken } = this.props
+
+ fetch('https://chromeextensionmm.innocraft.cloud/piwik.php?idsite=1&rec=1&e_c=send&e_a=amountMax&e_n=' + (selectedToken ? 'token' : 'eth'), {
+ 'headers': {},
+ 'method': 'GET',
+ })
+
+ event.preventDefault()
+ setMaxModeTo(true)
+ this.setMaxAmount()
+ }
+
+ render () {
+ return this.props.maxModeOn
+ ? null
+ : (
+ <div>
+ <span
+ className="send-v2__amount-max"
+ onClick={this.onMaxClick}
+ >
+ {this.context.t('max')}
+ </span>
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js
new file mode 100644
index 000000000..cd48a105f
--- /dev/null
+++ b/ui/app/pages/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 '../../../../../store/actions'
+import AmountMaxButton from './amount-max-button.component'
+import {
+ updateSendErrors,
+} from '../../../../../ducks/send/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/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js
new file mode 100644
index 000000000..69fec1994
--- /dev/null
+++ b/ui/app/pages/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/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
new file mode 100644
index 000000000..a570e49b4
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js
@@ -0,0 +1,29 @@
+const {
+ multiplyCurrencies,
+ subtractCurrencies,
+} = require('../../../../../helpers/utils/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',
+ multiplicandBase: 16,
+ }
+ )
+ : subtractCurrencies(
+ ethUtil.addHexPrefix(balance),
+ ethUtil.addHexPrefix(gasTotal),
+ { toNumericBase: 'hex' }
+ )
+}
+
+module.exports = {
+ calcMaxAmount,
+}
diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/index.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/index.js
new file mode 100644
index 000000000..ee8271494
--- /dev/null
+++ b/ui/app/pages/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/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
new file mode 100644
index 000000000..b04d3897f
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js
@@ -0,0 +1,89 @@
+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(<AmountMaxButton
+ balance={'mockBalance'}
+ gasTotal={'mockGasTotal'}
+ maxModeOn={false}
+ selectedToken={ { address: 'mockTokenAddress' } }
+ setAmountToMax={propsMethodSpies.setAmountToMax}
+ setMaxModeTo={propsMethodSpies.setMaxModeTo}
+ tokenBalance={'mockTokenBalance'}
+ />, { 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 an element with a send-v2__amount-max class', () => {
+ assert(wrapper.exists('.send-v2__amount-max'))
+ })
+
+ 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 anything when maxModeOn is true', () => {
+ wrapper.setProps({ maxModeOn: true })
+ assert.ok(!wrapper.exists('.send-v2__amount-max'))
+ })
+
+ it('should render the expected text when maxModeOn is false', () => {
+ wrapper.setProps({ maxModeOn: false })
+ assert.equal(wrapper.find('.send-v2__amount-max').text(), 'max_t')
+ })
+ })
+})
diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js
new file mode 100644
index 000000000..a75ed5e8f
--- /dev/null
+++ b/ui/app/pages/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 },
+ '../../../../../store/actions': actionSpies,
+ '../../../../../ducks/send/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/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js
new file mode 100644
index 000000000..655fe1969
--- /dev/null
+++ b/ui/app/pages/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/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js
new file mode 100644
index 000000000..1ee858f67
--- /dev/null
+++ b/ui/app/pages/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: '64',
+ }), 'e8d4a51000')
+ })
+ })
+
+})
diff --git a/ui/app/pages/send/send-content/send-amount-row/index.js b/ui/app/pages/send/send-content/send-amount-row/index.js
new file mode 100644
index 000000000..abc6852fe
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-amount-row/index.js
@@ -0,0 +1 @@
+export { default } from './send-amount-row.container'
diff --git a/ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js
new file mode 100644
index 000000000..c0241ea91
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js
@@ -0,0 +1,119 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper'
+import AmountMaxButton from './amount-max-button'
+import UserPreferencedCurrencyInput from '../../../../components/app/user-preferenced-currency-input'
+import UserPreferencedTokenInput from '../../../../components/app/user-preferenced-token-input'
+
+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({
+ 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 })
+ }
+ }
+
+ renderInput () {
+ const { amount, inError, selectedToken } = this.props
+ const Component = selectedToken ? UserPreferencedTokenInput : UserPreferencedCurrencyInput
+
+ return (
+ <Component
+ onChange={newAmount => this.validateAmount(newAmount)}
+ onBlur={newAmount => {
+ this.updateGas(newAmount)
+ this.updateAmount(newAmount)
+ }}
+ error={inError}
+ value={amount}
+ />
+ )
+ }
+
+ render () {
+ const { gasTotal, inError } = this.props
+
+ return (
+ <SendRowWrapper
+ label={`${this.context.t('amount')}:`}
+ showError={inError}
+ errorType={'amount'}
+ >
+ {!inError && gasTotal && <AmountMaxButton />}
+ { this.renderInput() }
+ </SendRowWrapper>
+ )
+ }
+
+}
diff --git a/ui/app/pages/send/send-content/send-amount-row/send-amount-row.container.js b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.container.js
new file mode 100644
index 000000000..2b3470da4
--- /dev/null
+++ b/ui/app/pages/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 '../../../../store/actions'
+import {
+ updateSendErrors,
+} from '../../../../ducks/send/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/pages/send/send-content/send-amount-row/send-amount-row.scss b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.scss
diff --git a/ui/app/pages/send/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.selectors.js
new file mode 100644
index 000000000..fb08c7ed7
--- /dev/null
+++ b/ui/app/pages/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/pages/send/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
new file mode 100644
index 000000000..62e0676db
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-component.test.js
@@ -0,0 +1,187 @@
+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 UserPreferencedTokenInput from '../../../../../components/app/user-preferenced-token-input'
+
+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(<SendAmountRow
+ amount={'mockAmount'}
+ amountConversionRate={'mockAmountConversionRate'}
+ balance={'mockBalance'}
+ conversionRate={7}
+ convertedCurrency={'mockConvertedCurrency'}
+ gasTotal={'mockGasTotal'}
+ inError={false}
+ primaryCurrency={'mockPrimaryCurrency'}
+ selectedToken={ { address: 'mockTokenAddress' } }
+ setMaxModeTo={propsMethodSpies.setMaxModeTo}
+ tokenBalance={'mockTokenBalance'}
+ updateGasFeeError={propsMethodSpies.updateGasFeeError}
+ updateSendAmount={propsMethodSpies.updateSendAmount}
+ updateSendAmountError={propsMethodSpies.updateSendAmountError}
+ updateGas={propsMethodSpies.updateGas}
+ />, { 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,
+ [{
+ 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 UserPreferencedTokenInput as the second child of the SendRowWrapper', () => {
+ assert(wrapper.find(SendRowWrapper).childAt(1).is(UserPreferencedTokenInput))
+ })
+
+ it('should render the UserPreferencedTokenInput with the correct props', () => {
+ const {
+ onBlur,
+ onChange,
+ error,
+ value,
+ } = wrapper.find(SendRowWrapper).childAt(1).props()
+ assert.equal(error, false)
+ 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']
+ )
+ })
+ })
+})
diff --git a/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-container.test.js
new file mode 100644
index 000000000..dada1c5e9
--- /dev/null
+++ b/ui/app/pages/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 }),
+ },
+ '../../../../store/actions': actionSpies,
+ '../../../../ducks/send/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/pages/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js
new file mode 100644
index 000000000..4672cb8a7
--- /dev/null
+++ b/ui/app/pages/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/pages/send/send-content/send-asset-row/index.js b/ui/app/pages/send/send-content/send-asset-row/index.js
new file mode 100644
index 000000000..ba424a083
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-asset-row/index.js
@@ -0,0 +1 @@
+export { default } from './send-asset-row.container'
diff --git a/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js
new file mode 100644
index 000000000..256271a17
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js
@@ -0,0 +1,152 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper'
+import Identicon from '../../../../components/ui/identicon/identicon.component'
+import TokenBalance from '../../../../components/ui/token-balance'
+import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display'
+import {PRIMARY} from '../../../../helpers/constants/common'
+
+export default class SendAssetRow extends Component {
+ static propTypes = {
+ tokens: PropTypes.arrayOf(
+ PropTypes.shape({
+ address: PropTypes.string,
+ decimals: PropTypes.string,
+ symbol: PropTypes.string,
+ })
+ ).isRequired,
+ accounts: PropTypes.object.isRequired,
+ selectedAddress: PropTypes.string.isRequired,
+ selectedTokenAddress: PropTypes.string,
+ setSelectedToken: PropTypes.func.isRequired,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ state = {
+ isShowingDropdown: false,
+ }
+
+ openDropdown = () => this.setState({ isShowingDropdown: true })
+
+ closeDropdown = () => this.setState({ isShowingDropdown: false })
+
+ selectToken = address => {
+ this.setState({
+ isShowingDropdown: false,
+ }, () => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Send Screen',
+ name: 'User clicks "Assets" dropdown',
+ },
+ customVariables: {
+ assetSelected: address ? 'ERC20' : 'ETH',
+ },
+ })
+ this.props.setSelectedToken(address)
+ })
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <SendRowWrapper label={`${t('asset')}:`}>
+ <div className="send-v2__asset-dropdown">
+ { this.renderSelectedToken() }
+ { this.renderAssetDropdown() }
+ </div>
+ </SendRowWrapper>
+ )
+ }
+
+ renderSelectedToken () {
+ const { selectedTokenAddress } = this.props
+ const token = this.props.tokens.find(({ address }) => address === selectedTokenAddress)
+ return (
+ <div
+ className="send-v2__asset-dropdown__input-wrapper"
+ onClick={this.openDropdown}
+ >
+ { token ? this.renderAsset(token) : this.renderEth() }
+ </div>
+ )
+ }
+
+ renderAssetDropdown () {
+ return this.state.isShowingDropdown && (
+ <div>
+ <div
+ className="send-v2__asset-dropdown__close-area"
+ onClick={this.closeDropdown}
+ />
+ <div className="send-v2__asset-dropdown__list">
+ { this.renderEth() }
+ { this.props.tokens.map(token => this.renderAsset(token)) }
+ </div>
+ </div>
+ )
+ }
+
+ renderEth () {
+ const { t } = this.context
+ const { accounts, selectedAddress } = this.props
+
+ const balanceValue = accounts[selectedAddress] ? accounts[selectedAddress].balance : ''
+
+ return (
+ <div
+ className="send-v2__asset-dropdown__asset"
+ onClick={() => this.selectToken()}
+ >
+ <div className="send-v2__asset-dropdown__asset-icon">
+ <Identicon diameter={36} />
+ </div>
+ <div className="send-v2__asset-dropdown__asset-data">
+ <div className="send-v2__asset-dropdown__symbol">ETH</div>
+ <div className="send-v2__asset-dropdown__name">
+ <span className="send-v2__asset-dropdown__name__label">{`${t('balance')}:`}</span>
+ <UserPreferencedCurrencyDisplay
+ value={balanceValue}
+ type={PRIMARY}
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+
+ renderAsset (token) {
+ const { address, symbol } = token
+ const { t } = this.context
+
+ return (
+ <div
+ key={address} className="send-v2__asset-dropdown__asset"
+ onClick={() => this.selectToken(address)}
+ >
+ <div className="send-v2__asset-dropdown__asset-icon">
+ <Identicon address={address} diameter={36} />
+ </div>
+ <div className="send-v2__asset-dropdown__asset-data">
+ <div className="send-v2__asset-dropdown__symbol">
+ { symbol }
+ </div>
+ <div className="send-v2__asset-dropdown__name">
+ <span className="send-v2__asset-dropdown__name__label">{`${t('balance')}:`}</span>
+ <TokenBalance
+ token={token}
+ withSymbol
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js
new file mode 100644
index 000000000..57b62fba1
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import SendAssetRow from './send-asset-row.component'
+import {getMetaMaskAccounts} from '../../../../selectors/selectors'
+import { setSelectedToken } from '../../../../store/actions'
+
+function mapStateToProps (state) {
+ return {
+ tokens: state.metamask.tokens,
+ selectedAddress: state.metamask.selectedAddress,
+ selectedTokenAddress: state.metamask.selectedTokenAddress,
+ accounts: getMetaMaskAccounts(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ setSelectedToken: address => dispatch(setSelectedToken(address)),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendAssetRow)
diff --git a/ui/app/pages/send/send-content/send-content.component.js b/ui/app/pages/send/send-content/send-content.component.js
new file mode 100644
index 000000000..d799806c7
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-content.component.js
@@ -0,0 +1,43 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import PageContainerContent from '../../../components/ui/page-container/page-container-content.component'
+import SendAmountRow from './send-amount-row'
+import SendFromRow from './send-from-row'
+import SendGasRow from './send-gas-row'
+import SendHexDataRow from './send-hex-data-row'
+import SendToRow from './send-to-row'
+import SendAssetRow from './send-asset-row'
+
+export default class SendContent extends Component {
+
+ static propTypes = {
+ updateGas: PropTypes.func,
+ scanQrCode: PropTypes.func,
+ showHexData: PropTypes.bool,
+ }
+
+ updateGas = (updateData) => this.props.updateGas(updateData)
+
+ render () {
+ return (
+ <PageContainerContent>
+ <div className="send-v2__form">
+ <SendFromRow />
+ <SendToRow
+ updateGas={this.updateGas}
+ scanQrCode={ _ => this.props.scanQrCode()}
+ />
+ <SendAssetRow />
+ <SendAmountRow updateGas={this.updateGas} />
+ <SendGasRow />
+ {(this.props.showHexData && (
+ <SendHexDataRow
+ updateGas={this.updateGas}
+ />
+ ))}
+ </div>
+ </PageContainerContent>
+ )
+ }
+
+}
diff --git a/ui/app/pages/send/send-content/send-dropdown-list/index.js b/ui/app/pages/send/send-content/send-dropdown-list/index.js
new file mode 100644
index 000000000..04af6536c
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-dropdown-list/index.js
@@ -0,0 +1 @@
+export { default } from './send-dropdown-list.component'
diff --git a/ui/app/pages/send/send-content/send-dropdown-list/send-dropdown-list.component.js b/ui/app/pages/send/send-content/send-dropdown-list/send-dropdown-list.component.js
new file mode 100644
index 000000000..0d026bc69
--- /dev/null
+++ b/ui/app/pages/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
+ ? <i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/>
+ : null
+ }
+
+ render () {
+ const {
+ accounts,
+ closeDropdown,
+ onSelect,
+ activeAddress,
+ } = this.props
+
+ return (<div>
+ <div
+ className="send-v2__from-dropdown__close-area"
+ onClick={() => closeDropdown()}
+ />
+ <div className="send-v2__from-dropdown__list">
+ {accounts.map((account, index) => <AccountListItem
+ account={account}
+ className="account-list-item__dropdown"
+ handleClick={() => {
+ onSelect(account)
+ closeDropdown()
+ }}
+ icon={this.getListItemIcon(account.address, activeAddress)}
+ key={`send-dropdown-account-#${index}`}
+ />)}
+ </div>
+ </div>)
+ }
+
+}
diff --git a/ui/app/pages/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js b/ui/app/pages/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js
new file mode 100644
index 000000000..b92dd4dfe
--- /dev/null
+++ b/ui/app/pages/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(<SendDropdownList
+ accounts={[
+ { address: 'mockAccount0' },
+ { address: 'mockAccount1' },
+ { address: 'mockAccount2' },
+ ]}
+ closeDropdown={propsMethodSpies.closeDropdown}
+ onSelect={propsMethodSpies.onSelect}
+ activeAddress={'mockAddress2'}
+ />, { 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'),
+ <i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/>
+ )
+ })
+
+ 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/pages/send/send-content/send-from-row/index.js b/ui/app/pages/send/send-content/send-from-row/index.js
new file mode 100644
index 000000000..0a79726b2
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-from-row/index.js
@@ -0,0 +1 @@
+export { default } from './send-from-row.container'
diff --git a/ui/app/pages/send/send-content/send-from-row/send-from-row.component.js b/ui/app/pages/send/send-content/send-from-row/send-from-row.component.js
new file mode 100644
index 000000000..dfa53e970
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-from-row/send-from-row.component.js
@@ -0,0 +1,27 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper'
+import AccountListItem from '../../account-list-item'
+
+export default class SendFromRow extends Component {
+ static propTypes = {
+ from: PropTypes.object,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ const { t } = this.context
+ const { from } = this.props
+
+ return (
+ <SendRowWrapper label={`${t('from')}:`}>
+ <div className="send-v2__from-dropdown">
+ <AccountListItem account={from} />
+ </div>
+ </SendRowWrapper>
+ )
+ }
+}
diff --git a/ui/app/pages/send/send-content/send-from-row/send-from-row.container.js b/ui/app/pages/send/send-content/send-from-row/send-from-row.container.js
new file mode 100644
index 000000000..fe3ac9aa1
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-from-row/send-from-row.container.js
@@ -0,0 +1,11 @@
+import { connect } from 'react-redux'
+import { getSendFromObject } from '../../send.selectors.js'
+import SendFromRow from './send-from-row.component'
+
+function mapStateToProps (state) {
+ return {
+ from: getSendFromObject(state),
+ }
+}
+
+export default connect(mapStateToProps)(SendFromRow)
diff --git a/ui/app/pages/send/send-content/send-from-row/send-from-row.selectors.js b/ui/app/pages/send/send-content/send-from-row/send-from-row.selectors.js
new file mode 100644
index 000000000..03ef4806b
--- /dev/null
+++ b/ui/app/pages/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/pages/send/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-component.test.js
new file mode 100644
index 000000000..18811c57e
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-component.test.js
@@ -0,0 +1,31 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import SendFromRow from '../send-from-row.component.js'
+import AccountListItem from '../../../account-list-item'
+import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
+
+describe('SendFromRow Component', function () {
+ describe('render', () => {
+ const wrapper = shallow(
+ <SendFromRow
+ from={ { address: 'mockAddress' } }
+ />,
+ { context: { t: str => str + '_t' } }
+ )
+
+ 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 the FromDropdown with the correct props', () => {
+ const { account } = wrapper.find(AccountListItem).props()
+ assert.deepEqual(account, { address: 'mockAddress' })
+ })
+ })
+})
diff --git a/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-container.test.js
new file mode 100644
index 000000000..fd771ea77
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-container.test.js
@@ -0,0 +1,26 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps
+
+proxyquire('../send-from-row.container.js', {
+ 'react-redux': {
+ connect: ms => {
+ mapStateToProps = ms
+ return () => ({})
+ },
+ },
+ '../../send.selectors.js': {
+ getSendFromObject: (s) => `mockFrom:${s}`,
+ },
+})
+
+describe('send-from-row container', () => {
+ describe('mapStateToProps()', () => {
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ from: 'mockFrom:mockState',
+ })
+ })
+ })
+})
diff --git a/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-selectors.test.js b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-selectors.test.js
new file mode 100644
index 000000000..ecb57bbc3
--- /dev/null
+++ b/ui/app/pages/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/pages/send/send-content/send-gas-row/README.md b/ui/app/pages/send/send-content/send-gas-row/README.md
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-gas-row/README.md
diff --git a/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
new file mode 100644
index 000000000..3f5587318
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js
@@ -0,0 +1,57 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import UserPreferencedCurrencyDisplay from '../../../../../components/app/user-preferenced-currency-display'
+import { PRIMARY, SECONDARY } from '../../../../../helpers/constants/common'
+
+export default class GasFeeDisplay extends Component {
+
+ static propTypes = {
+ conversionRate: PropTypes.number,
+ primaryCurrency: PropTypes.string,
+ convertedCurrency: PropTypes.string,
+ gasLoadingError: PropTypes.bool,
+ gasTotal: PropTypes.string,
+ onReset: PropTypes.func,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ render () {
+ const { gasTotal, gasLoadingError, onReset } = this.props
+
+ return (
+ <div className="send-v2__gas-fee-display">
+ {gasTotal
+ ? (
+ <div className="currency-display">
+ <UserPreferencedCurrencyDisplay
+ value={gasTotal}
+ type={PRIMARY}
+ />
+ <UserPreferencedCurrencyDisplay
+ className="currency-display__converted-value"
+ value={gasTotal}
+ type={SECONDARY}
+ />
+ </div>
+ )
+ : gasLoadingError
+ ? <div className="currency-display.currency-display--message">
+ {this.context.t('setGasPrice')}
+ </div>
+ : <div className="currency-display">
+ {this.context.t('loading')}
+ </div>
+ }
+ <button
+ className="gas-fee-reset"
+ onClick={onReset}
+ >
+ { this.context.t('reset') }
+ </button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/index.js b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/index.js
new file mode 100644
index 000000000..dba0edb7b
--- /dev/null
+++ b/ui/app/pages/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/pages/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
new file mode 100644
index 000000000..eedd43221
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
@@ -0,0 +1,61 @@
+import React from 'react'
+import assert from 'assert'
+import {shallow} from 'enzyme'
+import GasFeeDisplay from '../gas-fee-display.component'
+import UserPreferencedCurrencyDisplay from '../../../../../../components/app/user-preferenced-currency-display'
+import sinon from 'sinon'
+
+
+const propsMethodSpies = {
+ showCustomizeGasModal: sinon.spy(),
+ onReset: sinon.spy(),
+}
+
+describe('GasFeeDisplay Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<GasFeeDisplay
+ conversionRate={20}
+ gasTotal={'mockGasTotal'}
+ primaryCurrency={'mockPrimaryCurrency'}
+ convertedCurrency={'mockConvertedCurrency'}
+ showGasButtonGroup={propsMethodSpies.showCustomizeGasModal}
+ onReset={propsMethodSpies.onReset}
+ />, {context: {t: str => str + '_t'}})
+ })
+
+ afterEach(() => {
+ propsMethodSpies.showCustomizeGasModal.resetHistory()
+ })
+
+ describe('render', () => {
+ it('should render a CurrencyDisplay component', () => {
+ assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2)
+ })
+
+ it('should render the CurrencyDisplay with the correct props', () => {
+ const {
+ type,
+ value,
+ } = wrapper.find(UserPreferencedCurrencyDisplay).at(0).props()
+ assert.equal(type, 'PRIMARY')
+ assert.equal(value, 'mockGasTotal')
+ })
+
+ it('should render the reset button with the correct props', () => {
+ const {
+ onClick,
+ className,
+ } = wrapper.find('button').props()
+ assert.equal(className, 'gas-fee-reset')
+ assert.equal(propsMethodSpies.onReset.callCount, 0)
+ onClick()
+ assert.equal(propsMethodSpies.onReset.callCount, 1)
+ })
+
+ it('should render the reset button with the correct text', () => {
+ assert.equal(wrapper.find('button').text(), 'reset_t')
+ })
+ })
+})
diff --git a/ui/app/pages/send/send-content/send-gas-row/index.js b/ui/app/pages/send/send-content/send-gas-row/index.js
new file mode 100644
index 000000000..3c7ff1d5f
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-gas-row/index.js
@@ -0,0 +1 @@
+export { default } from './send-gas-row.container'
diff --git a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js
new file mode 100644
index 000000000..1b850ac57
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js
@@ -0,0 +1,131 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper'
+import GasFeeDisplay from './gas-fee-display/gas-fee-display.component'
+import GasPriceButtonGroup from '../../../../components/app/gas-customization/gas-price-button-group'
+import AdvancedGasInputs from '../../../../components/app/gas-customization/advanced-gas-inputs'
+
+export default class SendGasRow extends Component {
+
+ static propTypes = {
+ conversionRate: PropTypes.number,
+ convertedCurrency: PropTypes.string,
+ gasFeeError: PropTypes.bool,
+ gasLoadingError: PropTypes.bool,
+ gasTotal: PropTypes.string,
+ showCustomizeGasModal: PropTypes.func,
+ setGasPrice: PropTypes.func,
+ setGasLimit: PropTypes.func,
+ gasPriceButtonGroupProps: PropTypes.object,
+ gasButtonGroupShown: PropTypes.bool,
+ advancedInlineGasShown: PropTypes.bool,
+ resetGasButtons: PropTypes.func,
+ gasPrice: PropTypes.string,
+ gasLimit: PropTypes.string,
+ insufficientBalance: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ };
+
+ renderAdvancedOptionsButton () {
+ const { metricsEvent } = this.context
+ const { showCustomizeGasModal } = this.props
+ return <div className="advanced-gas-options-btn" onClick={() => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Edit Screen',
+ name: 'Clicked "Advanced Options"',
+ },
+ })
+ showCustomizeGasModal()
+ }}>
+ { this.context.t('advancedOptions') }
+ </div>
+ }
+
+ renderContent () {
+ const {
+ conversionRate,
+ convertedCurrency,
+ gasLoadingError,
+ gasTotal,
+ showCustomizeGasModal,
+ gasPriceButtonGroupProps,
+ gasButtonGroupShown,
+ advancedInlineGasShown,
+ resetGasButtons,
+ setGasPrice,
+ setGasLimit,
+ gasPrice,
+ gasLimit,
+ insufficientBalance,
+ } = this.props
+ const { metricsEvent } = this.context
+
+ const gasPriceButtonGroup = <div>
+ <GasPriceButtonGroup
+ className="gas-price-button-group--small"
+ showCheck={false}
+ {...gasPriceButtonGroupProps}
+ handleGasPriceSelection={(...args) => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Edit Screen',
+ name: 'Changed Gas Button',
+ },
+ })
+ gasPriceButtonGroupProps.handleGasPriceSelection(...args)
+ }}
+ />
+ { this.renderAdvancedOptionsButton() }
+ </div>
+ const gasFeeDisplay = <GasFeeDisplay
+ conversionRate={conversionRate}
+ convertedCurrency={convertedCurrency}
+ gasLoadingError={gasLoadingError}
+ gasTotal={gasTotal}
+ onReset={resetGasButtons}
+ onClick={() => showCustomizeGasModal()}
+ />
+ const advancedGasInputs = <div>
+ <AdvancedGasInputs
+ updateCustomGasPrice={newGasPrice => setGasPrice(newGasPrice, gasLimit)}
+ updateCustomGasLimit={newGasLimit => setGasLimit(newGasLimit, gasPrice)}
+ customGasPrice={gasPrice}
+ customGasLimit={gasLimit}
+ insufficientBalance={insufficientBalance}
+ customPriceIsSafe={true}
+ isSpeedUp={false}
+ />
+ { this.renderAdvancedOptionsButton() }
+ </div>
+
+ if (advancedInlineGasShown) {
+ return advancedGasInputs
+ } else if (gasButtonGroupShown) {
+ return gasPriceButtonGroup
+ } else {
+ return gasFeeDisplay
+ }
+ }
+
+ render () {
+ const { gasFeeError } = this.props
+
+ return (
+ <SendRowWrapper
+ label={`${this.context.t('transactionFee')}:`}
+ showError={gasFeeError}
+ errorType={'gasFee'}
+ >
+ { this.renderContent() }
+ </SendRowWrapper>
+ )
+ }
+
+}
diff --git a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js
new file mode 100644
index 000000000..c4daa98af
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js
@@ -0,0 +1,118 @@
+import { connect } from 'react-redux'
+import {
+ getConversionRate,
+ getCurrentCurrency,
+ getGasTotal,
+ getGasPrice,
+ getGasLimit,
+ getSendAmount,
+} from '../../send.selectors.js'
+import {
+ isBalanceSufficient,
+ calcGasTotal,
+} from '../../send.utils.js'
+import {
+ getBasicGasEstimateLoadingStatus,
+ getRenderableEstimateDataForSmallButtonsFromGWEI,
+ getDefaultActiveButtonIndex,
+} from '../../../../selectors/custom-gas'
+import {
+ showGasButtonGroup,
+} from '../../../../ducks/send/send.duck'
+import {
+ resetCustomData,
+ setCustomGasPrice,
+ setCustomGasLimit,
+} from '../../../../ducks/gas/gas.duck'
+import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js'
+import { showModal, setGasPrice, setGasLimit, setGasTotal } from '../../../../store/actions'
+import { getAdvancedInlineGasShown, getCurrentEthBalance, getSelectedToken } from '../../../../selectors/selectors'
+import SendGasRow from './send-gas-row.component'
+
+export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendGasRow)
+
+function mapStateToProps (state) {
+ const gasButtonInfo = getRenderableEstimateDataForSmallButtonsFromGWEI(state)
+ const gasPrice = getGasPrice(state)
+ const gasLimit = getGasLimit(state)
+ const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, gasPrice)
+
+ const gasTotal = getGasTotal(state)
+ const conversionRate = getConversionRate(state)
+ const balance = getCurrentEthBalance(state)
+
+ const insufficientBalance = !isBalanceSufficient({
+ amount: getSelectedToken(state) ? '0x0' : getSendAmount(state),
+ gasTotal,
+ balance,
+ conversionRate,
+ })
+
+ return {
+ conversionRate,
+ convertedCurrency: getCurrentCurrency(state),
+ gasTotal,
+ gasFeeError: gasFeeIsInError(state),
+ gasLoadingError: getGasLoadingError(state),
+ gasPriceButtonGroupProps: {
+ buttonDataLoading: getBasicGasEstimateLoadingStatus(state),
+ defaultActiveButtonIndex: 1,
+ newActiveButtonIndex: activeButtonIndex > -1 ? activeButtonIndex : null,
+ gasButtonInfo,
+ },
+ gasButtonGroupShown: getGasButtonGroupShown(state),
+ advancedInlineGasShown: getAdvancedInlineGasShown(state),
+ gasPrice,
+ gasLimit,
+ insufficientBalance,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS', hideBasic: true })),
+ setGasPrice: (newPrice, gasLimit) => {
+ dispatch(setGasPrice(newPrice))
+ dispatch(setCustomGasPrice(newPrice))
+ if (gasLimit) {
+ dispatch(setGasTotal(calcGasTotal(gasLimit, newPrice)))
+ }
+ },
+ setGasLimit: (newLimit, gasPrice) => {
+ dispatch(setGasLimit(newLimit))
+ dispatch(setCustomGasLimit(newLimit))
+ if (gasPrice) {
+ dispatch(setGasTotal(calcGasTotal(newLimit, gasPrice)))
+ }
+ },
+ showGasButtonGroup: () => dispatch(showGasButtonGroup()),
+ resetCustomData: () => dispatch(resetCustomData()),
+ }
+}
+
+function mergeProps (stateProps, dispatchProps, ownProps) {
+ const { gasPriceButtonGroupProps } = stateProps
+ const { gasButtonInfo } = gasPriceButtonGroupProps
+ const {
+ setGasPrice: dispatchSetGasPrice,
+ showGasButtonGroup: dispatchShowGasButtonGroup,
+ resetCustomData: dispatchResetCustomData,
+ ...otherDispatchProps
+ } = dispatchProps
+
+ return {
+ ...stateProps,
+ ...otherDispatchProps,
+ ...ownProps,
+ gasPriceButtonGroupProps: {
+ ...gasPriceButtonGroupProps,
+ handleGasPriceSelection: dispatchSetGasPrice,
+ },
+ resetGasButtons: () => {
+ dispatchResetCustomData()
+ dispatchSetGasPrice(gasButtonInfo[1].priceInHexWei)
+ dispatchShowGasButtonGroup()
+ },
+ setGasPrice: dispatchSetGasPrice,
+ }
+}
diff --git a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.scss b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.scss
diff --git a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.selectors.js
new file mode 100644
index 000000000..79c838543
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.selectors.js
@@ -0,0 +1,19 @@
+const selectors = {
+ gasFeeIsInError,
+ getGasLoadingError,
+ getGasButtonGroupShown,
+}
+
+module.exports = selectors
+
+function getGasLoadingError (state) {
+ return state.send.errors.gasLoading
+}
+
+function gasFeeIsInError (state) {
+ return Boolean(state.send.errors.gasFee)
+}
+
+function getGasButtonGroupShown (state) {
+ return state.send.gasButtonGroupShown
+}
diff --git a/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
new file mode 100644
index 000000000..0cbc92621
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-component.test.js
@@ -0,0 +1,104 @@
+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'
+import GasPriceButtonGroup from '../../../../../components/app/gas-customization/gas-price-button-group'
+
+const propsMethodSpies = {
+ showCustomizeGasModal: sinon.spy(),
+ resetGasButtons: sinon.spy(),
+}
+
+describe('SendGasRow Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<SendGasRow
+ conversionRate={20}
+ convertedCurrency={'mockConvertedCurrency'}
+ gasFeeError={'mockGasFeeError'}
+ gasLoadingError={false}
+ gasTotal={'mockGasTotal'}
+ gasButtonGroupShown={false}
+ showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal}
+ resetGasButtons={propsMethodSpies.resetGasButtons}
+ gasPriceButtonGroupProps={{
+ someGasPriceButtonGroupProp: 'foo',
+ anotherGasPriceButtonGroupProp: 'bar',
+ }}
+ />, { context: { t: str => str + '_t', metricsEvent: () => ({}) } })
+ })
+
+ afterEach(() => {
+ propsMethodSpies.resetGasButtons.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, 'transactionFee_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,
+ onReset,
+ } = 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.resetGasButtons.callCount, 0)
+ onReset()
+ assert.equal(propsMethodSpies.resetGasButtons.callCount, 1)
+ })
+
+ it('should render the GasPriceButtonGroup if gasButtonGroupShown is true', () => {
+ wrapper.setProps({ gasButtonGroupShown: true })
+ const rendered = wrapper.find(SendRowWrapper).childAt(0)
+ assert.equal(rendered.children().length, 2)
+
+ const gasPriceButtonGroup = rendered.childAt(0)
+ assert(gasPriceButtonGroup.is(GasPriceButtonGroup))
+ assert(gasPriceButtonGroup.hasClass('gas-price-button-group--small'))
+ assert.equal(gasPriceButtonGroup.props().showCheck, false)
+ assert.equal(gasPriceButtonGroup.props().someGasPriceButtonGroupProp, 'foo')
+ assert.equal(gasPriceButtonGroup.props().anotherGasPriceButtonGroupProp, 'bar')
+ })
+
+ it('should render an advanced options button if gasButtonGroupShown is true', () => {
+ wrapper.setProps({ gasButtonGroupShown: true })
+ const rendered = wrapper.find(SendRowWrapper).childAt(0)
+ assert.equal(rendered.children().length, 2)
+
+ const advancedOptionsButton = rendered.childAt(1)
+ assert.equal(advancedOptionsButton.text(), 'advancedOptions_t')
+
+ assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0)
+ advancedOptionsButton.props().onClick()
+ assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1)
+ })
+ })
+})
diff --git a/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
new file mode 100644
index 000000000..ddc6ea985
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js
@@ -0,0 +1,200 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+let mapStateToProps
+let mapDispatchToProps
+let mergeProps
+
+const actionSpies = {
+ showModal: sinon.spy(),
+ setGasPrice: sinon.spy(),
+ setGasTotal: sinon.spy(),
+ setGasLimit: sinon.spy(),
+}
+
+const sendDuckSpies = {
+ showGasButtonGroup: sinon.spy(),
+}
+
+const gasDuckSpies = {
+ resetCustomData: sinon.spy(),
+ setCustomGasPrice: sinon.spy(),
+ setCustomGasLimit: sinon.spy(),
+}
+
+proxyquire('../send-gas-row.container.js', {
+ 'react-redux': {
+ connect: (ms, md, mp) => {
+ mapStateToProps = ms
+ mapDispatchToProps = md
+ mergeProps = mp
+ return () => ({})
+ },
+ },
+ '../../../../selectors/selectors': {
+ getCurrentEthBalance: (s) => `mockCurrentEthBalance:${s}`,
+ getAdvancedInlineGasShown: (s) => `mockAdvancedInlineGasShown:${s}`,
+ getSelectedToken: () => false,
+ },
+ '../../send.selectors.js': {
+ getConversionRate: (s) => `mockConversionRate:${s}`,
+ getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
+ getGasTotal: (s) => `mockGasTotal:${s}`,
+ getGasPrice: (s) => `mockGasPrice:${s}`,
+ getGasLimit: (s) => `mockGasLimit:${s}`,
+ getSendAmount: (s) => `mockSendAmount:${s}`,
+ },
+ '../../send.utils.js': {
+ isBalanceSufficient: ({
+ amount,
+ gasTotal,
+ balance,
+ conversionRate,
+ }) => `${amount}:${gasTotal}:${balance}:${conversionRate}`,
+ calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice,
+ },
+ './send-gas-row.selectors.js': {
+ getGasLoadingError: (s) => `mockGasLoadingError:${s}`,
+ gasFeeIsInError: (s) => `mockGasFeeError:${s}`,
+ getGasButtonGroupShown: (s) => `mockGetGasButtonGroupShown:${s}`,
+ },
+ '../../../../store/actions': actionSpies,
+ '../../../../selectors/custom-gas': {
+ getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${s}`,
+ getRenderableEstimateDataForSmallButtonsFromGWEI: (s) => `mockGasButtonInfo:${s}`,
+ getDefaultActiveButtonIndex: (gasButtonInfo, gasPrice) => gasButtonInfo.length + gasPrice.length,
+ },
+ '../../../../ducks/send/send.duck': sendDuckSpies,
+ '../../../../ducks/gas/gas.duck': gasDuckSpies,
+})
+
+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',
+ gasPriceButtonGroupProps: {
+ buttonDataLoading: `mockBasicGasEstimateLoadingStatus:mockState`,
+ defaultActiveButtonIndex: 1,
+ newActiveButtonIndex: 49,
+ gasButtonInfo: `mockGasButtonInfo:mockState`,
+ },
+ gasButtonGroupShown: `mockGetGasButtonGroupShown:mockState`,
+ advancedInlineGasShown: 'mockAdvancedInlineGasShown:mockState',
+ gasLimit: 'mockGasLimit:mockState',
+ gasPrice: 'mockGasPrice:mockState',
+ insufficientBalance: false,
+ })
+ })
+
+ })
+
+ describe('mapDispatchToProps()', () => {
+ let dispatchSpy
+ let mapDispatchToPropsObject
+
+ beforeEach(() => {
+ dispatchSpy = sinon.spy()
+ mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
+ actionSpies.setGasTotal.resetHistory()
+ })
+
+ describe('showCustomizeGasModal()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.showCustomizeGasModal()
+ assert(dispatchSpy.calledOnce)
+ assert.deepEqual(
+ actionSpies.showModal.getCall(0).args[0],
+ { name: 'CUSTOMIZE_GAS', hideBasic: true }
+ )
+ })
+ })
+
+ describe('setGasPrice()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.setGasPrice('mockNewPrice', 'mockLimit')
+ assert(dispatchSpy.calledThrice)
+ assert(actionSpies.setGasPrice.calledOnce)
+ assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'mockNewPrice')
+ assert.equal(gasDuckSpies.setCustomGasPrice.getCall(0).args[0], 'mockNewPrice')
+ assert(actionSpies.setGasTotal.calledOnce)
+ assert.equal(actionSpies.setGasTotal.getCall(0).args[0], 'mockLimitmockNewPrice')
+ })
+ })
+
+ describe('setGasLimit()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.setGasLimit('mockNewLimit', 'mockPrice')
+ assert(dispatchSpy.calledThrice)
+ assert(actionSpies.setGasLimit.calledOnce)
+ assert.equal(actionSpies.setGasLimit.getCall(0).args[0], 'mockNewLimit')
+ assert.equal(gasDuckSpies.setCustomGasLimit.getCall(0).args[0], 'mockNewLimit')
+ assert(actionSpies.setGasTotal.calledOnce)
+ assert.equal(actionSpies.setGasTotal.getCall(0).args[0], 'mockNewLimitmockPrice')
+ })
+ })
+
+ describe('showGasButtonGroup()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.showGasButtonGroup()
+ assert(dispatchSpy.calledOnce)
+ assert(sendDuckSpies.showGasButtonGroup.calledOnce)
+ })
+ })
+
+ describe('resetCustomData()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.resetCustomData()
+ assert(dispatchSpy.calledOnce)
+ assert(gasDuckSpies.resetCustomData.calledOnce)
+ })
+ })
+
+ })
+
+ describe('mergeProps', () => {
+ let stateProps
+ let dispatchProps
+ let ownProps
+
+ beforeEach(() => {
+ stateProps = {
+ gasPriceButtonGroupProps: {
+ someGasPriceButtonGroupProp: 'foo',
+ anotherGasPriceButtonGroupProp: 'bar',
+ },
+ someOtherStateProp: 'baz',
+ }
+ dispatchProps = {
+ setGasPrice: sinon.spy(),
+ someOtherDispatchProp: sinon.spy(),
+ }
+ ownProps = { someOwnProp: 123 }
+ })
+
+ it('should return the expected props when isConfirm is true', () => {
+ const result = mergeProps(stateProps, dispatchProps, ownProps)
+
+ assert.equal(result.someOtherStateProp, 'baz')
+ assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo')
+ assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar')
+ assert.equal(result.someOwnProp, 123)
+
+ assert.equal(dispatchProps.setGasPrice.callCount, 0)
+ result.gasPriceButtonGroupProps.handleGasPriceSelection()
+ assert.equal(dispatchProps.setGasPrice.callCount, 1)
+
+ assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0)
+ result.someOtherDispatchProp()
+ assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1)
+ })
+ })
+
+})
diff --git a/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
new file mode 100644
index 000000000..bd3c9a257
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js
@@ -0,0 +1,62 @@
+import assert from 'assert'
+import {
+ gasFeeIsInError,
+ getGasLoadingError,
+ getGasButtonGroupShown,
+} 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)
+ })
+ })
+
+ describe('getGasButtonGroupShown()', () => {
+ it('should return send.gasButtonGroupShown', () => {
+ const state = {
+ send: {
+ gasButtonGroupShown: 'foobar',
+ },
+ }
+
+ assert.equal(getGasButtonGroupShown(state), 'foobar')
+ })
+ })
+
+})
diff --git a/ui/app/pages/send/send-content/send-hex-data-row/index.js b/ui/app/pages/send/send-content/send-hex-data-row/index.js
new file mode 100644
index 000000000..08c341067
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-hex-data-row/index.js
@@ -0,0 +1 @@
+export { default } from './send-hex-data-row.container'
diff --git a/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js
new file mode 100644
index 000000000..62a74a77b
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js
@@ -0,0 +1,42 @@
+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,
+ updateSendHexData: PropTypes.func.isRequired,
+ updateGas: PropTypes.func.isRequired,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ onInput = (event) => {
+ const {updateSendHexData, updateGas} = this.props
+ const data = event.target.value.replace(/\n/g, '') || null
+ updateSendHexData(data)
+ updateGas({ data })
+ }
+
+ render () {
+ const {inError} = this.props
+ const {t} = this.context
+
+ return (
+ <SendRowWrapper
+ label={`${t('hexData')}:`}
+ showError={inError}
+ errorType={'amount'}
+ >
+ <textarea
+ onInput={this.onInput}
+ placeholder="Optional"
+ className="send-v2__hex-data__input"
+ />
+ </SendRowWrapper>
+ )
+ }
+}
diff --git a/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js b/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js
new file mode 100644
index 000000000..8b1c540c3
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js
@@ -0,0 +1,21 @@
+import { connect } from 'react-redux'
+import {
+ updateSendHexData,
+} from '../../../../store/actions'
+import SendHexDataRow from './send-hex-data-row.component'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendHexDataRow)
+
+function mapStateToProps (state) {
+ return {
+ data: state.metamask.send.data,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ updateSendHexData (data) {
+ return dispatch(updateSendHexData(data))
+ },
+ }
+}
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/index.js b/ui/app/pages/send/send-content/send-row-wrapper/index.js
new file mode 100644
index 000000000..d17545dcc
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/index.js
@@ -0,0 +1 @@
+export { default } from './send-row-wrapper.component'
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/index.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/index.js
new file mode 100644
index 000000000..c00617f83
--- /dev/null
+++ b/ui/app/pages/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/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js
new file mode 100644
index 000000000..61bc7bab7
--- /dev/null
+++ b/ui/app/pages/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
+ ? <div className="send-v2__error">{this.context.t(errorMessage)}</div>
+ : null
+ )
+ }
+
+}
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js
new file mode 100644
index 000000000..59622047f
--- /dev/null
+++ b/ui/app/pages/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/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js
new file mode 100644
index 000000000..2304a43d2
--- /dev/null
+++ b/ui/app/pages/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(<SendRowErrorMessage
+ errors={{ error1: 'abc', error2: 'def' }}
+ errorType={'error3'}
+ />, { 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/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js
new file mode 100644
index 000000000..eecff165d
--- /dev/null
+++ b/ui/app/pages/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/pages/send/send-content/send-row-wrapper/send-row-warning-message/index.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/index.js
new file mode 100644
index 000000000..fd4d19ef7
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/index.js
@@ -0,0 +1 @@
+export { default } from './send-row-warning-message.container'
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js
new file mode 100644
index 000000000..f1caa8f99
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js
@@ -0,0 +1,27 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class SendRowWarningMessage extends Component {
+
+ static propTypes = {
+ warnings: PropTypes.object,
+ warningType: PropTypes.string,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ render () {
+ const { warnings, warningType } = this.props
+
+ const warningMessage = warningType in warnings && warnings[warningType]
+
+ return (
+ warningMessage
+ ? <div className="send-v2__warning">{this.context.t(warningMessage)}</div>
+ : null
+ )
+ }
+
+}
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js
new file mode 100644
index 000000000..7df14fd96
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import { getSendWarnings } from '../../../send.selectors'
+import SendRowWarningMessage from './send-row-warning-message.component'
+
+export default connect(mapStateToProps)(SendRowWarningMessage)
+
+function mapStateToProps (state, ownProps) {
+ return {
+ warnings: getSendWarnings(state),
+ warningType: ownProps.warningType,
+ }
+}
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js
new file mode 100644
index 000000000..bd803d833
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import SendRowWarningMessage from '../send-row-warning-message.component.js'
+
+describe('SendRowWarningMessage Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<SendRowWarningMessage
+ warnings={{ warning1: 'abc', warning2: 'def' }}
+ warningType={'warning3'}
+ />, { context: { t: str => str + '_t' } })
+ })
+
+ describe('render', () => {
+ it('should render null if the passed warnings do not contain a warning of warningType', () => {
+ assert.equal(wrapper.find('.send-v2__warning').length, 0)
+ assert.equal(wrapper.html(), null)
+ })
+
+ it('should render a warning message if the passed warnings contain a warning of warningType', () => {
+ wrapper.setProps({ warnings: { warning1: 'abc', warning2: 'def', warning3: 'xyz' } })
+ assert.equal(wrapper.find('.send-v2__warning').length, 1)
+ assert.equal(wrapper.find('.send-v2__warning').text(), 'xyz_t')
+ })
+ })
+})
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js
new file mode 100644
index 000000000..225bf056c
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js
@@ -0,0 +1,28 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps
+
+proxyquire('../send-row-warning-message.container.js', {
+ 'react-redux': {
+ connect: (ms, md) => {
+ mapStateToProps = ms
+ return () => ({})
+ },
+ },
+ '../../../send.selectors': { getSendWarnings: (s) => `mockWarnings:${s}` },
+})
+
+describe('send-row-warning-message container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState', { warningType: 'someType' }), {
+ warnings: 'mockWarnings:mockState',
+ warningType: 'someType' })
+ })
+
+ })
+
+})
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper-README.md b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper-README.md
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper-README.md
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.component.js
new file mode 100644
index 000000000..94309bd96
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.component.js
@@ -0,0 +1,48 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowErrorMessage from './send-row-error-message'
+import SendRowWarningMessage from './send-row-warning-message'
+
+export default class SendRowWrapper extends Component {
+
+ static propTypes = {
+ children: PropTypes.node,
+ errorType: PropTypes.string,
+ label: PropTypes.string,
+ showError: PropTypes.bool,
+ showWarning: PropTypes.bool,
+ warningType: PropTypes.string,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ render () {
+ const {
+ children,
+ errorType = '',
+ label,
+ showError = false,
+ showWarning = false,
+ warningType = '',
+ } = this.props
+ const formField = Array.isArray(children) ? children[1] || children[0] : children
+ const customLabelContent = children.length > 1 ? children[0] : null
+
+ return (
+ <div className="send-v2__form-row">
+ <div className="send-v2__form-label">
+ {label}
+ {showError && <SendRowErrorMessage errorType={errorType}/>}
+ {!showError && showWarning && <SendRowWarningMessage warningType={warningType} />}
+ {customLabelContent}
+ </div>
+ <div className="send-v2__form-field">
+ {formField}
+ </div>
+ </div>
+ )
+ }
+
+}
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.scss b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.scss
diff --git a/ui/app/pages/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js b/ui/app/pages/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js
new file mode 100644
index 000000000..30280e1d0
--- /dev/null
+++ b/ui/app/pages/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(<SendRowWrapper
+ errorType={'mockErrorType'}
+ label={'mockLabel'}
+ showError={false}
+ >
+ <span>Mock Form Field</span>
+ </SendRowWrapper>)
+ })
+
+ 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(<SendRowWrapper
+ errorType={'mockErrorType'}
+ label={'mockLabel'}
+ showError={false}
+ >
+ <span>Mock Custom Label Content</span>
+ <span>Mock Form Field</span>
+ </SendRowWrapper>)
+ 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(<SendRowWrapper
+ errorType={'mockErrorType'}
+ label={'mockLabel'}
+ showError={false}
+ >
+ <span>Mock Custom Label Content</span>
+ <span>Mock Form Field</span>
+ </SendRowWrapper>)
+ assert.equal(wrapper.find('.send-v2__form-row > .send-v2__form-label').childAt(1).text(), 'Mock Custom Label Content')
+ })
+ })
+})
diff --git a/ui/app/pages/send/send-content/send-to-row/index.js b/ui/app/pages/send/send-content/send-to-row/index.js
new file mode 100644
index 000000000..121f15148
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-to-row/index.js
@@ -0,0 +1 @@
+export { default } from './send-to-row.container'
diff --git a/ui/app/pages/send/send-content/send-to-row/send-to-row-README.md b/ui/app/pages/send/send-content/send-to-row/send-to-row-README.md
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-to-row/send-to-row-README.md
diff --git a/ui/app/pages/send/send-content/send-to-row/send-to-row.component.js b/ui/app/pages/send/send-content/send-to-row/send-to-row.component.js
new file mode 100644
index 000000000..9baf327c1
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-to-row/send-to-row.component.js
@@ -0,0 +1,91 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import SendRowWrapper from '../send-row-wrapper'
+import EnsInput from '../../../../components/app/ens-input'
+import { getToErrorObject, getToWarningObject } from './send-to-row.utils.js'
+
+export default class SendToRow extends Component {
+
+ static propTypes = {
+ closeToDropdown: PropTypes.func,
+ hasHexData: PropTypes.bool.isRequired,
+ inError: PropTypes.bool,
+ inWarning: PropTypes.bool,
+ network: PropTypes.string,
+ openToDropdown: PropTypes.func,
+ selectedToken: PropTypes.object,
+ to: PropTypes.string,
+ toAccounts: PropTypes.array,
+ toDropdownOpen: PropTypes.bool,
+ tokens: PropTypes.array,
+ updateGas: PropTypes.func,
+ updateSendTo: PropTypes.func,
+ updateSendToError: PropTypes.func,
+ updateSendToWarning: PropTypes.func,
+ scanQrCode: PropTypes.func,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ handleToChange (to, nickname = '', toError, toWarning, network) {
+ const { hasHexData, updateSendTo, updateSendToError, updateGas, tokens, selectedToken, updateSendToWarning } = this.props
+ const toErrorObject = getToErrorObject(to, toError, hasHexData, tokens, selectedToken, network)
+ const toWarningObject = getToWarningObject(to, toWarning, tokens, selectedToken)
+ updateSendTo(to, nickname)
+ updateSendToError(toErrorObject)
+ updateSendToWarning(toWarningObject)
+ if (toErrorObject.to === null) {
+ updateGas({ to })
+ }
+ }
+
+ render () {
+ const {
+ closeToDropdown,
+ inError,
+ inWarning,
+ network,
+ openToDropdown,
+ to,
+ toAccounts,
+ toDropdownOpen,
+ } = this.props
+
+ return (
+ <SendRowWrapper
+ errorType={'to'}
+ label={`${this.context.t('to')}: `}
+ showError={inError}
+ showWarning={inWarning}
+ warningType={'to'}
+ >
+ <EnsInput
+ scanQrCode={_ => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Edit Screen',
+ name: 'Used QR scanner',
+ },
+ })
+ this.props.scanQrCode()
+ }}
+ accounts={toAccounts}
+ closeDropdown={() => closeToDropdown()}
+ dropdownOpen={toDropdownOpen}
+ inError={inError}
+ name={'address'}
+ network={network}
+ onChange={({ toAddress, nickname, toError, toWarning }) => this.handleToChange(toAddress, nickname, toError, toWarning, this.props.network)}
+ openDropdown={() => openToDropdown()}
+ placeholder={this.context.t('recipientAddress')}
+ to={to}
+ />
+ </SendRowWrapper>
+ )
+ }
+
+}
diff --git a/ui/app/pages/send/send-content/send-to-row/send-to-row.container.js b/ui/app/pages/send/send-content/send-to-row/send-to-row.container.js
new file mode 100644
index 000000000..2cbe9fcd0
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-to-row/send-to-row.container.js
@@ -0,0 +1,54 @@
+import { connect } from 'react-redux'
+import {
+ getCurrentNetwork,
+ getSelectedToken,
+ getSendTo,
+ getSendToAccounts,
+ getSendHexData,
+} from '../../send.selectors.js'
+import {
+ getToDropdownOpen,
+ getTokens,
+ sendToIsInError,
+ sendToIsInWarning,
+} from './send-to-row.selectors.js'
+import {
+ updateSendTo,
+} from '../../../../store/actions'
+import {
+ updateSendErrors,
+ updateSendWarnings,
+ openToDropdown,
+ closeToDropdown,
+} from '../../../../ducks/send/send.duck'
+import SendToRow from './send-to-row.component'
+
+export default connect(mapStateToProps, mapDispatchToProps)(SendToRow)
+
+function mapStateToProps (state) {
+ return {
+ hasHexData: Boolean(getSendHexData(state)),
+ inError: sendToIsInError(state),
+ inWarning: sendToIsInWarning(state),
+ network: getCurrentNetwork(state),
+ selectedToken: getSelectedToken(state),
+ to: getSendTo(state),
+ toAccounts: getSendToAccounts(state),
+ toDropdownOpen: getToDropdownOpen(state),
+ tokens: getTokens(state),
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ closeToDropdown: () => dispatch(closeToDropdown()),
+ openToDropdown: () => dispatch(openToDropdown()),
+ updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
+ updateSendToError: (toErrorObject) => {
+ dispatch(updateSendErrors(toErrorObject))
+ },
+ updateSendToWarning: (toWarningObject) => {
+ dispatch(updateSendWarnings(toWarningObject))
+ },
+ }
+}
diff --git a/ui/app/pages/send/send-content/send-to-row/send-to-row.selectors.js b/ui/app/pages/send/send-content/send-to-row/send-to-row.selectors.js
new file mode 100644
index 000000000..a6160d335
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-to-row/send-to-row.selectors.js
@@ -0,0 +1,24 @@
+const selectors = {
+ getToDropdownOpen,
+ getTokens,
+ sendToIsInError,
+ sendToIsInWarning,
+}
+
+module.exports = selectors
+
+function getToDropdownOpen (state) {
+ return state.send.toDropdownOpen
+}
+
+function sendToIsInError (state) {
+ return Boolean(state.send.errors.to)
+}
+
+function sendToIsInWarning (state) {
+ return Boolean(state.send.warnings.to)
+}
+
+function getTokens (state) {
+ return state.metamask.tokens
+}
diff --git a/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js b/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js
new file mode 100644
index 000000000..d0a43f086
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js
@@ -0,0 +1,36 @@
+const {
+ REQUIRED_ERROR,
+ INVALID_RECIPIENT_ADDRESS_ERROR,
+ KNOWN_RECIPIENT_ADDRESS_ERROR,
+ INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR,
+} = require('../../send.constants')
+const { isValidAddress, isEthNetwork } = require('../../../../helpers/utils/util')
+import { checkExistingAddresses } from '../../../add-token/util'
+
+const ethUtil = require('ethereumjs-util')
+const contractMap = require('eth-contract-metadata')
+
+function getToErrorObject (to, toError = null, hasHexData = false, tokens = [], selectedToken = null, network) {
+ if (!to) {
+ if (!hasHexData) {
+ toError = REQUIRED_ERROR
+ }
+ } else if (!isValidAddress(to, network) && !toError) {
+ toError = isEthNetwork(network) ? INVALID_RECIPIENT_ADDRESS_ERROR : INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR
+ } else if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) {
+ toError = KNOWN_RECIPIENT_ADDRESS_ERROR
+ }
+ return { to: toError }
+}
+
+function getToWarningObject (to, toWarning = null, tokens = [], selectedToken = null) {
+ if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) {
+ toWarning = KNOWN_RECIPIENT_ADDRESS_ERROR
+ }
+ return { to: toWarning }
+}
+
+module.exports = {
+ getToErrorObject,
+ getToWarningObject,
+}
diff --git a/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-component.test.js
new file mode 100644
index 000000000..c180d97f1
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-component.test.js
@@ -0,0 +1,166 @@
+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}`,
+ }),
+ getToWarningObject: (to, toWarning) => ({
+ to: to === false ? null : `mockToWarningObject:${to}${toWarning}`,
+ }),
+ },
+}).default
+
+import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
+import EnsInput from '../../../../../components/app/ens-input'
+
+const propsMethodSpies = {
+ closeToDropdown: sinon.spy(),
+ openToDropdown: sinon.spy(),
+ updateGas: sinon.spy(),
+ updateSendTo: sinon.spy(),
+ updateSendToError: sinon.spy(),
+ updateSendToWarning: sinon.spy(),
+}
+
+sinon.spy(SendToRow.prototype, 'handleToChange')
+
+describe('SendToRow Component', function () {
+ let wrapper
+ let instance
+
+ beforeEach(() => {
+ wrapper = shallow(<SendToRow
+ closeToDropdown={propsMethodSpies.closeToDropdown}
+ inError={false}
+ inWarning={false}
+ network={'mockNetwork'}
+ openToDropdown={propsMethodSpies.openToDropdown}
+ to={'mockTo'}
+ toAccounts={['mockAccount']}
+ toDropdownOpen={false}
+ updateGas={propsMethodSpies.updateGas}
+ updateSendTo={propsMethodSpies.updateSendTo}
+ updateSendToError={propsMethodSpies.updateSendToError}
+ updateSendToWarning={propsMethodSpies.updateSendToWarning}
+ />, { context: { t: str => str + '_t' } })
+ instance = wrapper.instance()
+ })
+
+ afterEach(() => {
+ propsMethodSpies.closeToDropdown.resetHistory()
+ propsMethodSpies.openToDropdown.resetHistory()
+ propsMethodSpies.updateSendTo.resetHistory()
+ propsMethodSpies.updateSendToError.resetHistory()
+ propsMethodSpies.updateSendToWarning.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 call updateSendToWarning', () => {
+ assert.equal(propsMethodSpies.updateSendToWarning.callCount, 0)
+ instance.handleToChange('mockTo2', '', '', 'mockToWarning')
+ assert.equal(propsMethodSpies.updateSendToWarning.callCount, 1)
+ assert.deepEqual(
+ propsMethodSpies.updateSendToWarning.getCall(0).args,
+ [{ to: 'mockToWarningObject:mockTo2mockToWarning' }]
+ )
+ })
+
+ 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', toWarning: 'mockToWarning' })
+ assert.equal(SendToRow.prototype.handleToChange.callCount, 1)
+ assert.deepEqual(
+ SendToRow.prototype.handleToChange.getCall(0).args,
+ ['mockNewTo', 'mockNewNickname', 'mockToError', 'mockToWarning', 'mockNetwork' ]
+ )
+ })
+ })
+})
diff --git a/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-container.test.js b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-container.test.js
new file mode 100644
index 000000000..bb8702e9a
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-container.test.js
@@ -0,0 +1,134 @@
+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(),
+ updateSendWarnings: 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}`,
+ getSelectedToken: (s) => `mockSelectedToken:${s}`,
+ getSendHexData: (s) => s,
+ getSendTo: (s) => `mockTo:${s}`,
+ getSendToAccounts: (s) => `mockToAccounts:${s}`,
+ },
+ './send-to-row.selectors.js': {
+ getToDropdownOpen: (s) => `mockToDropdownOpen:${s}`,
+ sendToIsInError: (s) => `mockInError:${s}`,
+ sendToIsInWarning: (s) => `mockInWarning:${s}`,
+ getTokens: (s) => `mockTokens:${s}`,
+ },
+ '../../../../store/actions': actionSpies,
+ '../../../../ducks/send/send.duck': duckActionSpies,
+})
+
+describe('send-to-row container', () => {
+
+ describe('mapStateToProps()', () => {
+
+ it('should map the correct properties to props', () => {
+ assert.deepEqual(mapStateToProps('mockState'), {
+ hasHexData: true,
+ inError: 'mockInError:mockState',
+ inWarning: 'mockInWarning:mockState',
+ network: 'mockNetwork:mockState',
+ selectedToken: 'mockSelectedToken:mockState',
+ to: 'mockTo:mockState',
+ toAccounts: 'mockToAccounts:mockState',
+ toDropdownOpen: 'mockToDropdownOpen:mockState',
+ tokens: 'mockTokens: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'
+ )
+ })
+ })
+
+ describe('updateSendToWarning()', () => {
+ it('should dispatch an action', () => {
+ mapDispatchToPropsObject.updateSendToWarning('mockToWarningObject')
+ assert(dispatchSpy.calledOnce)
+ assert(duckActionSpies.updateSendWarnings.calledOnce)
+ assert.equal(
+ duckActionSpies.updateSendWarnings.getCall(0).args[0],
+ 'mockToWarningObject'
+ )
+ })
+ })
+
+ })
+
+})
diff --git a/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-selectors.test.js b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
new file mode 100644
index 000000000..0fa342d1e
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-selectors.test.js
@@ -0,0 +1,59 @@
+import assert from 'assert'
+import {
+ getToDropdownOpen,
+ getTokens,
+ 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)
+ })
+ })
+
+ describe('getTokens()', () => {
+ it('should return empty array if no tokens in state', () => {
+ const state = {
+ metamask: {
+ tokens: [],
+ },
+ }
+
+ assert.deepStrictEqual(getTokens(state), [])
+ })
+ })
+})
diff --git a/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js
new file mode 100644
index 000000000..f29f5efec
--- /dev/null
+++ b/ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js
@@ -0,0 +1,107 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+import sinon from 'sinon'
+
+import {
+ REQUIRED_ERROR,
+ INVALID_RECIPIENT_ADDRESS_ERROR,
+ KNOWN_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', {
+ '../../../../helpers/utils/util': {
+ isValidAddress: stubs.isValidAddress,
+ },
+})
+const {
+ getToErrorObject,
+ getToWarningObject,
+} = 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 null if to is falsy and hexData is truthy', () => {
+ assert.deepEqual(getToErrorObject(null, undefined, true), {
+ to: null,
+ })
+ })
+
+ 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',
+ })
+ })
+
+ it('should return a known address recipient if to is truthy but part of state tokens', () => {
+ assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+
+ it('should null if to is truthy part of tokens but selectedToken falsy', () => {
+ assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}]), {
+ to: null,
+ })
+ })
+
+ it('should return a known address recipient if to is truthy but part of contract metadata', () => {
+ assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+ it('should null if to is truthy part of contract metadata but selectedToken falsy', () => {
+ assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+ })
+
+ describe('getToWarningObject()', () => {
+ it('should return a known address recipient if to is truthy but part of state tokens', () => {
+ assert.deepEqual(getToWarningObject('0xabc123', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+
+ it('should null if to is truthy part of tokens but selectedToken falsy', () => {
+ assert.deepEqual(getToWarningObject('0xabc123', undefined, [{'address': '0xabc123'}]), {
+ to: null,
+ })
+ })
+
+ it('should return a known address recipient if to is truthy but part of contract metadata', () => {
+ assert.deepEqual(getToWarningObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+ it('should null if to is truthy part of contract metadata but selectedToken falsy', () => {
+ assert.deepEqual(getToWarningObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), {
+ to: KNOWN_RECIPIENT_ADDRESS_ERROR,
+ })
+ })
+ })
+
+})
diff --git a/ui/app/pages/send/send-content/tests/send-content-component.test.js b/ui/app/pages/send/send-content/tests/send-content-component.test.js
new file mode 100644
index 000000000..521c6523e
--- /dev/null
+++ b/ui/app/pages/send/send-content/tests/send-content-component.test.js
@@ -0,0 +1,53 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import SendContent from '../send-content.component.js'
+
+import PageContainerContent from '../../../../components/ui/page-container/page-container-content.component'
+import SendAmountRow from '../send-amount-row/send-amount-row.container'
+import SendFromRow from '../send-from-row/send-from-row.container'
+import SendGasRow from '../send-gas-row/send-gas-row.container'
+import SendToRow from '../send-to-row/send-to-row.container'
+import SendHexDataRow from '../send-hex-data-row/send-hex-data-row.container'
+import SendAssetRow from '../send-asset-row/send-asset-row.container'
+
+describe('SendContent Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<SendContent showHexData={true} />)
+ })
+
+ 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(SendAssetRow))
+ assert(PageContainerContentChild.childAt(3).is(SendAmountRow))
+ assert(PageContainerContentChild.childAt(4).is(SendGasRow))
+ assert(PageContainerContentChild.childAt(5).is(SendHexDataRow))
+ })
+
+ it('should not render the SendHexDataRow if props.showHexData is false', () => {
+ wrapper.setProps({ showHexData: false })
+ const PageContainerContentChild = wrapper.find(PageContainerContent).children()
+ assert(PageContainerContentChild.childAt(0).is(SendFromRow))
+ assert(PageContainerContentChild.childAt(1).is(SendToRow))
+ assert(PageContainerContentChild.childAt(2).is(SendAssetRow))
+ assert(PageContainerContentChild.childAt(3).is(SendAmountRow))
+ assert(PageContainerContentChild.childAt(4).is(SendGasRow))
+ assert.equal(PageContainerContentChild.childAt(5).exists(), false)
+ })
+ })
+})