diff --git a/ui/app/actions.js b/ui/app/actions.js index ad4270cef..aae037a2d 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -167,6 +167,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 ( +
+ {this.props.children} +
+ ); + } + +} 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 ( +
+ + + + + +
+ ); + } + +} + +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 ( +
+ +
+ {title} +
+ +
+ {subtitle} +
+ +
onClose()} + /> + +
+ ); + } + +} 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 ( +
+ {this.props.children} +
+ ); + } + +} 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 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 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 diff --git a/ui/app/components/send_/README.md b/ui/app/components/send_/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-amount-row/README.md b/ui/app/components/send_/send-content/send-amount-row/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.component.js new file mode 100644 index 000000000..e69de29bb 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..e69de29bb diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js new file mode 100644 index 000000000..e69de29bb 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..e69de29bb 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 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 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 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 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..e69de29bb 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..6ae80e7f2 --- /dev/null +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js @@ -0,0 +1,48 @@ +import { + getSelectedToken, + getPrimaryCurrency, + getAmountConversionRate, + getConvertedCurrency, + getSendAmount, + getGasTotal, + getSelectedBalance, + getTokenBalance, +} from '../../send.selectors.js' +import { + getMaxModeOn, + getSendAmountError, +} from './send-amount-row.selectors.js' +import { getAmountErrorObject } 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-amount-row/send-amount-row.scss b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js new file mode 100644 index 000000000..e69de29bb 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..e69de29bb 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 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 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 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 diff --git a/ui/app/components/send_/send-content/send-content-README.md b/ui/app/components/send_/send-content/send-content-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-content.component.js b/ui/app/components/send_/send-content/send-content.component.js new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-content.scss b/ui/app/components/send_/send-content/send-content.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.component.js new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss b/ui/app/components/send_/send-content/send-from-row/from-dropdown/from-dropdown.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-from-row/from-dropdown/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 diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row-README.md b/ui/app/components/send_/send-content/send-from-row/send-from-row-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js new file mode 100644 index 000000000..7582cb2e6 --- /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/from-dropdown' +import FromDropdown from '' + +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 ( + + this.handleFromChange(newFrom)} + openDropdown={() => openFromDropdown()} + closeDropdown={() => closeFromDropdown()} + conversionRate={conversionRate} + /> + + ); + } + +} + +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..2ff3f0ccd --- /dev/null +++ b/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js @@ -0,0 +1,44 @@ +import { + getSendFrom, + conversionRateSelector, + 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: conversionRateSelector(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 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 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 diff --git a/ui/app/components/send_/send-content/send-gas-row/README.md b/ui/app/components/send_/send-content/send-gas-row/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-gas-row/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..e69de29bb 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..e69de29bb diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.selectors.js new file mode 100644 index 000000000..e69de29bb 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 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 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 diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js new file mode 100644 index 000000000..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 + ?
{errorMessage}
+ : 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 diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js new file mode 100644 index 000000000..a1ac591b9 --- /dev/null +++ b/ui/app/components/send_/send-content/send-row-wrapper/send-row-wrapper.component.js @@ -0,0 +1,39 @@ +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 + + return ( +
+
+ {label} + (showError && ) +
+
+ {children} +
+
+ ); + } + +} + +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 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 diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row-README.md b/ui/app/components/send_/send-content/send-to-row/send-to-row-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js new file mode 100644 index 000000000..abcb54efc --- /dev/null +++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js @@ -0,0 +1,62 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../../../send/from-dropdown' +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 ( + + openToDropdown()} + closeDropdown={() => closeToDropdown()} + onChange={this.handleToChange} + inError={inError} + /> + + ); + } + +} + +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..05bb65fa3 --- /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.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 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 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 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 diff --git a/ui/app/components/send_/send-footer/README.md b/ui/app/components/send_/send-footer/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-footer/send-footer.component.js b/ui/app/components/send_/send-footer/send-footer.component.js new file mode 100644 index 000000000..e69de29bb 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 diff --git a/ui/app/components/send_/send-footer/send-footer.scss b/ui/app/components/send_/send-footer/send-footer.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-footer/send-footer.selectors.js b/ui/app/components/send_/send-footer/send-footer.selectors.js new file mode 100644 index 000000000..e69de29bb 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 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 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 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 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 diff --git a/ui/app/components/send_/send-header/README.md b/ui/app/components/send_/send-header/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send-header/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 ( + { + 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 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 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 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 diff --git a/ui/app/components/send_/send.scss b/ui/app/components/send_/send.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/components/send_/send.selectors.js b/ui/app/components/send_/send.selectors.js new file mode 100644 index 000000000..8c088098e --- /dev/null +++ b/ui/app/components/send_/send.selectors.js @@ -0,0 +1,217 @@ +import { valuesFor } from '../../util' +import abi from 'human-standard-token-abi' +import { + multiplyCurrencies, +} from './conversion-util' + +const selectors = { + getSelectedAddress, + getSelectedIdentity, + getSelectedAccount, + getSelectedToken, + getSelectedTokenExchangeRate, + getTokenExchangeRate, + conversionRateSelector, + transactionsSelector, + accountsWithSendEtherInfoSelector, + getCurrentAccountWithSendEtherInfo, + getGasPrice, + getGasLimit, + getForceGasMin, + getAddressBook, + getSendFrom, + getCurrentCurrency, + getSendAmount, + getSelectedTokenToFiatRate, + getSelectedTokenContract, + autoAddToBetaUI, + getSendMaxModeState, + getCurrentViewContext, + getSendErrors, + getSendTo, + getCurrentNetwork, +} + +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 conversionRateSelector (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 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 = conversionRateSelector(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 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 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 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 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 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 094743ff0..d608957c8 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -31,6 +31,11 @@ const { } = require('./components/send/send-utils') const { isValidAddress } = require('./util') +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, } @@ -181,25 +186,6 @@ SendTransactionScreen.prototype.componentDidUpdate = function (prevProps) { } } -SendTransactionScreen.prototype.renderHeader = function () { - const { selectedToken, clearSend, goHome } = this.props - - return h('div.page-container__header', [ - - h('div.page-container__title', selectedToken ? this.context.t('sendTokens') : this.context.t('sendETH')), - - h('div.page-container__subtitle', this.context.t('onlySendToEtherAddress')), - - h('div.page-container__header-close', { - onClick: () => { - clearSend() - goHome() - }, - }), - - ]) -} - SendTransactionScreen.prototype.renderErrorMessage = function (errorType) { const { errors } = this.props const errorMessage = errors[errorType] @@ -477,7 +463,7 @@ SendTransactionScreen.prototype.renderMemoRow = function () { } SendTransactionScreen.prototype.renderForm = function () { - return h('.page-container__content', {}, [ + return h(PageContainerContent, [ h('.send-v2__form', [ this.renderFromRow(), @@ -486,9 +472,6 @@ SendTransactionScreen.prototype.renderForm = function () { this.renderAmountRow(), this.renderGasRow(), - - // this.renderMemoRow(), - ]), ]) } @@ -506,26 +489,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() - goHome() - }, - }, 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() + goHome() + }, + 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(),