diff options
93 files changed, 1256 insertions, 18 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js index 81d9c333b..674669eed 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -168,6 +168,8 @@ var actions = { UPDATE_MAX_MODE: 'UPDATE_MAX_MODE', UPDATE_SEND: 'UPDATE_SEND', CLEAR_SEND: 'CLEAR_SEND', + OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN', + CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN', updateGasLimit, updateGasPrice, updateGasTotal, diff --git a/ui/app/components/page-container/page-container-content.component.js b/ui/app/components/page-container/page-container-content.component.js new file mode 100644 index 000000000..ffd62894c --- /dev/null +++ b/ui/app/components/page-container/page-container-content.component.js @@ -0,0 +1,18 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class PageContainerContent extends Component { + + static propTypes = { + children: PropTypes.node.isRequired, + }; + + render () { + return ( + <div className="page-container__content"> + {this.props.children} + </div> + ); + } + +} diff --git a/ui/app/components/page-container/page-container-footer.component.js b/ui/app/components/page-container/page-container-footer.component.js new file mode 100644 index 000000000..0ef14c9d7 --- /dev/null +++ b/ui/app/components/page-container/page-container-footer.component.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class PageContainerFooter extends Component { + + static propTypes = { + onCancel: PropTypes.func, + onSubmit: PropTypes.func, + disabled: PropTypes.bool, + }; + + render () { + const { onCancel, onSubmit, disabled } = this.props + + return ( + <div className="page-container__footer"> + + <button + className="btn-secondary--lg page-container__footer-button" + onClick={() => onCancel()} + > + {this.context.t('cancel')} + </button> + + <button + className="btn-primary--lg page-container__footer-button" + disabled={disabled} + onClick={(e) => onSubmit(e)} + > + {this.context.t('next')} + </button> + + </div> + ); + } + +} + +PageContainerFooter.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/components/page-container/page-container-header.component.js b/ui/app/components/page-container/page-container-header.component.js new file mode 100644 index 000000000..9adc88fb3 --- /dev/null +++ b/ui/app/components/page-container/page-container-header.component.js @@ -0,0 +1,35 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class PageContainerHeader extends Component { + + static propTypes = { + title: PropTypes.string, + subtitle: PropTypes.string, + onClose: PropTypes.func, + }; + + render () { + const { title, subtitle, onClose } = this.props + + return ( + <div className="page-container__header"> + + <div className="page-container__title"> + {title} + </div> + + <div className="page-container__subtitle"> + {subtitle} + </div> + + <div + className="page-container__header-close" + onClick={() => onClose()} + /> + + </div> + ); + } + +} diff --git a/ui/app/components/page-container/page-container.component.js b/ui/app/components/page-container/page-container.component.js new file mode 100644 index 000000000..7df1d48d8 --- /dev/null +++ b/ui/app/components/page-container/page-container.component.js @@ -0,0 +1,18 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class PageContainer extends Component { + + static propTypes = { + children: PropTypes.node.isRequired, + }; + + render () { + return ( + <div className="page-container"> + {this.props.children} + </div> + ); + } + +} diff --git a/ui/app/components/page-container/tests/page-container-content-component.test.js b/ui/app/components/page-container/tests/page-container-content-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/page-container/tests/page-container-content-component.test.js diff --git a/ui/app/components/page-container/tests/page-container-footer-component.test.js b/ui/app/components/page-container/tests/page-container-footer-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/page-container/tests/page-container-footer-component.test.js diff --git a/ui/app/components/page-container/tests/page-container-header-component.test.js b/ui/app/components/page-container/tests/page-container-header-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/page-container/tests/page-container-header-component.test.js diff --git a/ui/app/components/send_/README.md b/ui/app/components/send_/README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/README.md diff --git a/ui/app/components/send_/send-content/send-amount-row/README.md b/ui/app/components/send_/send-content/send-amount-row/README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/README.md diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js new file mode 100644 index 000000000..59a1fd6db --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js @@ -0,0 +1,54 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class AmountMaxButton extends Component { + + static propTypes = { + tokenBalance: PropTypes.string, + gasTotal: PropTypes.string, + balance: PropTypes.string, + selectedToken: PropTypes.object, + setAmountToMax: PropTypes.func, + setMaxModeTo: PropTypes.func, + maxModeOn: PropTypes.bool, + }; + + setAmountToMax = function () { + const { + balance, + tokenBalance, + selectedToken, + gasTotal, + setAmountToMax, + } = this.props + + setAmountToMax({ + tokenBalance, + selectedToken, + gasTotal, + setAmountToMax, + }) + } + + render () { + const { setMaxModeTo } = this.props + + return ( + <div + className='send-v2__amount-max' + onClick={(event) => { + event.preventDefault() + setMaxModeTo(true) + this.setAmountToMax() + }} + > + {!maxModeOn ? this.context.t('max') : '' ])} + </div> + ); + } + +} + +AmountMaxButton.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js new file mode 100644 index 000000000..572e1fc46 --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js @@ -0,0 +1,36 @@ +import { + getSelectedToken, + getGasTotal, + getTokenBalance, + getSendFromBalance, +} from '../../../send.selectors.js' +import { getMaxModeOn } from '../send-amount-row.selectors.js' +import { calcMaxAmount } from './amount-max-button.utils.js' +import { + updateSendAmount, + setMaxModeTo, +} from '../../../actions' +import AmountMaxButton from './amount-max-button.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendToRow) + +function mapStateToProps (state) { + + return { + selectedToken: getSelectedToken(state), + maxModeOn: getMaxModeOn(state), + gasTotal: getGasTotal(state), + tokenBalance: getTokenBalance(state), + balance: getSendFromBalance(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + setAmountToMax: maxAmountDataObject => { + updateSendErrors({ amount: null }) + updateSendAmount(calcMaxAmount(maxAmountDataObject)) + } + setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), + } +}
\ No newline at end of file diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js new file mode 100644 index 000000000..54aacc8d7 --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js @@ -0,0 +1,22 @@ +const { + multiplyCurrencies, + subtractCurrencies, +} = require('../../../../conversion-util') +const ethUtil = require('ethereumjs-util') + +function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) { + const { decimals } = selectedToken || {} + const multiplier = Math.pow(10, Number(decimals || 0)) + + return selectedToken + ? multiplyCurrencies(tokenBalance, multiplier, {toNumericBase: 'hex'}) + : subtractCurrencies( + ethUtil.addHexPrefix(balance), + ethUtil.addHexPrefix(gasTotal), + { toNumericBase: 'hex' } + ) +} + +module.exports = { + calcMaxAmount +} diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js new file mode 100644 index 000000000..78038f714 --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js @@ -0,0 +1,91 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component' +import AmountMaxButton from '../amount-max-button/amount-max-button.component' +import CurrencyDisplay from '../../../send/currency-display' + +export default class SendAmountRow extends Component { + + static propTypes = { + amountConversionRate: PropTypes.string, + conversionRate: PropTypes.string, + from: PropTypes.object, + gasTotal: PropTypes.string, + primaryCurrency: PropTypes.string, + selectedToken: PropTypes.object, + tokenBalance: PropTypes.string, + updateSendAmountError: PropTypes.func, + updateSendAmount: PropTypes.func, + setMaxModeTo: PropTypes.func + } + + validateAmount (amount) { + const { + amountConversionRate, + conversionRate, + from: { balance }, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + updateSendAmountError, + } = this.props + + updateSendAmountError({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + }) + } + + handleAmountChange (amount) { + const { updateSendAmount, setMaxModeTo } = this.props + + setMaxModeTo(false) + this.validateAmount(amount) + updateSendAmount(amount) + } + + render () { + const { + amount, + amountConversionRate, + convertedCurrency, + inError, + gasTotal, + maxModeOn, + primaryCurrency = 'ETH', + selectedToken, + } = this.props + + return ( + <SendRowWrapper + label={`${this.context.t('amount')}:`} + showError={inError} + errorType={'amount'} + > + !inError && gasTotal && <AmountMaxButton /> + <CurrencyDisplay + inError={inError}, + primaryCurrency={primaryCurrency}, + convertedCurrency={convertedCurrency}, + selectedToken={selectedToken}, + value={amount || '0x0'}, + conversionRate={amountConversionRate}, + handleChange={this.handleAmountChange}, + > + </SendRowWrapper> + ); + } + +} + +SendAmountRow.contextTypes = { + t: PropTypes.func, +} + diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js new file mode 100644 index 000000000..098855a02 --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js @@ -0,0 +1,49 @@ +import { + getSelectedToken, + getPrimaryCurrency, + getAmountConversionRate, + getConvertedCurrency, + getSendAmount, + getGasTotal, + getSelectedBalance, + getTokenBalance, + getSendFromBalance, +} from '../../send.selectors.js' +import { + getMaxModeOn, + sendAmountIsInError, +} from './send-amount-row.selectors.js' +import { getAmountErrorObject } from './send-amount-row.utils.js' +import { + updateSendAmount, + setMaxModeTo, +} from '../../../actions' +import SendAmountRow from './send-amount-row.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendToRow) + +function mapStateToProps (state) { +updateSendTo +return { + selectedToken: getSelectedToken(state), + primaryCurrency: getPrimaryCurrency(state), + convertedCurrency: getConvertedCurrency(state), + amountConversionRate: getAmountConversionRate(state), + inError: sendAmountIsInError(state), + amount: getSendAmount(state), + maxModeOn: getMaxModeOn(state), + gasTotal: getGasTotal(state), + tokenBalance: getTokenBalance(state), + balance: getSendFromBalance(state), +} +} + +function mapDispatchToProps (dispatch) { + return { + updateSendAmountError: (amountDataObject) => { + dispatch(updateSendErrors(getAmountErrorObject(amountDataObject))) + }, + updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)), + setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), + } +}
\ No newline at end of file diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js new file mode 100644 index 000000000..724f345af --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js @@ -0,0 +1,14 @@ +const selectors = { + getMaxModeOn, + sendAmountIsInError, +} + +module.exports = selectors + +function getMaxModeOn (state) { + return state.metamask.send.maxModeOn +} + +function sendAmountIsInError (state) { + return Boolean(state.metamask.send.errors.amount) +} diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.utils.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.utils.js new file mode 100644 index 000000000..5b01b4594 --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.utils.js @@ -0,0 +1,55 @@ +const { isValidAddress } = require('../../../../util') + +function getAmountErrorObject ({ + amount, + balance, + amountConversionRate, + conversionRate, + primaryCurrency, + selectedToken, + gasTotal, + tokenBalance, +}) { + let insufficientFunds = false + if (gasTotal && conversionRate) { + insufficientFunds = !isBalanceSufficient({ + amount: selectedToken ? '0x0' : amount, + gasTotal, + balance, + primaryCurrency, + amountConversionRate, + conversionRate, + }) + } + + let inSufficientTokens = false + if (selectedToken && tokenBalance !== null) { + const { decimals } = selectedToken + inSufficientTokens = !isTokenBalanceSufficient({ + tokenBalance, + amount, + decimals, + }) + } + + const amountLessThanZero = conversionGreaterThan( + { value: 0, fromNumericBase: 'dec' }, + { value: amount, fromNumericBase: 'hex' }, + ) + + let amountError = null + + if (insufficientFunds) { + amountError = this.context.t('insufficientFunds') + } else if (insufficientTokens) { + amountError = this.context.t('insufficientTokens') + } else if (amountLessThanZero) { + amountError = this.context.t('negativeETH') + } + + return { amount: amountError } +} + +module.exports = { + getAmountErrorObject +} diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-selectors.test.js diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-utils.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-utils.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-utils.test.js diff --git a/ui/app/components/send_/send-content/send-content-README.md b/ui/app/components/send_/send-content/send-content-README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-content-README.md diff --git a/ui/app/components/send_/send-content/send-content.component.js b/ui/app/components/send_/send-content/send-content.component.js new file mode 100644 index 000000000..ad6b4a982 --- /dev/null +++ b/ui/app/components/send_/send-content/send-content.component.js @@ -0,0 +1,23 @@ +import React, { Component } from 'react' +import PageContainerContent from '../../page-container/page-container-header.component' +import SendFromRow from './send-from-row/send-from-row.component' +import SendToRow from './send-to-row/send-to-row.component' +import SendAmountRow from './send-amount-row/send-amount-row.component' +import SendGasRow from './send-gas-row/send-gas-row.component' + +export default class SendContent extends Component { + + render () { + return ( + <PageContainerContent> + <div className='.send-v2__form'> + <SendFromRow /> + <SendToRow /> + <SendAmountRow /> + <SendGasRow /> + </div> + </PageContainerContent> + ); + } + +} diff --git a/ui/app/components/send_/send-content/send-content.scss b/ui/app/components/send_/send-content/send-content.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-content.scss diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js b/ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row-README.md b/ui/app/components/send_/send-content/send-from-row/send-from-row-README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/send-from-row-README.md diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js new file mode 100644 index 000000000..b17f749a6 --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component' +import FromDropdown from '../../../send/from-dropdown' + +export default class SendFromRow extends Component { + + static propTypes = { + closeFromDropdown: PropTypes.func, + conversionRate: PropTypes.string, + from: PropTypes.string, + fromAccounts: PropTypes.array, + fromDropdownOpen: PropTypes.bool, + openFromDropdown: PropTypes.func, + tokenContract: PropTypes.object, + updateSendFrom: PropTypes.func, + updateSendTokenBalance: PropTypes.func, + }; + + async handleFromChange (newFrom) { + const { + updateSendFrom, + tokenContract, + updateSendTokenBalance, + } = this.props + + if (tokenContract) { + const usersToken = await tokenContract.balanceOf(newFrom.address) + updateSendTokenBalance(usersToken) + } + updateSendFrom(newFrom) + } + + render () { + const { + from, + fromAccounts, + conversionRate, + fromDropdownOpen, + tokenContract, + openFromDropdown, + closeFromDropdown, + } = this.props + + return ( + <SendRowWrapper label={`${this.context.t('from')}:`}> + <FromDropdown + dropdownOpen={fromDropdownOpen} + accounts={fromAccounts} + selectedAccount={from} + onSelect={newFrom => this.handleFromChange(newFrom)} + openDropdown={() => openFromDropdown()} + closeDropdown={() => closeFromDropdown()} + conversionRate={conversionRate} + /> + </SendRowWrapper> + ); + } + +} + +SendFromRow.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js new file mode 100644 index 000000000..eeeb51629 --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js @@ -0,0 +1,44 @@ +import { + getSendFrom, + getConversionRate, + getSelectedTokenContract, + getCurrentAccountWithSendEtherInfo, + accountsWithSendEtherInfoSelector, +} from '../../send.selectors.js' +import { getFromDropdownOpen } from './send-from-row.selectors.js' +import { calcTokenUpdateAmount } from './send-from-row.utils.js' +import { + updateSendTokenBalance, + updateSendFrom, +} from '../../../actions' +import { + openFromDropdown, + closeFromDropdown, +} from '../../../ducks/send' +import SendFromRow from './send-from-row.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendFromRow) + +function mapStateToProps (state) { + return { + from: getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state), + fromAccounts: accountsWithSendEtherInfoSelector(state), + conversionRate: getConversionRate(state), + fromDropdownOpen: getFromDropdownOpen(state), + tokenContract: getSelectedTokenContract(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + updateSendTokenBalance: (usersToken, selectedToken) => { + if (!usersToken) return + + const tokenBalance = calcTokenUpdateAmount(selectedToken, selectedToken) + dispatch(updateSendTokenBalance(tokenBalance)) + }, + updateSendFrom: newFrom => dispatch(updateSendFrom(newFrom)), + openFromDropdown: () => dispatch(()), + closeFromDropdown: () => dispatch(()), + } +} diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js new file mode 100644 index 000000000..03ef4806b --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/send-from-row.selectors.js @@ -0,0 +1,9 @@ +const selectors = { + getFromDropdownOpen, +} + +module.exports = selectors + +function getFromDropdownOpen (state) { + return state.send.fromDropdownOpen +} diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.utils.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.utils.js new file mode 100644 index 000000000..2be25816f --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/send-from-row.utils.js @@ -0,0 +1,12 @@ +const { + calcTokenAmount, +} = require('../../token-util') + +function calcTokenUpdateAmount (usersToken, selectedToken) { + const { decimals } = selectedToken || {} + return calcTokenAmount(usersToken.balance.toString(), decimals) +} + +module.exports = { + calcTokenUpdateAmount +} diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-component.test.js diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-container.test.js diff --git a/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js b/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/tests/send-from-row-selectors.test.js diff --git a/ui/app/components/send_/send-content/send-gas-row/README.md b/ui/app/components/send_/send-content/send-gas-row/README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-gas-row/README.md diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js new file mode 100644 index 000000000..8c1f14f48 --- /dev/null +++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component' +import GasFeeDisplay from '../../../send/gas-fee-display-v2' + +export default class SendGasRow extends Component { + + static propTypes = { + closeFromDropdown: PropTypes.func, + conversionRate: PropTypes.string, + from: PropTypes.string, + fromAccounts: PropTypes.array, + fromDropdownOpen: PropTypes.bool, + openFromDropdown: PropTypes.func, + tokenContract: PropTypes.object, + updateSendFrom: PropTypes.func, + updateSendTokenBalance: PropTypes.func, + }; + + async handleFromChange (newFrom) { + const { + updateSendFrom, + tokenContract, + updateSendTokenBalance, + } = this.props + + if (tokenContract) { + const usersToken = await tokenContract.balanceOf(newFrom.address) + updateSendTokenBalance(usersToken) + } + updateSendFrom(newFrom) + } + + render () { + const { + conversionRate, + convertedCurrency, + showCustomizeGasModal, + gasTotal, + gasLoadingError, + } = this.props + + return ( + <SendRowWrapper label={`${this.context.t('gasFee')}:`}> + <GasFeeDisplay + gasTotal={gasTotal}, + conversionRate={conversionRate}, + convertedCurrency={convertedCurrency}, + onClick={() => showCustomizeGasModal()}, + gasLoadingError={gasLoadingError}, + /> + </SendRowWrapper> + ); + } + +} + +SendGasRow.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js new file mode 100644 index 000000000..7fb3a68be --- /dev/null +++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js @@ -0,0 +1,26 @@ +import { + getConversionRate, + getConvertedCurrency, + getGasTotal, +} from '../../send.selectors.js' +import { getGasLoadingError } from './send-gas-row.selectors.js' +import { calcTokenUpdateAmount } from './send-gas-row.utils.js' +import { showModal } from '../../../actions' +import SendGasRow from './send-from-row.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow) + +function mapStateToProps (state) { + return { + conversionRate: getConversionRate(state), + convertedCurrency: getConvertedCurrency(state), + gasTotal: getGasTotal(state), + gasLoadingError: getGasLoadingError(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS' })), + } +} diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js new file mode 100644 index 000000000..d069ae8c6 --- /dev/null +++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js @@ -0,0 +1,9 @@ +const selectors = { + sendGasIsInError, +} + +module.exports = selectors + +function sendGasIsInError (state) { + return state.send.errors.gasLoading +} diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-selectors.test.js diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js new file mode 100644 index 000000000..08f830cc5 --- /dev/null +++ b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js @@ -0,0 +1,23 @@ +export default class SendRowErrorMessage extends Component { + + static propTypes = { + errors: PropTypes.object, + errorType: PropTypes.string, + }; + + render () { + const { errors, errorType } = this.props + const errorMessage = errors[errorType] + + return ( + errorMessage + ? <div className='send-v2__error'>{errorMessage}</div> + : null + ); + } + +} + +SendRowErrorMessage.contextTypes = { + t: PropTypes.func, +}
\ No newline at end of file diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js new file mode 100644 index 000000000..2278dbe63 --- /dev/null +++ b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js @@ -0,0 +1,11 @@ +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, + } +}
\ No newline at end of file diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js new file mode 100644 index 000000000..92382da01 --- /dev/null +++ b/ui/app/components/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/send-row-error-message.container' + +export default class SendRowWrapper extends Component { + + static propTypes = { + label: PropTypes.string, + showError: PropTypes.bool, + children: PropTypes.node, + errorType: PropTypes.string, + }; + + render () { + const { + label, + errorType = '', + showError = false, + children, + } = this.props + + let formField = children[0] + let customLabelContent = null + + if (children.length === 2) { + formField = children[1] + customLabelContent = children[0] + } + + return ( + <div className="send-v2__form-row"> + <div className="send-v2__form-label"> + {label} + (showError && <SendRowErrorMessage errorType={errorType}/>) + {customLabelContent} + </div> + <div className="send-v2__form-field"> + {formField} + </div> + </div> + ); + } + +} + +SendRowWrapper.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.scss diff --git a/ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js b/ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row-README.md b/ui/app/components/send_/send-content/send-to-row/send-to-row-README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-to-row/send-to-row-README.md diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js new file mode 100644 index 000000000..5f81402d8 --- /dev/null +++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js @@ -0,0 +1,66 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper/send-row-wrapper.component' +import ToDropdown from '../../../ens-input' + +export default class SendToRow extends Component { + + static propTypes = { + to: PropTypes.string, + toAccounts: PropTypes.array, + toDropdownOpen: PropTypes.bool, + inError: PropTypes.bool, + updateSendTo: PropTypes.func, + updateSendToError: PropTypes.func, + openToDropdown: PropTypes.func, + closeToDropdown: PropTypes.func, + network: PropTypes.number, + }; + + handleToChange (to, nickname = '') { + const { updateSendTo, updateSendToError } = this.props + updateSendTo(to, nickname) + updateSendErrors(to) + } + + render () { + const { + from, + fromAccounts, + conversionRate, + fromDropdownOpen, + tokenContract, + openToDropdown, + closeToDropdown, + network, + inError, + } = this.props + + return ( + <SendRowWrapper + label={`${this.context.t('to')}:`} + showError={inError} + errorType={'to'} + > + <EnsInput + name={'address'} + placeholder={this.context.t('recipient Address')} + network={network}, + to={to}, + accounts={toAccounts} + dropdownOpen={toDropdownOpen} + openDropdown={() => openToDropdown()} + closeDropdown={() => closeToDropdown()} + onChange={this.handleToChange} + inError={inError} + /> + </SendRowWrapper> + ); + } + +} + +SendToRow.contextTypes = { + t: PropTypes.func, +} + diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js new file mode 100644 index 000000000..1c446c168 --- /dev/null +++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js @@ -0,0 +1,43 @@ +import { + getSendTo, + getToAccounts, + getCurrentNetwork, +} from '../../send.selectors.js' +import { + getToDropdownOpen, + sendToIsInError, +} from './send-to-row.selectors.js' +import { getToErrorObject } from './send-to-row.utils.js' +import { + updateSendErrors, + updateSendTo, +} from '../../../actions' +import { + openToDropdown, + closeToDropdown, +} from '../../../ducks/send' +import SendToRow from './send-to-row.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendToRow) + +function mapStateToProps (state) { + updateSendTo + return { + to: getSendTo(state), + toAccounts: getSendToAccounts(state), + toDropdownOpen: getToDropdownOpen(state), + inError: sendToIsInError(state), + network: getCurrentNetwork(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + updateSendToError: (to) => { + dispatch(updateSendErrors(getToErrorObject(to))) + }, + updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), + openToDropdown: () => dispatch(()), + closeToDropdown: () => dispatch(()), + } +}
\ No newline at end of file diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js new file mode 100644 index 000000000..c741aad84 --- /dev/null +++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.selectors.js @@ -0,0 +1,14 @@ +const selectors = { + getToDropdownOpen, + sendToIsInError, +} + +module.exports = selectors + +function getToDropdownOpen (state) { + return state.send.toDropdownOpen +} + +function sendToIsInError (state) { + return Boolean(state.metamask.send.errors.to) +} diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js new file mode 100644 index 000000000..52bfde009 --- /dev/null +++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js @@ -0,0 +1,17 @@ +const { isValidAddress } = require('../../../../util') + +function getToErrorObject (to) { + let toError = null + + if (!to) { + toError = 'required' + } else if (!isValidAddress(to)) { + toError = 'invalidAddressRecipient' + } + + return { to: toError } +} + +module.exports = { + getToErrorObject +} diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-selectors.test.js diff --git a/ui/app/components/send_/send-content/tests/send-content-component.test.js b/ui/app/components/send_/send-content/tests/send-content-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-content/tests/send-content-component.test.js diff --git a/ui/app/components/send_/send-footer/README.md b/ui/app/components/send_/send-footer/README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-footer/README.md diff --git a/ui/app/components/send_/send-footer/send-footer.component.js b/ui/app/components/send_/send-footer/send-footer.component.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-footer/send-footer.component.js diff --git a/ui/app/components/send_/send-footer/send-footer.container.js b/ui/app/components/send_/send-footer/send-footer.container.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-footer/send-footer.container.js diff --git a/ui/app/components/send_/send-footer/send-footer.scss b/ui/app/components/send_/send-footer/send-footer.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-footer/send-footer.scss diff --git a/ui/app/components/send_/send-footer/send-footer.selectors.js b/ui/app/components/send_/send-footer/send-footer.selectors.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-footer/send-footer.selectors.js diff --git a/ui/app/components/send_/send-footer/send-footer.utils.js b/ui/app/components/send_/send-footer/send-footer.utils.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-footer/send-footer.utils.js diff --git a/ui/app/components/send_/send-footer/tests/send-footer-component.test.js b/ui/app/components/send_/send-footer/tests/send-footer-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-footer/tests/send-footer-component.test.js diff --git a/ui/app/components/send_/send-footer/tests/send-footer-container.test.js b/ui/app/components/send_/send-footer/tests/send-footer-container.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-footer/tests/send-footer-container.test.js diff --git a/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js b/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-footer/tests/send-footer-selectors.test.js diff --git a/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js b/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-footer/tests/send-footer-utils.test.js diff --git a/ui/app/components/send_/send-header/README.md b/ui/app/components/send_/send-header/README.md new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-header/README.md diff --git a/ui/app/components/send_/send-header/send-header.component.js b/ui/app/components/send_/send-header/send-header.component.js new file mode 100644 index 000000000..99adfc7e8 --- /dev/null +++ b/ui/app/components/send_/send-header/send-header.component.js @@ -0,0 +1,32 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import PageContainerHeader from '../../page-container/page-container-header.component' + +export default class SendHeader extends Component { + + static propTypes = { + isToken: PropTypes.bool, + clearSend: PropTypes.func, + goHome: PropTypes.func, + }; + + render () { + const { isToken, clearSend, goHome } = this.props + + return ( + <PageContainerHeader + title={isToken ? this.context.t('sendTokens') : this.context.t('sendETH')} + subtitle={this.context.t('onlySendToEtherAddress')} + onClose={() => { + clearSend() + goHome() + }} + /> + ); + } + +} + +SendHeader.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/components/send_/send-header/send-header.container.js b/ui/app/components/send_/send-header/send-header.container.js new file mode 100644 index 000000000..a4d3ac54f --- /dev/null +++ b/ui/app/components/send_/send-header/send-header.container.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux' +import { goHome, clearSend } from '../../../actions' +import SendHeader from './send-header.component' +import { getSelectedToken } from '../../../selectors' + +export default connect(mapStateToProps, mapDispatchToProps)(SendHeader) + +function mapStateToProps (state) { + return { + isToken: Boolean(getSelectedToken(state)) + } +} + +function mapDispatchToProps (dispatch) { + return { + goHome: () => dispatch(goHome()), + clearSend: () => dispatch(clearSend()), + } +} diff --git a/ui/app/components/send_/send-header/tests/send-header-component.test.js b/ui/app/components/send_/send-header/tests/send-header-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-header/tests/send-header-component.test.js diff --git a/ui/app/components/send_/send-header/tests/send-header-container.test.js b/ui/app/components/send_/send-header/tests/send-header-container.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send-header/tests/send-header-container.test.js diff --git a/ui/app/components/send_/send.component.js b/ui/app/components/send_/send.component.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send.component.js diff --git a/ui/app/components/send_/send.container.js b/ui/app/components/send_/send.container.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send.container.js diff --git a/ui/app/components/send_/send.scss b/ui/app/components/send_/send.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send.scss diff --git a/ui/app/components/send_/send.selectors.js b/ui/app/components/send_/send.selectors.js new file mode 100644 index 000000000..9ef13193c --- /dev/null +++ b/ui/app/components/send_/send.selectors.js @@ -0,0 +1,224 @@ +import { valuesFor } from '../../util' +import abi from 'human-standard-token-abi' +import { + multiplyCurrencies, +} from './conversion-util' + +const selectors = { + accountsWithSendEtherInfoSelector, + autoAddToBetaUI, + getConversionRate, + getAddressBook, + getConversionRate, + getCurrentAccountWithSendEtherInfo, + getCurrentCurrency, + getCurrentNetwork, + getCurrentViewContext, + getForceGasMin, + getGasLimit, + getGasPrice, + getSelectedAccount, + getSelectedAddress, + getSelectedIdentity, + getSelectedToken, + getSelectedTokenContract, + getSelectedTokenExchangeRate, + getSelectedTokenToFiatRate, + getSendAmount, + getSendErrors, + getSendFrom, + getSendFromBalance, + getSendMaxModeState, + getSendTo, + getTokenExchangeRate, + transactionsSelector, +} + +module.exports = selectors + +function getSelectedAddress (state) { + const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0] + + return selectedAddress +} + +function getSelectedIdentity (state) { + const selectedAddress = getSelectedAddress(state) + const identities = state.metamask.identities + + return identities[selectedAddress] +} + +function getSelectedAccount (state) { + const accounts = state.metamask.accounts + const selectedAddress = getSelectedAddress(state) + + return accounts[selectedAddress] +} + +function getSelectedToken (state) { + const tokens = state.metamask.tokens || [] + const selectedTokenAddress = state.metamask.selectedTokenAddress + const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0] + const sendToken = state.metamask.send.token + + return selectedToken || sendToken || null +} + +function getSelectedTokenExchangeRate (state) { + const tokenExchangeRates = state.metamask.tokenExchangeRates + const selectedToken = getSelectedToken(state) || {} + const { symbol = '' } = selectedToken + + const pair = `${symbol.toLowerCase()}_eth` + const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} + + return tokenExchangeRate +} + +function getTokenExchangeRate (state, tokenSymbol) { + const pair = `${tokenSymbol.toLowerCase()}_eth` + const tokenExchangeRates = state.metamask.tokenExchangeRates + const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} + + return tokenExchangeRate +} + +function getConversionRate (state) { + return state.metamask.conversionRate +} + +function getAddressBook (state) { + return state.metamask.addressBook +} + +function accountsWithSendEtherInfoSelector (state) { + const { + accounts, + identities, + } = state.metamask + + const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => { + return Object.assign({}, account, identities[key]) + }) + + return accountsWithSendEtherInfo +} + +function getCurrentAccountWithSendEtherInfo (state) { + const currentAddress = getSelectedAddress(state) + const accounts = accountsWithSendEtherInfoSelector(state) + + return accounts.find(({ address }) => address === currentAddress) +} + +function transactionsSelector (state) { + const { network, selectedTokenAddress } = state.metamask + const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs) + const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined + const transactions = state.metamask.selectedAddressTxList || [] + const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) + + // console.log({txsToRender, selectedTokenAddress}) + return selectedTokenAddress + ? txsToRender + .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress) + .sort((a, b) => b.time - a.time) + : txsToRender + .sort((a, b) => b.time - a.time) +} + +function getGasPrice (state) { + return state.metamask.send.gasPrice +} + +function getGasLimit (state) { + return state.metamask.send.gasLimit +} + +function getForceGasMin (state) { + return state.metamask.send.forceGasMin +} + +function getSendFrom (state) { + return state.metamask.send.from +} + +function getSendFromBalance (state) { + const from = state.metamask.send.from || {} + return from.balance +} + +function getSendAmount (state) { + return state.metamask.send.amount +} + +function getSendMaxModeState (state) { + return state.metamask.send.maxModeOn +} + +function getCurrentCurrency (state) { + return state.metamask.currentCurrency +} + +function getSelectedTokenToFiatRate (state) { + const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state) + const conversionRate = getConversionRate(state) + + const tokenToFiatRate = multiplyCurrencies( + conversionRate, + selectedTokenExchangeRate, + { toNumericBase: 'dec' } + ) + + return tokenToFiatRate +} + +function getSelectedTokenContract (state) { + const selectedToken = getSelectedToken(state) + return selectedToken + ? global.eth.contract(abi).at(selectedToken.address) + : null +} + +function autoAddToBetaUI (state) { + const autoAddTransactionThreshold = 12 + const autoAddAccountsThreshold = 2 + const autoAddTokensThreshold = 1 + + const numberOfTransactions = state.metamask.selectedAddressTxList.length + const numberOfAccounts = Object.keys(state.metamask.accounts).length + const numberOfTokensAdded = state.metamask.tokens.length + + const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) && + (numberOfAccounts > autoAddAccountsThreshold) && + (numberOfTokensAdded > autoAddTokensThreshold) + const userIsNotInBeta = !state.metamask.featureFlags.betaUI + + return userIsNotInBeta && userPassesThreshold +} + +function getCurrentViewContext (state) { + const { currentView = {} } = state.appState + return currentView.context +} + +function getSendErrors (state) { + return state.metamask.send.errors +} + +function getSendTo (state) { + return state.metamask.send.to +} + +function getSendToAccounts (state) { + const fromAccounts = accountsWithSendEtherInfoSelector(state) + const addressBookAccounts = getAddressBook(state) + const allAccounts = [...fromAccounts, ...addressBookAccounts] + // TODO: figure out exactly what the below returns and put a descriptive variable name on it + return Object.entries(allAccounts).map(([key, account]) => account) +} + +function getCurrentNetwork (state) { + return state.metamask.network +}
\ No newline at end of file diff --git a/ui/app/components/send_/send.utils.js b/ui/app/components/send_/send.utils.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/send.utils.js diff --git a/ui/app/components/send_/tests/send-component.test.js b/ui/app/components/send_/tests/send-component.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/tests/send-component.test.js diff --git a/ui/app/components/send_/tests/send-container.test.js b/ui/app/components/send_/tests/send-container.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/tests/send-container.test.js diff --git a/ui/app/components/send_/tests/send-selectors.test.js b/ui/app/components/send_/tests/send-selectors.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/tests/send-selectors.test.js diff --git a/ui/app/components/send_/tests/send-utils.test.js b/ui/app/components/send_/tests/send-utils.test.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ui/app/components/send_/tests/send-utils.test.js diff --git a/ui/app/ducks/send.js b/ui/app/ducks/send.js new file mode 100644 index 000000000..aeca9f92f --- /dev/null +++ b/ui/app/ducks/send.js @@ -0,0 +1,54 @@ +import extend from 'xtend' + +// Actions +const OPEN_FROM_DROPDOWN = 'metamask/send/OPEN_FROM_DROPDOWN'; +const CLOSE_FROM_DROPDOWN = 'metamask/send/CLOSE_FROM_DROPDOWN'; +const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN'; +const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN'; + +// TODO: determine if this approach to initState is consistent with conventional ducks pattern +const initState = { + fromDropdownOpen: false, + toDropdownOpen: false, +} + +// Reducer +export default function reducer(state = initState, action = {}) { + switch (action.type) { + case OPEN_FROM_DROPDOWN: + return extend(sendState, { + fromDropdownOpen: true, + }) + case CLOSE_FROM_DROPDOWN: + return extend(sendState, { + fromDropdownOpen: false, + }) + case OPEN_TO_DROPDOWN: + return extend(sendState, { + toDropdownOpen: true, + }) + case CLOSE_TO_DROPDOWN: + return extend(sendState, { + toDropdownOpen: false, + }) + default: + return sendState + } +} + +// Action Creators +export function openFromDropdown() { + return { type: OPEN_FROM_DROPDOWN }; +} + +export function closeFromDropdown() { + return { type: CLOSE_FROM_DROPDOWN }; +} + +export function openToDropdown() { + return { type: OPEN_TO_DROPDOWN }; +} + +export function closeToDropdown() { + return { type: CLOSE_TO_DROPDOWN }; +}
\ No newline at end of file diff --git a/ui/app/reducers.js b/ui/app/reducers.js index f155b2bf3..ff766e856 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -8,6 +8,7 @@ const reduceIdentities = require('./reducers/identities') const reduceMetamask = require('./reducers/metamask') const reduceApp = require('./reducers/app') const reduceLocale = require('./reducers/locale') +const reduceSend = require('./ducks/send') window.METAMASK_CACHED_LOG_STATE = null @@ -45,6 +46,12 @@ function rootReducer (state, action) { state.localeMessages = reduceLocale(state, action) + // + // Send + // + + state.send = reduceSend(state, action) + window.METAMASK_CACHED_LOG_STATE = state return state } diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 30d3d3152..c5085d9ec 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -32,6 +32,11 @@ const { const { isValidAddress } = require('./util') const { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } = require('./routes') +import PageContainer from './components/page-container/page-container.component' +import SendHeader from './components/send_/send-header/send-header.container' +import PageContainerContent from './components/page-container/page-container-content.component' +import PageContainerFooter from './components/page-container/page-container-footer.component' + SendTransactionScreen.contextTypes = { t: PropTypes.func, } @@ -467,7 +472,7 @@ SendTransactionScreen.prototype.renderMemoRow = function () { } SendTransactionScreen.prototype.renderForm = function () { - return h('.page-container__content', {}, [ + return h(PageContainerContent, [ h('.send-v2__form', [ this.renderFromRow(), @@ -476,9 +481,6 @@ SendTransactionScreen.prototype.renderForm = function () { this.renderAmountRow(), this.renderGasRow(), - - // this.renderMemoRow(), - ]), ]) } @@ -496,26 +498,22 @@ SendTransactionScreen.prototype.renderFooter = function () { const missingTokenBalance = selectedToken && !tokenBalance const noErrors = !amountError && toError === null - return h('div.page-container__footer', [ - h('button.btn-secondary--lg.page-container__footer-button', { - onClick: () => { - clearSend() - history.push(DEFAULT_ROUTE) - }, - }, this.context.t('cancel')), - h('button.btn-primary--lg.page-container__footer-button', { - disabled: !noErrors || !gasTotal || missingTokenBalance, - onClick: event => this.onSubmit(event), - }, this.context.t('next')), - ]) + return h(PageContainerFooter, { + onCancel: () => { + clearSend() + history.push(DEFAULT_ROUTE) + }, + onSubmit: e => this.onSubmit(e), + disabled: !noErrors || !gasTotal || missingTokenBalance, + }) } SendTransactionScreen.prototype.render = function () { return ( - h('div.page-container', [ + h(PageContainer, [ - this.renderHeader(), + h(SendHeader), this.renderForm(), |