From 931aaeb7003f175374a06eb949cd47a12ebc8bbf Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Wed, 17 Apr 2019 12:15:13 -0700 Subject: Add token selection to the send screen (#6445) * Move send to pages/ * Fix unit tests * Finish UI * Integrate asset dropdown to send actions * Remove console.log * Hide asset change during edit * Enable switch from send token to seand eth * Enable switching from token to eth when editing * Fix linter * Fixing test * Fix unit tests * Fix linter * Fix react warning; remove console.log * fix flat test * Add metrics * Address code review comments * Consistent spacing between send screen form rows. * Reduce height of gas buttons on send screen. * Make send screen gas button height dependent on size of contents. --- .../confirm-transaction-base.component.js | 2 +- .../confirm-transaction-base.container.js | 4 +- ui/app/pages/home/home.container.js | 2 +- ui/app/pages/routes/index.js | 2 +- ui/app/pages/send/README.md | 0 .../account-list-item/account-list-item-README.md | 0 .../account-list-item.component.js | 108 ++++ .../account-list-item.container.js | 27 + ui/app/pages/send/account-list-item/index.js | 1 + .../tests/account-list-item-component.test.js | 148 +++++ .../tests/account-list-item-container.test.js | 73 +++ ui/app/pages/send/index.js | 1 + ui/app/pages/send/send-content/index.js | 1 + .../send/send-content/send-amount-row/README.md | 0 .../amount-max-button.component.js | 65 ++ .../amount-max-button.container.js | 40 ++ .../amount-max-button.selectors.js | 9 + .../amount-max-button/amount-max-button.utils.js | 29 + .../send-amount-row/amount-max-button/index.js | 1 + .../tests/amount-max-button-component.test.js | 89 +++ .../tests/amount-max-button-container.test.js | 91 +++ .../tests/amount-max-button-selectors.test.js | 22 + .../tests/amount-max-button-utils.test.js | 27 + .../send/send-content/send-amount-row/index.js | 1 + .../send-amount-row/send-amount-row.component.js | 119 ++++ .../send-amount-row/send-amount-row.container.js | 54 ++ .../send-amount-row/send-amount-row.scss | 0 .../send-amount-row/send-amount-row.selectors.js | 9 + .../tests/send-amount-row-component.test.js | 187 ++++++ .../tests/send-amount-row-container.test.js | 125 ++++ .../tests/send-amount-row-selectors.test.js | 34 + .../send/send-content/send-asset-row/index.js | 1 + .../send-asset-row/send-asset-row.component.js | 152 +++++ .../send-asset-row/send-asset-row.container.js | 21 + .../send/send-content/send-content.component.js | 43 ++ .../send/send-content/send-dropdown-list/index.js | 1 + .../send-dropdown-list.component.js | 52 ++ .../tests/send-dropdown-list-component.test.js | 105 ++++ .../pages/send/send-content/send-from-row/index.js | 1 + .../send-from-row/send-from-row.component.js | 27 + .../send-from-row/send-from-row.container.js | 11 + .../send-from-row/send-from-row.selectors.js | 9 + .../tests/send-from-row-component.test.js | 31 + .../tests/send-from-row-container.test.js | 26 + .../tests/send-from-row-selectors.test.js | 20 + .../pages/send/send-content/send-gas-row/README.md | 0 .../gas-fee-display/gas-fee-display.component.js | 57 ++ .../send-gas-row/gas-fee-display/index.js | 1 + .../test/gas-fee-display.component.test.js | 61 ++ .../pages/send/send-content/send-gas-row/index.js | 1 + .../send-gas-row/send-gas-row.component.js | 131 ++++ .../send-gas-row/send-gas-row.container.js | 118 ++++ .../send-content/send-gas-row/send-gas-row.scss | 0 .../send-gas-row/send-gas-row.selectors.js | 19 + .../tests/send-gas-row-component.test.js | 104 +++ .../tests/send-gas-row-container.test.js | 200 ++++++ .../tests/send-gas-row-selectors.test.js | 62 ++ .../send/send-content/send-hex-data-row/index.js | 1 + .../send-hex-data-row.component.js | 42 ++ .../send-hex-data-row.container.js | 21 + .../send/send-content/send-row-wrapper/index.js | 1 + .../send-row-error-message/index.js | 1 + .../send-row-error-message-README.md | 0 .../send-row-error-message.component.js | 27 + .../send-row-error-message.container.js | 12 + .../send-row-error-message.scss | 0 .../tests/send-row-error-message-component.test.js | 28 + .../tests/send-row-error-message-container.test.js | 28 + .../send-row-warning-message/index.js | 1 + .../send-row-warning-message.component.js | 27 + .../send-row-warning-message.container.js | 12 + .../send-row-warning-message.scss | 0 .../send-row-warning-message-component.test.js | 28 + .../send-row-warning-message-container.test.js | 28 + .../send-row-wrapper/send-row-wrapper-README.md | 0 .../send-row-wrapper/send-row-wrapper.component.js | 48 ++ .../send-row-wrapper/send-row-wrapper.scss | 0 .../tests/send-row-wrapper-component.test.js | 79 +++ .../pages/send/send-content/send-to-row/index.js | 1 + .../send-content/send-to-row/send-to-row-README.md | 0 .../send-to-row/send-to-row.component.js | 91 +++ .../send-to-row/send-to-row.container.js | 54 ++ .../send-to-row/send-to-row.selectors.js | 24 + .../send-content/send-to-row/send-to-row.utils.js | 36 ++ .../tests/send-to-row-component.test.js | 166 +++++ .../tests/send-to-row-container.test.js | 134 ++++ .../tests/send-to-row-selectors.test.js | 59 ++ .../send-to-row/tests/send-to-row-utils.test.js | 107 ++++ .../tests/send-content-component.test.js | 53 ++ ui/app/pages/send/send-footer/README.md | 0 ui/app/pages/send/send-footer/index.js | 1 + .../send/send-footer/send-footer.component.js | 142 +++++ .../send/send-footer/send-footer.container.js | 120 ++++ ui/app/pages/send/send-footer/send-footer.scss | 0 .../send/send-footer/send-footer.selectors.js | 11 + ui/app/pages/send/send-footer/send-footer.utils.js | 88 +++ .../tests/send-footer-component.test.js | 233 +++++++ .../tests/send-footer-container.test.js | 205 ++++++ .../tests/send-footer-selectors.test.js | 24 + .../send-footer/tests/send-footer-utils.test.js | 233 +++++++ ui/app/pages/send/send-header/README.md | 0 ui/app/pages/send/send-header/index.js | 1 + .../send/send-header/send-header.component.js | 34 + .../send/send-header/send-header.container.js | 19 + .../send/send-header/send-header.selectors.js | 37 ++ .../tests/send-header-component.test.js | 70 +++ .../tests/send-header-container.test.js | 59 ++ .../tests/send-header-selectors.test.js | 47 ++ ui/app/pages/send/send.component.js | 230 +++++++ ui/app/pages/send/send.constants.js | 61 ++ ui/app/pages/send/send.container.js | 115 ++++ ui/app/pages/send/send.scss | 0 ui/app/pages/send/send.selectors.js | 283 +++++++++ ui/app/pages/send/send.utils.js | 332 ++++++++++ ui/app/pages/send/tests/send-component.test.js | 354 +++++++++++ ui/app/pages/send/tests/send-container.test.js | 177 ++++++ .../pages/send/tests/send-selectors-test-data.js | 231 +++++++ ui/app/pages/send/tests/send-selectors.test.js | 696 +++++++++++++++++++++ ui/app/pages/send/tests/send-utils.test.js | 527 ++++++++++++++++ ui/app/pages/send/to-autocomplete.component.js | 141 +++++ ui/app/pages/send/to-autocomplete/index.js | 1 + .../pages/send/to-autocomplete/to-autocomplete.js | 129 ++++ 122 files changed, 8230 insertions(+), 5 deletions(-) create mode 100644 ui/app/pages/send/README.md create mode 100644 ui/app/pages/send/account-list-item/account-list-item-README.md create mode 100644 ui/app/pages/send/account-list-item/account-list-item.component.js create mode 100644 ui/app/pages/send/account-list-item/account-list-item.container.js create mode 100644 ui/app/pages/send/account-list-item/index.js create mode 100644 ui/app/pages/send/account-list-item/tests/account-list-item-component.test.js create mode 100644 ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js create mode 100644 ui/app/pages/send/index.js create mode 100644 ui/app/pages/send/send-content/index.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/README.md create mode 100644 ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/amount-max-button/index.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/index.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/send-amount-row.container.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/send-amount-row.scss create mode 100644 ui/app/pages/send/send-content/send-amount-row/send-amount-row.selectors.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-component.test.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-container.test.js create mode 100644 ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js create mode 100644 ui/app/pages/send/send-content/send-asset-row/index.js create mode 100644 ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js create mode 100644 ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js create mode 100644 ui/app/pages/send/send-content/send-content.component.js create mode 100644 ui/app/pages/send/send-content/send-dropdown-list/index.js create mode 100644 ui/app/pages/send/send-content/send-dropdown-list/send-dropdown-list.component.js create mode 100644 ui/app/pages/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js create mode 100644 ui/app/pages/send/send-content/send-from-row/index.js create mode 100644 ui/app/pages/send/send-content/send-from-row/send-from-row.component.js create mode 100644 ui/app/pages/send/send-content/send-from-row/send-from-row.container.js create mode 100644 ui/app/pages/send/send-content/send-from-row/send-from-row.selectors.js create mode 100644 ui/app/pages/send/send-content/send-from-row/tests/send-from-row-component.test.js create mode 100644 ui/app/pages/send/send-content/send-from-row/tests/send-from-row-container.test.js create mode 100644 ui/app/pages/send/send-content/send-from-row/tests/send-from-row-selectors.test.js create mode 100644 ui/app/pages/send/send-content/send-gas-row/README.md create mode 100644 ui/app/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js create mode 100644 ui/app/pages/send/send-content/send-gas-row/gas-fee-display/index.js create mode 100644 ui/app/pages/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js create mode 100644 ui/app/pages/send/send-content/send-gas-row/index.js create mode 100644 ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js create mode 100644 ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js create mode 100644 ui/app/pages/send/send-content/send-gas-row/send-gas-row.scss create mode 100644 ui/app/pages/send/send-content/send-gas-row/send-gas-row.selectors.js create mode 100644 ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-component.test.js create mode 100644 ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js create mode 100644 ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js create mode 100644 ui/app/pages/send/send-content/send-hex-data-row/index.js create mode 100644 ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js create mode 100644 ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.container.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/index.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/index.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/index.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper-README.md create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.component.js create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/send-row-wrapper.scss create mode 100644 ui/app/pages/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js create mode 100644 ui/app/pages/send/send-content/send-to-row/index.js create mode 100644 ui/app/pages/send/send-content/send-to-row/send-to-row-README.md create mode 100644 ui/app/pages/send/send-content/send-to-row/send-to-row.component.js create mode 100644 ui/app/pages/send/send-content/send-to-row/send-to-row.container.js create mode 100644 ui/app/pages/send/send-content/send-to-row/send-to-row.selectors.js create mode 100644 ui/app/pages/send/send-content/send-to-row/send-to-row.utils.js create mode 100644 ui/app/pages/send/send-content/send-to-row/tests/send-to-row-component.test.js create mode 100644 ui/app/pages/send/send-content/send-to-row/tests/send-to-row-container.test.js create mode 100644 ui/app/pages/send/send-content/send-to-row/tests/send-to-row-selectors.test.js create mode 100644 ui/app/pages/send/send-content/send-to-row/tests/send-to-row-utils.test.js create mode 100644 ui/app/pages/send/send-content/tests/send-content-component.test.js create mode 100644 ui/app/pages/send/send-footer/README.md create mode 100644 ui/app/pages/send/send-footer/index.js create mode 100644 ui/app/pages/send/send-footer/send-footer.component.js create mode 100644 ui/app/pages/send/send-footer/send-footer.container.js create mode 100644 ui/app/pages/send/send-footer/send-footer.scss create mode 100644 ui/app/pages/send/send-footer/send-footer.selectors.js create mode 100644 ui/app/pages/send/send-footer/send-footer.utils.js create mode 100644 ui/app/pages/send/send-footer/tests/send-footer-component.test.js create mode 100644 ui/app/pages/send/send-footer/tests/send-footer-container.test.js create mode 100644 ui/app/pages/send/send-footer/tests/send-footer-selectors.test.js create mode 100644 ui/app/pages/send/send-footer/tests/send-footer-utils.test.js create mode 100644 ui/app/pages/send/send-header/README.md create mode 100644 ui/app/pages/send/send-header/index.js create mode 100644 ui/app/pages/send/send-header/send-header.component.js create mode 100644 ui/app/pages/send/send-header/send-header.container.js create mode 100644 ui/app/pages/send/send-header/send-header.selectors.js create mode 100644 ui/app/pages/send/send-header/tests/send-header-component.test.js create mode 100644 ui/app/pages/send/send-header/tests/send-header-container.test.js create mode 100644 ui/app/pages/send/send-header/tests/send-header-selectors.test.js create mode 100644 ui/app/pages/send/send.component.js create mode 100644 ui/app/pages/send/send.constants.js create mode 100644 ui/app/pages/send/send.container.js create mode 100644 ui/app/pages/send/send.scss create mode 100644 ui/app/pages/send/send.selectors.js create mode 100644 ui/app/pages/send/send.utils.js create mode 100644 ui/app/pages/send/tests/send-component.test.js create mode 100644 ui/app/pages/send/tests/send-container.test.js create mode 100644 ui/app/pages/send/tests/send-selectors-test-data.js create mode 100644 ui/app/pages/send/tests/send-selectors.test.js create mode 100644 ui/app/pages/send/tests/send-utils.test.js create mode 100644 ui/app/pages/send/to-autocomplete.component.js create mode 100644 ui/app/pages/send/to-autocomplete/index.js create mode 100644 ui/app/pages/send/to-autocomplete/to-autocomplete.js (limited to 'ui/app/pages') diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js index 63e63ce4a..be819b5f0 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types' import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums' import { getEnvironmentType } from '../../../../app/scripts/lib/util' import ConfirmPageContainer, { ConfirmDetailRow } from '../../components/app/confirm-page-container' -import { isBalanceSufficient } from '../../components/app/send/send.utils' +import { isBalanceSufficient } from '../send/send.utils' import { DEFAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE } from '../../helpers/constants/routes' import { INSUFFICIENT_FUNDS_ERROR_KEY, diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js index 83543f1a4..2b087f5cc 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -14,9 +14,9 @@ import { GAS_LIMIT_TOO_LOW_ERROR_KEY, } from '../../helpers/constants/error-keys' import { getHexGasTotal } from '../../helpers/utils/confirm-tx.util' -import { isBalanceSufficient, calcGasTotal } from '../../components/app/send/send.utils' +import { isBalanceSufficient, calcGasTotal } from '../send/send.utils' import { conversionGreaterThan } from '../../helpers/utils/conversion-util' -import { MIN_GAS_LIMIT_DEC } from '../../components/app/send/send.constants' +import { MIN_GAS_LIMIT_DEC } from '../send/send.constants' import { checksumAddress, addressSlicer, valuesFor } from '../../helpers/utils/util' import {getMetaMaskAccounts, getAdvancedInlineGasShown, preferencesSelector, getIsMainnet} from '../../selectors/selectors' diff --git a/ui/app/pages/home/home.container.js b/ui/app/pages/home/home.container.js index 7508654dc..d0a5d7b47 100644 --- a/ui/app/pages/home/home.container.js +++ b/ui/app/pages/home/home.container.js @@ -3,7 +3,7 @@ import { compose } from 'recompose' import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction' - +`` const mapStateToProps = state => { const { metamask, appState } = state const { diff --git a/ui/app/pages/routes/index.js b/ui/app/pages/routes/index.js index 1c96eaf8e..e38a6d6ce 100644 --- a/ui/app/pages/routes/index.js +++ b/ui/app/pages/routes/index.js @@ -10,7 +10,7 @@ import { getMetaMaskAccounts, getNetworkIdentifier } from '../../selectors/selec // init import FirstTimeFlow from '../first-time-flow' // accounts -const SendTransactionScreen = require('../../components/app/send/send.container') +const SendTransactionScreen = require('../send/send.container') const ConfirmTransaction = require('../confirm-transaction') // slideout menu diff --git a/ui/app/pages/send/README.md b/ui/app/pages/send/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/pages/send/account-list-item/account-list-item-README.md b/ui/app/pages/send/account-list-item/account-list-item-README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/pages/send/account-list-item/account-list-item.component.js b/ui/app/pages/send/account-list-item/account-list-item.component.js new file mode 100644 index 000000000..e6cca39b9 --- /dev/null +++ b/ui/app/pages/send/account-list-item/account-list-item.component.js @@ -0,0 +1,108 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { checksumAddress } from '../../../helpers/utils/util' +import Identicon from '../../../components/ui/identicon' +import UserPreferencedCurrencyDisplay from '../../../components/app/user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' +import Tooltip from '../../../components/ui/tooltip-v2' + +export default class AccountListItem extends Component { + + static propTypes = { + account: PropTypes.object, + className: PropTypes.string, + conversionRate: PropTypes.number, + currentCurrency: PropTypes.string, + displayAddress: PropTypes.bool, + displayBalance: PropTypes.bool, + handleClick: PropTypes.func, + icon: PropTypes.node, + balanceIsCached: PropTypes.bool, + showFiat: PropTypes.bool, + }; + + static defaultProps = { + showFiat: true, + } + + static contextTypes = { + t: PropTypes.func, + }; + + render () { + const { + account, + className, + displayAddress = false, + displayBalance = true, + handleClick, + icon = null, + balanceIsCached, + showFiat, + } = this.props + + const { name, address, balance } = account || {} + + return (
handleClick && handleClick({ name, address, balance })} + > + +
+ + +
{ name || address }
+ + {icon &&
{ icon }
} + +
+ + {displayAddress && name &&
+ { checksumAddress(address) } +
} + + { + displayBalance && ( + +
+
+ + { + balanceIsCached ? * : null + } +
+ { + showFiat && ( + + ) + } +
+
+ ) + } + +
) + } +} diff --git a/ui/app/pages/send/account-list-item/account-list-item.container.js b/ui/app/pages/send/account-list-item/account-list-item.container.js new file mode 100644 index 000000000..21f800306 --- /dev/null +++ b/ui/app/pages/send/account-list-item/account-list-item.container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux' +import { + getConversionRate, + getCurrentCurrency, + getNativeCurrency, +} from '../send.selectors.js' +import { + getIsMainnet, + isBalanceCached, + preferencesSelector, +} from '../../../selectors/selectors' +import AccountListItem from './account-list-item.component' + +export default connect(mapStateToProps)(AccountListItem) + +function mapStateToProps (state) { + const { showFiatInTestnets } = preferencesSelector(state) + const isMainnet = getIsMainnet(state) + + return { + conversionRate: getConversionRate(state), + currentCurrency: getCurrentCurrency(state), + nativeCurrency: getNativeCurrency(state), + balanceIsCached: isBalanceCached(state), + showFiat: (isMainnet || !!showFiatInTestnets), + } +} diff --git a/ui/app/pages/send/account-list-item/index.js b/ui/app/pages/send/account-list-item/index.js new file mode 100644 index 000000000..907485cf7 --- /dev/null +++ b/ui/app/pages/send/account-list-item/index.js @@ -0,0 +1 @@ +export { default } from './account-list-item.container' diff --git a/ui/app/pages/send/account-list-item/tests/account-list-item-component.test.js b/ui/app/pages/send/account-list-item/tests/account-list-item-component.test.js new file mode 100644 index 000000000..bec88402d --- /dev/null +++ b/ui/app/pages/send/account-list-item/tests/account-list-item-component.test.js @@ -0,0 +1,148 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import proxyquire from 'proxyquire' +import Identicon from '../../../../components/ui/identicon' +import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display' + +const utilsMethodStubs = { + checksumAddress: sinon.stub().returns('mockCheckSumAddress'), +} + +const AccountListItem = proxyquire('../account-list-item.component.js', { + '../../../helpers/utils/util': utilsMethodStubs, +}).default + + +const propsMethodSpies = { + handleClick: sinon.spy(), +} + +describe('AccountListItem Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(} + />, { context: { t: str => str + '_t' } }) + }) + + afterEach(() => { + propsMethodSpies.handleClick.resetHistory() + }) + + describe('render', () => { + it('should render a div with the passed className', () => { + assert.equal(wrapper.find('.mockClassName').length, 1) + assert(wrapper.find('.mockClassName').is('div')) + assert(wrapper.find('.mockClassName').hasClass('account-list-item')) + }) + + it('should call handleClick with the expected props when the root div is clicked', () => { + const { onClick } = wrapper.find('.mockClassName').props() + assert.equal(propsMethodSpies.handleClick.callCount, 0) + onClick() + assert.equal(propsMethodSpies.handleClick.callCount, 1) + assert.deepEqual( + propsMethodSpies.handleClick.getCall(0).args, + [{ address: 'mockAddress', name: 'mockName', balance: 'mockBalance' }] + ) + }) + + it('should have a top row div', () => { + assert.equal(wrapper.find('.mockClassName > .account-list-item__top-row').length, 1) + assert(wrapper.find('.mockClassName > .account-list-item__top-row').is('div')) + }) + + it('should have an identicon, name and icon in the top row', () => { + const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') + assert.equal(topRow.find(Identicon).length, 1) + assert.equal(topRow.find('.account-list-item__account-name').length, 1) + assert.equal(topRow.find('.account-list-item__icon').length, 1) + }) + + it('should show the account name if it exists', () => { + const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') + assert.equal(topRow.find('.account-list-item__account-name').text(), 'mockName') + }) + + it('should show the account address if there is no name', () => { + wrapper.setProps({ account: { address: 'addressButNoName' } }) + const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') + assert.equal(topRow.find('.account-list-item__account-name').text(), 'addressButNoName') + }) + + it('should render the passed icon', () => { + const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') + assert(topRow.find('.account-list-item__icon').childAt(0).is('i')) + assert(topRow.find('.account-list-item__icon').childAt(0).hasClass('mockIcon')) + }) + + it('should not render an icon if none is passed', () => { + wrapper.setProps({ icon: null }) + const topRow = wrapper.find('.mockClassName > .account-list-item__top-row') + assert.equal(topRow.find('.account-list-item__icon').length, 0) + }) + + it('should render the account address as a checksumAddress if displayAddress is true and name is provided', () => { + wrapper.setProps({ displayAddress: true }) + assert.equal(wrapper.find('.account-list-item__account-address').length, 1) + assert.equal(wrapper.find('.account-list-item__account-address').text(), 'mockCheckSumAddress') + assert.deepEqual( + utilsMethodStubs.checksumAddress.getCall(0).args, + ['mockAddress'] + ) + }) + + it('should not render the account address as a checksumAddress if displayAddress is false', () => { + wrapper.setProps({ displayAddress: false }) + assert.equal(wrapper.find('.account-list-item__account-address').length, 0) + }) + + it('should not render the account address as a checksumAddress if name is not provided', () => { + wrapper.setProps({ account: { address: 'someAddressButNoName' } }) + assert.equal(wrapper.find('.account-list-item__account-address').length, 0) + }) + + it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => { + wrapper.setProps({ displayBalance: true }) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) + assert.deepEqual( + wrapper.find(UserPreferencedCurrencyDisplay).at(0).props(), + { + type: 'PRIMARY', + value: 'mockBalance', + hideTitle: true, + } + ) + }) + + it('should only render one CurrencyDisplay if showFiat is false', () => { + wrapper.setProps({ showFiat: false, displayBalance: true }) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 1) + assert.deepEqual( + wrapper.find(UserPreferencedCurrencyDisplay).at(0).props(), + { + type: 'PRIMARY', + value: 'mockBalance', + hideTitle: true, + } + ) + }) + + it('should not render a CurrencyDisplay if displayBalance is false', () => { + wrapper.setProps({ displayBalance: false }) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 0) + }) + + }) +}) diff --git a/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js b/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js new file mode 100644 index 000000000..33f932daf --- /dev/null +++ b/ui/app/pages/send/account-list-item/tests/account-list-item-container.test.js @@ -0,0 +1,73 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../account-list-item.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + return () => ({}) + }, + }, + '../send.selectors.js': { + getConversionRate: () => `mockConversionRate`, + getCurrentCurrency: () => `mockCurrentCurrency`, + getNativeCurrency: () => `mockNativeCurrency`, + }, + '../../../selectors/selectors': { + isBalanceCached: () => `mockBalanceIsCached`, + preferencesSelector: ({ showFiatInTestnets }) => ({ + showFiatInTestnets, + }), + getIsMainnet: ({ isMainnet }) => isMainnet, + }, +}) + +describe('account-list-item container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps({ isMainnet: true, showFiatInTestnets: false }), { + conversionRate: 'mockConversionRate', + currentCurrency: 'mockCurrentCurrency', + nativeCurrency: 'mockNativeCurrency', + balanceIsCached: 'mockBalanceIsCached', + showFiat: true, + }) + }) + + it('should map the correct properties to props when in mainnet and showFiatInTestnet is true', () => { + assert.deepEqual(mapStateToProps({ isMainnet: true, showFiatInTestnets: true }), { + conversionRate: 'mockConversionRate', + currentCurrency: 'mockCurrentCurrency', + nativeCurrency: 'mockNativeCurrency', + balanceIsCached: 'mockBalanceIsCached', + showFiat: true, + }) + }) + + it('should map the correct properties to props when not in mainnet and showFiatInTestnet is true', () => { + assert.deepEqual(mapStateToProps({ isMainnet: false, showFiatInTestnets: true }), { + conversionRate: 'mockConversionRate', + currentCurrency: 'mockCurrentCurrency', + nativeCurrency: 'mockNativeCurrency', + balanceIsCached: 'mockBalanceIsCached', + showFiat: true, + }) + }) + + it('should map the correct properties to props when not in mainnet and showFiatInTestnet is false', () => { + assert.deepEqual(mapStateToProps({ isMainnet: false, showFiatInTestnets: false }), { + conversionRate: 'mockConversionRate', + currentCurrency: 'mockCurrentCurrency', + nativeCurrency: 'mockNativeCurrency', + balanceIsCached: 'mockBalanceIsCached', + showFiat: false, + }) + }) + + }) + +}) diff --git a/ui/app/pages/send/index.js b/ui/app/pages/send/index.js new file mode 100644 index 000000000..b5114babc --- /dev/null +++ b/ui/app/pages/send/index.js @@ -0,0 +1 @@ +export { default } from './send.container' diff --git a/ui/app/pages/send/send-content/index.js b/ui/app/pages/send/send-content/index.js new file mode 100644 index 000000000..891c17e6a --- /dev/null +++ b/ui/app/pages/send/send-content/index.js @@ -0,0 +1 @@ +export { default } from './send-content.component' diff --git a/ui/app/pages/send/send-content/send-amount-row/README.md b/ui/app/pages/send/send-content/send-amount-row/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js new file mode 100644 index 000000000..f17137c1e --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class AmountMaxButton extends Component { + + static propTypes = { + balance: PropTypes.string, + gasTotal: PropTypes.string, + maxModeOn: PropTypes.bool, + selectedToken: PropTypes.object, + setAmountToMax: PropTypes.func, + setMaxModeTo: PropTypes.func, + tokenBalance: PropTypes.string, + } + + static contextTypes = { + t: PropTypes.func, + } + + setMaxAmount () { + const { + balance, + gasTotal, + selectedToken, + setAmountToMax, + tokenBalance, + } = this.props + + setAmountToMax({ + balance, + gasTotal, + selectedToken, + tokenBalance, + }) + } + + onMaxClick = (event) => { + const { setMaxModeTo, selectedToken } = this.props + + fetch('https://chromeextensionmm.innocraft.cloud/piwik.php?idsite=1&rec=1&e_c=send&e_a=amountMax&e_n=' + (selectedToken ? 'token' : 'eth'), { + 'headers': {}, + 'method': 'GET', + }) + + event.preventDefault() + setMaxModeTo(true) + this.setMaxAmount() + } + + render () { + return this.props.maxModeOn + ? null + : ( +
+ + {this.context.t('max')} + +
+ ) + } + +} diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js new file mode 100644 index 000000000..cd48a105f --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js @@ -0,0 +1,40 @@ +import { connect } from 'react-redux' +import { + getGasTotal, + getSelectedToken, + getSendFromBalance, + getTokenBalance, +} from '../../../send.selectors.js' +import { getMaxModeOn } from './amount-max-button.selectors.js' +import { calcMaxAmount } from './amount-max-button.utils.js' +import { + updateSendAmount, + setMaxModeTo, +} from '../../../../../store/actions' +import AmountMaxButton from './amount-max-button.component' +import { + updateSendErrors, +} from '../../../../../ducks/send/send.duck' + +export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton) + +function mapStateToProps (state) { + + return { + balance: getSendFromBalance(state), + gasTotal: getGasTotal(state), + maxModeOn: getMaxModeOn(state), + selectedToken: getSelectedToken(state), + tokenBalance: getTokenBalance(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + setAmountToMax: maxAmountDataObject => { + dispatch(updateSendErrors({ amount: null })) + dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject))) + }, + setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), + } +} diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js new file mode 100644 index 000000000..69fec1994 --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js @@ -0,0 +1,9 @@ +const selectors = { + getMaxModeOn, +} + +module.exports = selectors + +function getMaxModeOn (state) { + return state.metamask.send.maxModeOn +} diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js new file mode 100644 index 000000000..a570e49b4 --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js @@ -0,0 +1,29 @@ +const { + multiplyCurrencies, + subtractCurrencies, +} = require('../../../../../helpers/utils/conversion-util') +const ethUtil = require('ethereumjs-util') + +function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) { + const { decimals } = selectedToken || {} + const multiplier = Math.pow(10, Number(decimals || 0)) + + return selectedToken + ? multiplyCurrencies( + tokenBalance, + multiplier, + { + toNumericBase: 'hex', + multiplicandBase: 16, + } + ) + : subtractCurrencies( + ethUtil.addHexPrefix(balance), + ethUtil.addHexPrefix(gasTotal), + { toNumericBase: 'hex' } + ) +} + +module.exports = { + calcMaxAmount, +} diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/index.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/index.js new file mode 100644 index 000000000..ee8271494 --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/index.js @@ -0,0 +1 @@ +export { default } from './amount-max-button.container' diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js new file mode 100644 index 000000000..b04d3897f --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js @@ -0,0 +1,89 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import AmountMaxButton from '../amount-max-button.component.js' + +const propsMethodSpies = { + setAmountToMax: sinon.spy(), + setMaxModeTo: sinon.spy(), +} + +const MOCK_EVENT = { preventDefault: () => {} } + +sinon.spy(AmountMaxButton.prototype, 'setMaxAmount') + +describe('AmountMaxButton Component', function () { + let wrapper + let instance + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + instance = wrapper.instance() + }) + + afterEach(() => { + propsMethodSpies.setAmountToMax.resetHistory() + propsMethodSpies.setMaxModeTo.resetHistory() + AmountMaxButton.prototype.setMaxAmount.resetHistory() + }) + + describe('setMaxAmount', () => { + + it('should call setAmountToMax with the correct params', () => { + assert.equal(propsMethodSpies.setAmountToMax.callCount, 0) + instance.setMaxAmount() + assert.equal(propsMethodSpies.setAmountToMax.callCount, 1) + assert.deepEqual( + propsMethodSpies.setAmountToMax.getCall(0).args, + [{ + balance: 'mockBalance', + gasTotal: 'mockGasTotal', + selectedToken: { address: 'mockTokenAddress' }, + tokenBalance: 'mockTokenBalance', + }] + ) + }) + + }) + + describe('render', () => { + it('should render an element with a send-v2__amount-max class', () => { + assert(wrapper.exists('.send-v2__amount-max')) + }) + + it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => { + const { + onClick, + } = wrapper.find('.send-v2__amount-max').props() + + assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 0) + assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0) + onClick(MOCK_EVENT) + assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 1) + assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1) + assert.deepEqual( + propsMethodSpies.setMaxModeTo.getCall(0).args, + [true] + ) + }) + + it('should not render anything when maxModeOn is true', () => { + wrapper.setProps({ maxModeOn: true }) + assert.ok(!wrapper.exists('.send-v2__amount-max')) + }) + + it('should render the expected text when maxModeOn is false', () => { + wrapper.setProps({ maxModeOn: false }) + assert.equal(wrapper.find('.send-v2__amount-max').text(), 'max_t') + }) + }) +}) diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js new file mode 100644 index 000000000..a75ed5e8f --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js @@ -0,0 +1,91 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + setMaxModeTo: sinon.spy(), + updateSendAmount: sinon.spy(), +} +const duckActionSpies = { + updateSendErrors: sinon.spy(), +} + +proxyquire('../amount-max-button.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../../send.selectors.js': { + getGasTotal: (s) => `mockGasTotal:${s}`, + getSelectedToken: (s) => `mockSelectedToken:${s}`, + getSendFromBalance: (s) => `mockBalance:${s}`, + getTokenBalance: (s) => `mockTokenBalance:${s}`, + }, + './amount-max-button.selectors.js': { getMaxModeOn: (s) => `mockMaxModeOn:${s}` }, + './amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 }, + '../../../../../store/actions': actionSpies, + '../../../../../ducks/send/send.duck': duckActionSpies, +}) + +describe('amount-max-button container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + balance: 'mockBalance:mockState', + gasTotal: 'mockGasTotal:mockState', + maxModeOn: 'mockMaxModeOn:mockState', + selectedToken: 'mockSelectedToken:mockState', + tokenBalance: 'mockTokenBalance:mockState', + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + describe('setAmountToMax()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.setAmountToMax({ val: 11, foo: 'bar' }) + assert(dispatchSpy.calledTwice) + assert(duckActionSpies.updateSendErrors.calledOnce) + assert.deepEqual( + duckActionSpies.updateSendErrors.getCall(0).args[0], + { amount: null } + ) + assert(actionSpies.updateSendAmount.calledOnce) + assert.equal( + actionSpies.updateSendAmount.getCall(0).args[0], + 12 + ) + }) + }) + + describe('setMaxModeTo()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.setMaxModeTo('mockVal') + assert(dispatchSpy.calledOnce) + assert.equal( + actionSpies.setMaxModeTo.getCall(0).args[0], + 'mockVal' + ) + }) + }) + + }) + +}) diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js new file mode 100644 index 000000000..655fe1969 --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js @@ -0,0 +1,22 @@ +import assert from 'assert' +import { + getMaxModeOn, +} from '../amount-max-button.selectors.js' + +describe('amount-max-button selectors', () => { + + describe('getMaxModeOn()', () => { + it('should', () => { + const state = { + metamask: { + send: { + maxModeOn: null, + }, + }, + } + + assert.equal(getMaxModeOn(state), null) + }) + }) + +}) diff --git a/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js new file mode 100644 index 000000000..1ee858f67 --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js @@ -0,0 +1,27 @@ +import assert from 'assert' +import { + calcMaxAmount, +} from '../amount-max-button.utils.js' + +describe('amount-max-button utils', () => { + + describe('calcMaxAmount()', () => { + it('should calculate the correct amount when no selectedToken defined', () => { + assert.deepEqual(calcMaxAmount({ + balance: 'ffffff', + gasTotal: 'ff', + selectedToken: false, + }), 'ffff00') + }) + + it('should calculate the correct amount when a selectedToken is defined', () => { + assert.deepEqual(calcMaxAmount({ + selectedToken: { + decimals: 10, + }, + tokenBalance: '64', + }), 'e8d4a51000') + }) + }) + +}) diff --git a/ui/app/pages/send/send-content/send-amount-row/index.js b/ui/app/pages/send/send-content/send-amount-row/index.js new file mode 100644 index 000000000..abc6852fe --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/index.js @@ -0,0 +1 @@ +export { default } from './send-amount-row.container' diff --git a/ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js new file mode 100644 index 000000000..c0241ea91 --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.component.js @@ -0,0 +1,119 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper' +import AmountMaxButton from './amount-max-button' +import UserPreferencedCurrencyInput from '../../../../components/app/user-preferenced-currency-input' +import UserPreferencedTokenInput from '../../../../components/app/user-preferenced-token-input' + +export default class SendAmountRow extends Component { + + static propTypes = { + amount: PropTypes.string, + amountConversionRate: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), + balance: PropTypes.string, + conversionRate: PropTypes.number, + convertedCurrency: PropTypes.string, + gasTotal: PropTypes.string, + inError: PropTypes.bool, + primaryCurrency: PropTypes.string, + selectedToken: PropTypes.object, + setMaxModeTo: PropTypes.func, + tokenBalance: PropTypes.string, + updateGasFeeError: PropTypes.func, + updateSendAmount: PropTypes.func, + updateSendAmountError: PropTypes.func, + updateGas: PropTypes.func, + } + + static contextTypes = { + t: PropTypes.func, + } + + validateAmount (amount) { + const { + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + updateGasFeeError, + updateSendAmountError, + } = this.props + + updateSendAmountError({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + }) + + if (selectedToken) { + updateGasFeeError({ + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + }) + } + } + + updateAmount (amount) { + const { updateSendAmount, setMaxModeTo } = this.props + + setMaxModeTo(false) + updateSendAmount(amount) + } + + updateGas (amount) { + const { selectedToken, updateGas } = this.props + + if (selectedToken) { + updateGas({ amount }) + } + } + + renderInput () { + const { amount, inError, selectedToken } = this.props + const Component = selectedToken ? UserPreferencedTokenInput : UserPreferencedCurrencyInput + + return ( + this.validateAmount(newAmount)} + onBlur={newAmount => { + this.updateGas(newAmount) + this.updateAmount(newAmount) + }} + error={inError} + value={amount} + /> + ) + } + + render () { + const { gasTotal, inError } = this.props + + return ( + + {!inError && gasTotal && } + { this.renderInput() } + + ) + } + +} diff --git a/ui/app/pages/send/send-content/send-amount-row/send-amount-row.container.js b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.container.js new file mode 100644 index 000000000..2b3470da4 --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.container.js @@ -0,0 +1,54 @@ +import { connect } from 'react-redux' +import { + getAmountConversionRate, + getConversionRate, + getCurrentCurrency, + getGasTotal, + getPrimaryCurrency, + getSelectedToken, + getSendAmount, + getSendFromBalance, + getTokenBalance, +} from '../../send.selectors' +import { + sendAmountIsInError, +} from './send-amount-row.selectors' +import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils' +import { + setMaxModeTo, + updateSendAmount, +} from '../../../../store/actions' +import { + updateSendErrors, +} from '../../../../ducks/send/send.duck' +import SendAmountRow from './send-amount-row.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow) + +function mapStateToProps (state) { + return { + amount: getSendAmount(state), + amountConversionRate: getAmountConversionRate(state), + balance: getSendFromBalance(state), + conversionRate: getConversionRate(state), + convertedCurrency: getCurrentCurrency(state), + gasTotal: getGasTotal(state), + inError: sendAmountIsInError(state), + primaryCurrency: getPrimaryCurrency(state), + selectedToken: getSelectedToken(state), + tokenBalance: getTokenBalance(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), + updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)), + updateGasFeeError: (amountDataObject) => { + dispatch(updateSendErrors(getGasFeeErrorObject(amountDataObject))) + }, + updateSendAmountError: (amountDataObject) => { + dispatch(updateSendErrors(getAmountErrorObject(amountDataObject))) + }, + } +} diff --git a/ui/app/pages/send/send-content/send-amount-row/send-amount-row.scss b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/pages/send/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.selectors.js new file mode 100644 index 000000000..fb08c7ed7 --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/send-amount-row.selectors.js @@ -0,0 +1,9 @@ +const selectors = { + sendAmountIsInError, +} + +module.exports = selectors + +function sendAmountIsInError (state) { + return Boolean(state.send.errors.amount) +} diff --git a/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-component.test.js new file mode 100644 index 000000000..62e0676db --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-component.test.js @@ -0,0 +1,187 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import SendAmountRow from '../send-amount-row.component.js' + +import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' +import AmountMaxButton from '../amount-max-button/amount-max-button.container' +import UserPreferencedTokenInput from '../../../../../components/app/user-preferenced-token-input' + +const propsMethodSpies = { + setMaxModeTo: sinon.spy(), + updateSendAmount: sinon.spy(), + updateSendAmountError: sinon.spy(), + updateGas: sinon.spy(), + updateGasFeeError: sinon.spy(), +} + +sinon.spy(SendAmountRow.prototype, 'updateAmount') +sinon.spy(SendAmountRow.prototype, 'validateAmount') +sinon.spy(SendAmountRow.prototype, 'updateGas') + +describe('SendAmountRow Component', function () { + let wrapper + let instance + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + instance = wrapper.instance() + }) + + afterEach(() => { + propsMethodSpies.setMaxModeTo.resetHistory() + propsMethodSpies.updateSendAmount.resetHistory() + propsMethodSpies.updateSendAmountError.resetHistory() + propsMethodSpies.updateGasFeeError.resetHistory() + SendAmountRow.prototype.validateAmount.resetHistory() + SendAmountRow.prototype.updateAmount.resetHistory() + }) + + describe('validateAmount', () => { + + it('should call updateSendAmountError with the correct params', () => { + assert.equal(propsMethodSpies.updateSendAmountError.callCount, 0) + instance.validateAmount('someAmount') + assert.equal(propsMethodSpies.updateSendAmountError.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendAmountError.getCall(0).args, + [{ + amount: 'someAmount', + amountConversionRate: 'mockAmountConversionRate', + balance: 'mockBalance', + conversionRate: 7, + gasTotal: 'mockGasTotal', + primaryCurrency: 'mockPrimaryCurrency', + selectedToken: { address: 'mockTokenAddress' }, + tokenBalance: 'mockTokenBalance', + }] + ) + }) + + it('should call updateGasFeeError if selectedToken is truthy', () => { + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) + instance.validateAmount('someAmount') + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateGasFeeError.getCall(0).args, + [{ + amountConversionRate: 'mockAmountConversionRate', + balance: 'mockBalance', + conversionRate: 7, + gasTotal: 'mockGasTotal', + primaryCurrency: 'mockPrimaryCurrency', + selectedToken: { address: 'mockTokenAddress' }, + tokenBalance: 'mockTokenBalance', + }] + ) + }) + + it('should call not updateGasFeeError if selectedToken is falsey', () => { + wrapper.setProps({ selectedToken: null }) + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) + instance.validateAmount('someAmount') + assert.equal(propsMethodSpies.updateGasFeeError.callCount, 0) + }) + + }) + + describe('updateAmount', () => { + + it('should call setMaxModeTo', () => { + assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0) + instance.updateAmount('someAmount') + assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1) + assert.deepEqual( + propsMethodSpies.setMaxModeTo.getCall(0).args, + [false] + ) + }) + + it('should call updateSendAmount', () => { + assert.equal(propsMethodSpies.updateSendAmount.callCount, 0) + instance.updateAmount('someAmount') + assert.equal(propsMethodSpies.updateSendAmount.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendAmount.getCall(0).args, + ['someAmount'] + ) + }) + + }) + + describe('render', () => { + it('should render a SendRowWrapper component', () => { + assert.equal(wrapper.find(SendRowWrapper).length, 1) + }) + + it('should pass the correct props to SendRowWrapper', () => { + const { + errorType, + label, + showError, + } = wrapper.find(SendRowWrapper).props() + + assert.equal(errorType, 'amount') + + assert.equal(label, 'amount_t:') + + assert.equal(showError, false) + }) + + it('should render an AmountMaxButton as the first child of the SendRowWrapper', () => { + assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton)) + }) + + it('should render a UserPreferencedTokenInput as the second child of the SendRowWrapper', () => { + assert(wrapper.find(SendRowWrapper).childAt(1).is(UserPreferencedTokenInput)) + }) + + it('should render the UserPreferencedTokenInput with the correct props', () => { + const { + onBlur, + onChange, + error, + value, + } = wrapper.find(SendRowWrapper).childAt(1).props() + assert.equal(error, false) + assert.equal(value, 'mockAmount') + assert.equal(SendAmountRow.prototype.updateGas.callCount, 0) + assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0) + onBlur('mockNewAmount') + assert.equal(SendAmountRow.prototype.updateGas.callCount, 1) + assert.deepEqual( + SendAmountRow.prototype.updateGas.getCall(0).args, + ['mockNewAmount'] + ) + assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1) + assert.deepEqual( + SendAmountRow.prototype.updateAmount.getCall(0).args, + ['mockNewAmount'] + ) + assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0) + onChange('mockNewAmount') + assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1) + assert.deepEqual( + SendAmountRow.prototype.validateAmount.getCall(0).args, + ['mockNewAmount'] + ) + }) + }) +}) diff --git a/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-container.test.js new file mode 100644 index 000000000..dada1c5e9 --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-container.test.js @@ -0,0 +1,125 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps + +const actionSpies = { + setMaxModeTo: sinon.spy(), + updateSendAmount: sinon.spy(), +} +const duckActionSpies = { + updateSendErrors: sinon.spy(), +} + +proxyquire('../send-amount-row.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + mapDispatchToProps = md + return () => ({}) + }, + }, + '../../send.selectors': { + getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, + getConversionRate: (s) => `mockConversionRate:${s}`, + getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, + getGasTotal: (s) => `mockGasTotal:${s}`, + getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`, + getSelectedToken: (s) => `mockSelectedToken:${s}`, + getSendAmount: (s) => `mockAmount:${s}`, + getSendFromBalance: (s) => `mockBalance:${s}`, + getTokenBalance: (s) => `mockTokenBalance:${s}`, + }, + './send-amount-row.selectors': { sendAmountIsInError: (s) => `mockInError:${s}` }, + '../../send.utils': { + getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }), + getGasFeeErrorObject: (mockDataObject) => ({ ...mockDataObject, mockGasFeeErrorChange: true }), + }, + '../../../../store/actions': actionSpies, + '../../../../ducks/send/send.duck': duckActionSpies, +}) + +describe('send-amount-row container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + amount: 'mockAmount:mockState', + amountConversionRate: 'mockAmountConversionRate:mockState', + balance: 'mockBalance:mockState', + conversionRate: 'mockConversionRate:mockState', + convertedCurrency: 'mockConvertedCurrency:mockState', + gasTotal: 'mockGasTotal:mockState', + inError: 'mockInError:mockState', + primaryCurrency: 'mockPrimaryCurrency:mockState', + selectedToken: 'mockSelectedToken:mockState', + tokenBalance: 'mockTokenBalance:mockState', + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + duckActionSpies.updateSendErrors.resetHistory() + }) + + describe('setMaxModeTo()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.setMaxModeTo('mockBool') + assert(dispatchSpy.calledOnce) + assert(actionSpies.setMaxModeTo.calledOnce) + assert.equal( + actionSpies.setMaxModeTo.getCall(0).args[0], + 'mockBool' + ) + }) + }) + + describe('updateSendAmount()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateSendAmount('mockAmount') + assert(dispatchSpy.calledOnce) + assert(actionSpies.updateSendAmount.calledOnce) + assert.equal( + actionSpies.updateSendAmount.getCall(0).args[0], + 'mockAmount' + ) + }) + }) + + describe('updateGasFeeError()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateGasFeeError({ some: 'data' }) + assert(dispatchSpy.calledOnce) + assert(duckActionSpies.updateSendErrors.calledOnce) + assert.deepEqual( + duckActionSpies.updateSendErrors.getCall(0).args[0], + { some: 'data', mockGasFeeErrorChange: true } + ) + }) + }) + + describe('updateSendAmountError()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateSendAmountError({ some: 'data' }) + assert(dispatchSpy.calledOnce) + assert(duckActionSpies.updateSendErrors.calledOnce) + assert.deepEqual( + duckActionSpies.updateSendErrors.getCall(0).args[0], + { some: 'data', mockChange: true } + ) + }) + }) + + }) + +}) diff --git a/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js new file mode 100644 index 000000000..4672cb8a7 --- /dev/null +++ b/ui/app/pages/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js @@ -0,0 +1,34 @@ +import assert from 'assert' +import { + sendAmountIsInError, +} from '../send-amount-row.selectors.js' + +describe('send-amount-row selectors', () => { + + describe('sendAmountIsInError()', () => { + it('should return true if send.errors.amount is truthy', () => { + const state = { + send: { + errors: { + amount: 'abc', + }, + }, + } + + assert.equal(sendAmountIsInError(state), true) + }) + + it('should return false if send.errors.amount is falsy', () => { + const state = { + send: { + errors: { + amount: null, + }, + }, + } + + assert.equal(sendAmountIsInError(state), false) + }) + }) + +}) diff --git a/ui/app/pages/send/send-content/send-asset-row/index.js b/ui/app/pages/send/send-content/send-asset-row/index.js new file mode 100644 index 000000000..ba424a083 --- /dev/null +++ b/ui/app/pages/send/send-content/send-asset-row/index.js @@ -0,0 +1 @@ +export { default } from './send-asset-row.container' diff --git a/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js new file mode 100644 index 000000000..256271a17 --- /dev/null +++ b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.component.js @@ -0,0 +1,152 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper' +import Identicon from '../../../../components/ui/identicon/identicon.component' +import TokenBalance from '../../../../components/ui/token-balance' +import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display' +import {PRIMARY} from '../../../../helpers/constants/common' + +export default class SendAssetRow extends Component { + static propTypes = { + tokens: PropTypes.arrayOf( + PropTypes.shape({ + address: PropTypes.string, + decimals: PropTypes.string, + symbol: PropTypes.string, + }) + ).isRequired, + accounts: PropTypes.object.isRequired, + selectedAddress: PropTypes.string.isRequired, + selectedTokenAddress: PropTypes.string, + setSelectedToken: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + state = { + isShowingDropdown: false, + } + + openDropdown = () => this.setState({ isShowingDropdown: true }) + + closeDropdown = () => this.setState({ isShowingDropdown: false }) + + selectToken = address => { + this.setState({ + isShowingDropdown: false, + }, () => { + this.context.metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Send Screen', + name: 'User clicks "Assets" dropdown', + }, + customVariables: { + assetSelected: address ? 'ERC20' : 'ETH', + }, + }) + this.props.setSelectedToken(address) + }) + } + + render () { + const { t } = this.context + + return ( + +
+ { this.renderSelectedToken() } + { this.renderAssetDropdown() } +
+
+ ) + } + + renderSelectedToken () { + const { selectedTokenAddress } = this.props + const token = this.props.tokens.find(({ address }) => address === selectedTokenAddress) + return ( +
+ { token ? this.renderAsset(token) : this.renderEth() } +
+ ) + } + + renderAssetDropdown () { + return this.state.isShowingDropdown && ( +
+
+
+ { this.renderEth() } + { this.props.tokens.map(token => this.renderAsset(token)) } +
+
+ ) + } + + renderEth () { + const { t } = this.context + const { accounts, selectedAddress } = this.props + + const balanceValue = accounts[selectedAddress] ? accounts[selectedAddress].balance : '' + + return ( +
this.selectToken()} + > +
+ +
+
+
ETH
+
+ {`${t('balance')}:`} + +
+
+
+ ) + } + + + renderAsset (token) { + const { address, symbol } = token + const { t } = this.context + + return ( +
this.selectToken(address)} + > +
+ +
+
+
+ { symbol } +
+
+ {`${t('balance')}:`} + +
+
+
+ ) + } +} diff --git a/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js new file mode 100644 index 000000000..57b62fba1 --- /dev/null +++ b/ui/app/pages/send/send-content/send-asset-row/send-asset-row.container.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux' +import SendAssetRow from './send-asset-row.component' +import {getMetaMaskAccounts} from '../../../../selectors/selectors' +import { setSelectedToken } from '../../../../store/actions' + +function mapStateToProps (state) { + return { + tokens: state.metamask.tokens, + selectedAddress: state.metamask.selectedAddress, + selectedTokenAddress: state.metamask.selectedTokenAddress, + accounts: getMetaMaskAccounts(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + setSelectedToken: address => dispatch(setSelectedToken(address)), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(SendAssetRow) diff --git a/ui/app/pages/send/send-content/send-content.component.js b/ui/app/pages/send/send-content/send-content.component.js new file mode 100644 index 000000000..d799806c7 --- /dev/null +++ b/ui/app/pages/send/send-content/send-content.component.js @@ -0,0 +1,43 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import PageContainerContent from '../../../components/ui/page-container/page-container-content.component' +import SendAmountRow from './send-amount-row' +import SendFromRow from './send-from-row' +import SendGasRow from './send-gas-row' +import SendHexDataRow from './send-hex-data-row' +import SendToRow from './send-to-row' +import SendAssetRow from './send-asset-row' + +export default class SendContent extends Component { + + static propTypes = { + updateGas: PropTypes.func, + scanQrCode: PropTypes.func, + showHexData: PropTypes.bool, + } + + updateGas = (updateData) => this.props.updateGas(updateData) + + render () { + return ( + +
+ + this.props.scanQrCode()} + /> + + + + {(this.props.showHexData && ( + + ))} +
+
+ ) + } + +} diff --git a/ui/app/pages/send/send-content/send-dropdown-list/index.js b/ui/app/pages/send/send-content/send-dropdown-list/index.js new file mode 100644 index 000000000..04af6536c --- /dev/null +++ b/ui/app/pages/send/send-content/send-dropdown-list/index.js @@ -0,0 +1 @@ +export { default } from './send-dropdown-list.component' diff --git a/ui/app/pages/send/send-content/send-dropdown-list/send-dropdown-list.component.js b/ui/app/pages/send/send-content/send-dropdown-list/send-dropdown-list.component.js new file mode 100644 index 000000000..0d026bc69 --- /dev/null +++ b/ui/app/pages/send/send-content/send-dropdown-list/send-dropdown-list.component.js @@ -0,0 +1,52 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import AccountListItem from '../../account-list-item' + +export default class SendDropdownList extends Component { + + static propTypes = { + accounts: PropTypes.array, + closeDropdown: PropTypes.func, + onSelect: PropTypes.func, + activeAddress: PropTypes.string, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + getListItemIcon (accountAddress, activeAddress) { + return accountAddress === activeAddress + ? + : null + } + + render () { + const { + accounts, + closeDropdown, + onSelect, + activeAddress, + } = this.props + + return (
+
closeDropdown()} + /> +
+ {accounts.map((account, index) => { + onSelect(account) + closeDropdown() + }} + icon={this.getListItemIcon(account.address, activeAddress)} + key={`send-dropdown-account-#${index}`} + />)} +
+
) + } + +} diff --git a/ui/app/pages/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js b/ui/app/pages/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js new file mode 100644 index 000000000..b92dd4dfe --- /dev/null +++ b/ui/app/pages/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js @@ -0,0 +1,105 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import SendDropdownList from '../send-dropdown-list.component.js' + +import AccountListItem from '../../../account-list-item/account-list-item.container' + +const propsMethodSpies = { + closeDropdown: sinon.spy(), + onSelect: sinon.spy(), +} + +sinon.spy(SendDropdownList.prototype, 'getListItemIcon') + +describe('SendDropdownList Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t' } }) + }) + + afterEach(() => { + propsMethodSpies.closeDropdown.resetHistory() + propsMethodSpies.onSelect.resetHistory() + SendDropdownList.prototype.getListItemIcon.resetHistory() + }) + + describe('getListItemIcon', () => { + it('should return check icon if the passed addresses are the same', () => { + assert.deepEqual( + wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount0'), + + ) + }) + + it('should return null if the passed addresses are different', () => { + assert.equal( + wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount1'), + null + ) + }) + }) + + describe('render', () => { + it('should render a single div with two children', () => { + assert(wrapper.is('div')) + assert.equal(wrapper.children().length, 2) + }) + + it('should render the children with the correct classes', () => { + assert(wrapper.childAt(0).hasClass('send-v2__from-dropdown__close-area')) + assert(wrapper.childAt(1).hasClass('send-v2__from-dropdown__list')) + }) + + it('should call closeDropdown onClick of the send-v2__from-dropdown__close-area', () => { + assert.equal(propsMethodSpies.closeDropdown.callCount, 0) + wrapper.childAt(0).props().onClick() + assert.equal(propsMethodSpies.closeDropdown.callCount, 1) + }) + + it('should render an AccountListItem for each item in accounts', () => { + assert.equal(wrapper.childAt(1).children().length, 3) + assert(wrapper.childAt(1).children().every(AccountListItem)) + }) + + it('should pass the correct props to the AccountListItem', () => { + wrapper.childAt(1).children().forEach((accountListItem, index) => { + const { + account, + className, + handleClick, + } = accountListItem.props() + assert.deepEqual(account, { address: 'mockAccount' + index }) + assert.equal(className, 'account-list-item__dropdown') + assert.equal(propsMethodSpies.onSelect.callCount, 0) + handleClick() + assert.equal(propsMethodSpies.onSelect.callCount, 1) + assert.deepEqual(propsMethodSpies.onSelect.getCall(0).args[0], { address: 'mockAccount' + index }) + propsMethodSpies.onSelect.resetHistory() + propsMethodSpies.closeDropdown.resetHistory() + assert.equal(propsMethodSpies.closeDropdown.callCount, 0) + handleClick() + assert.equal(propsMethodSpies.closeDropdown.callCount, 1) + propsMethodSpies.onSelect.resetHistory() + propsMethodSpies.closeDropdown.resetHistory() + }) + }) + + it('should call this.getListItemIcon for each AccountListItem', () => { + assert.equal(SendDropdownList.prototype.getListItemIcon.callCount, 3) + const getListItemIconCalls = SendDropdownList.prototype.getListItemIcon.getCalls() + assert(getListItemIconCalls.every(({ args }, index) => args[0] === 'mockAccount' + index)) + }) + }) +}) diff --git a/ui/app/pages/send/send-content/send-from-row/index.js b/ui/app/pages/send/send-content/send-from-row/index.js new file mode 100644 index 000000000..0a79726b2 --- /dev/null +++ b/ui/app/pages/send/send-content/send-from-row/index.js @@ -0,0 +1 @@ +export { default } from './send-from-row.container' diff --git a/ui/app/pages/send/send-content/send-from-row/send-from-row.component.js b/ui/app/pages/send/send-content/send-from-row/send-from-row.component.js new file mode 100644 index 000000000..dfa53e970 --- /dev/null +++ b/ui/app/pages/send/send-content/send-from-row/send-from-row.component.js @@ -0,0 +1,27 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper' +import AccountListItem from '../../account-list-item' + +export default class SendFromRow extends Component { + static propTypes = { + from: PropTypes.object, + } + + static contextTypes = { + t: PropTypes.func, + } + + render () { + const { t } = this.context + const { from } = this.props + + return ( + +
+ +
+
+ ) + } +} diff --git a/ui/app/pages/send/send-content/send-from-row/send-from-row.container.js b/ui/app/pages/send/send-content/send-from-row/send-from-row.container.js new file mode 100644 index 000000000..fe3ac9aa1 --- /dev/null +++ b/ui/app/pages/send/send-content/send-from-row/send-from-row.container.js @@ -0,0 +1,11 @@ +import { connect } from 'react-redux' +import { getSendFromObject } from '../../send.selectors.js' +import SendFromRow from './send-from-row.component' + +function mapStateToProps (state) { + return { + from: getSendFromObject(state), + } +} + +export default connect(mapStateToProps)(SendFromRow) diff --git a/ui/app/pages/send/send-content/send-from-row/send-from-row.selectors.js b/ui/app/pages/send/send-content/send-from-row/send-from-row.selectors.js new file mode 100644 index 000000000..03ef4806b --- /dev/null +++ b/ui/app/pages/send/send-content/send-from-row/send-from-row.selectors.js @@ -0,0 +1,9 @@ +const selectors = { + getFromDropdownOpen, +} + +module.exports = selectors + +function getFromDropdownOpen (state) { + return state.send.fromDropdownOpen +} diff --git a/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-component.test.js new file mode 100644 index 000000000..18811c57e --- /dev/null +++ b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-component.test.js @@ -0,0 +1,31 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import SendFromRow from '../send-from-row.component.js' +import AccountListItem from '../../../account-list-item' +import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' + +describe('SendFromRow Component', function () { + describe('render', () => { + const wrapper = shallow( + , + { context: { t: str => str + '_t' } } + ) + + it('should render a SendRowWrapper component', () => { + assert.equal(wrapper.find(SendRowWrapper).length, 1) + }) + + it('should pass the correct props to SendRowWrapper', () => { + const { label } = wrapper.find(SendRowWrapper).props() + assert.equal(label, 'from_t:') + }) + + it('should render the FromDropdown with the correct props', () => { + const { account } = wrapper.find(AccountListItem).props() + assert.deepEqual(account, { address: 'mockAddress' }) + }) + }) +}) diff --git a/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-container.test.js new file mode 100644 index 000000000..fd771ea77 --- /dev/null +++ b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-container.test.js @@ -0,0 +1,26 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../send-from-row.container.js', { + 'react-redux': { + connect: ms => { + mapStateToProps = ms + return () => ({}) + }, + }, + '../../send.selectors.js': { + getSendFromObject: (s) => `mockFrom:${s}`, + }, +}) + +describe('send-from-row container', () => { + describe('mapStateToProps()', () => { + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + from: 'mockFrom:mockState', + }) + }) + }) +}) diff --git a/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-selectors.test.js b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-selectors.test.js new file mode 100644 index 000000000..ecb57bbc3 --- /dev/null +++ b/ui/app/pages/send/send-content/send-from-row/tests/send-from-row-selectors.test.js @@ -0,0 +1,20 @@ +import assert from 'assert' +import { + getFromDropdownOpen, +} from '../send-from-row.selectors.js' + +describe('send-from-row selectors', () => { + + describe('getFromDropdownOpen()', () => { + it('should get send.fromDropdownOpen', () => { + const state = { + send: { + fromDropdownOpen: null, + }, + } + + assert.equal(getFromDropdownOpen(state), null) + }) + }) + +}) diff --git a/ui/app/pages/send/send-content/send-gas-row/README.md b/ui/app/pages/send/send-content/send-gas-row/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js new file mode 100644 index 000000000..3f5587318 --- /dev/null +++ b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js @@ -0,0 +1,57 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import UserPreferencedCurrencyDisplay from '../../../../../components/app/user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../../../helpers/constants/common' + +export default class GasFeeDisplay extends Component { + + static propTypes = { + conversionRate: PropTypes.number, + primaryCurrency: PropTypes.string, + convertedCurrency: PropTypes.string, + gasLoadingError: PropTypes.bool, + gasTotal: PropTypes.string, + onReset: PropTypes.func, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + render () { + const { gasTotal, gasLoadingError, onReset } = this.props + + return ( +
+ {gasTotal + ? ( +
+ + +
+ ) + : gasLoadingError + ?
+ {this.context.t('setGasPrice')} +
+ :
+ {this.context.t('loading')} +
+ } + +
+ ) + } +} diff --git a/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/index.js b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/index.js new file mode 100644 index 000000000..dba0edb7b --- /dev/null +++ b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/index.js @@ -0,0 +1 @@ +export { default } from './gas-fee-display.component' diff --git a/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js new file mode 100644 index 000000000..eedd43221 --- /dev/null +++ b/ui/app/pages/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js @@ -0,0 +1,61 @@ +import React from 'react' +import assert from 'assert' +import {shallow} from 'enzyme' +import GasFeeDisplay from '../gas-fee-display.component' +import UserPreferencedCurrencyDisplay from '../../../../../../components/app/user-preferenced-currency-display' +import sinon from 'sinon' + + +const propsMethodSpies = { + showCustomizeGasModal: sinon.spy(), + onReset: sinon.spy(), +} + +describe('GasFeeDisplay Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, {context: {t: str => str + '_t'}}) + }) + + afterEach(() => { + propsMethodSpies.showCustomizeGasModal.resetHistory() + }) + + describe('render', () => { + it('should render a CurrencyDisplay component', () => { + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) + }) + + it('should render the CurrencyDisplay with the correct props', () => { + const { + type, + value, + } = wrapper.find(UserPreferencedCurrencyDisplay).at(0).props() + assert.equal(type, 'PRIMARY') + assert.equal(value, 'mockGasTotal') + }) + + it('should render the reset button with the correct props', () => { + const { + onClick, + className, + } = wrapper.find('button').props() + assert.equal(className, 'gas-fee-reset') + assert.equal(propsMethodSpies.onReset.callCount, 0) + onClick() + assert.equal(propsMethodSpies.onReset.callCount, 1) + }) + + it('should render the reset button with the correct text', () => { + assert.equal(wrapper.find('button').text(), 'reset_t') + }) + }) +}) diff --git a/ui/app/pages/send/send-content/send-gas-row/index.js b/ui/app/pages/send/send-content/send-gas-row/index.js new file mode 100644 index 000000000..3c7ff1d5f --- /dev/null +++ b/ui/app/pages/send/send-content/send-gas-row/index.js @@ -0,0 +1 @@ +export { default } from './send-gas-row.container' diff --git a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js new file mode 100644 index 000000000..1b850ac57 --- /dev/null +++ b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.component.js @@ -0,0 +1,131 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper' +import GasFeeDisplay from './gas-fee-display/gas-fee-display.component' +import GasPriceButtonGroup from '../../../../components/app/gas-customization/gas-price-button-group' +import AdvancedGasInputs from '../../../../components/app/gas-customization/advanced-gas-inputs' + +export default class SendGasRow extends Component { + + static propTypes = { + conversionRate: PropTypes.number, + convertedCurrency: PropTypes.string, + gasFeeError: PropTypes.bool, + gasLoadingError: PropTypes.bool, + gasTotal: PropTypes.string, + showCustomizeGasModal: PropTypes.func, + setGasPrice: PropTypes.func, + setGasLimit: PropTypes.func, + gasPriceButtonGroupProps: PropTypes.object, + gasButtonGroupShown: PropTypes.bool, + advancedInlineGasShown: PropTypes.bool, + resetGasButtons: PropTypes.func, + gasPrice: PropTypes.string, + gasLimit: PropTypes.string, + insufficientBalance: PropTypes.bool, + } + + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + }; + + renderAdvancedOptionsButton () { + const { metricsEvent } = this.context + const { showCustomizeGasModal } = this.props + return
{ + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Clicked "Advanced Options"', + }, + }) + showCustomizeGasModal() + }}> + { this.context.t('advancedOptions') } +
+ } + + renderContent () { + const { + conversionRate, + convertedCurrency, + gasLoadingError, + gasTotal, + showCustomizeGasModal, + gasPriceButtonGroupProps, + gasButtonGroupShown, + advancedInlineGasShown, + resetGasButtons, + setGasPrice, + setGasLimit, + gasPrice, + gasLimit, + insufficientBalance, + } = this.props + const { metricsEvent } = this.context + + const gasPriceButtonGroup =
+ { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Changed Gas Button', + }, + }) + gasPriceButtonGroupProps.handleGasPriceSelection(...args) + }} + /> + { this.renderAdvancedOptionsButton() } +
+ const gasFeeDisplay = showCustomizeGasModal()} + /> + const advancedGasInputs =
+ setGasPrice(newGasPrice, gasLimit)} + updateCustomGasLimit={newGasLimit => setGasLimit(newGasLimit, gasPrice)} + customGasPrice={gasPrice} + customGasLimit={gasLimit} + insufficientBalance={insufficientBalance} + customPriceIsSafe={true} + isSpeedUp={false} + /> + { this.renderAdvancedOptionsButton() } +
+ + if (advancedInlineGasShown) { + return advancedGasInputs + } else if (gasButtonGroupShown) { + return gasPriceButtonGroup + } else { + return gasFeeDisplay + } + } + + render () { + const { gasFeeError } = this.props + + return ( + + { this.renderContent() } + + ) + } + +} diff --git a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js new file mode 100644 index 000000000..c4daa98af --- /dev/null +++ b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.container.js @@ -0,0 +1,118 @@ +import { connect } from 'react-redux' +import { + getConversionRate, + getCurrentCurrency, + getGasTotal, + getGasPrice, + getGasLimit, + getSendAmount, +} from '../../send.selectors.js' +import { + isBalanceSufficient, + calcGasTotal, +} from '../../send.utils.js' +import { + getBasicGasEstimateLoadingStatus, + getRenderableEstimateDataForSmallButtonsFromGWEI, + getDefaultActiveButtonIndex, +} from '../../../../selectors/custom-gas' +import { + showGasButtonGroup, +} from '../../../../ducks/send/send.duck' +import { + resetCustomData, + setCustomGasPrice, + setCustomGasLimit, +} from '../../../../ducks/gas/gas.duck' +import { getGasLoadingError, gasFeeIsInError, getGasButtonGroupShown } from './send-gas-row.selectors.js' +import { showModal, setGasPrice, setGasLimit, setGasTotal } from '../../../../store/actions' +import { getAdvancedInlineGasShown, getCurrentEthBalance, getSelectedToken } from '../../../../selectors/selectors' +import SendGasRow from './send-gas-row.component' + +export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(SendGasRow) + +function mapStateToProps (state) { + const gasButtonInfo = getRenderableEstimateDataForSmallButtonsFromGWEI(state) + const gasPrice = getGasPrice(state) + const gasLimit = getGasLimit(state) + const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, gasPrice) + + const gasTotal = getGasTotal(state) + const conversionRate = getConversionRate(state) + const balance = getCurrentEthBalance(state) + + const insufficientBalance = !isBalanceSufficient({ + amount: getSelectedToken(state) ? '0x0' : getSendAmount(state), + gasTotal, + balance, + conversionRate, + }) + + return { + conversionRate, + convertedCurrency: getCurrentCurrency(state), + gasTotal, + gasFeeError: gasFeeIsInError(state), + gasLoadingError: getGasLoadingError(state), + gasPriceButtonGroupProps: { + buttonDataLoading: getBasicGasEstimateLoadingStatus(state), + defaultActiveButtonIndex: 1, + newActiveButtonIndex: activeButtonIndex > -1 ? activeButtonIndex : null, + gasButtonInfo, + }, + gasButtonGroupShown: getGasButtonGroupShown(state), + advancedInlineGasShown: getAdvancedInlineGasShown(state), + gasPrice, + gasLimit, + insufficientBalance, + } +} + +function mapDispatchToProps (dispatch) { + return { + showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS', hideBasic: true })), + setGasPrice: (newPrice, gasLimit) => { + dispatch(setGasPrice(newPrice)) + dispatch(setCustomGasPrice(newPrice)) + if (gasLimit) { + dispatch(setGasTotal(calcGasTotal(gasLimit, newPrice))) + } + }, + setGasLimit: (newLimit, gasPrice) => { + dispatch(setGasLimit(newLimit)) + dispatch(setCustomGasLimit(newLimit)) + if (gasPrice) { + dispatch(setGasTotal(calcGasTotal(newLimit, gasPrice))) + } + }, + showGasButtonGroup: () => dispatch(showGasButtonGroup()), + resetCustomData: () => dispatch(resetCustomData()), + } +} + +function mergeProps (stateProps, dispatchProps, ownProps) { + const { gasPriceButtonGroupProps } = stateProps + const { gasButtonInfo } = gasPriceButtonGroupProps + const { + setGasPrice: dispatchSetGasPrice, + showGasButtonGroup: dispatchShowGasButtonGroup, + resetCustomData: dispatchResetCustomData, + ...otherDispatchProps + } = dispatchProps + + return { + ...stateProps, + ...otherDispatchProps, + ...ownProps, + gasPriceButtonGroupProps: { + ...gasPriceButtonGroupProps, + handleGasPriceSelection: dispatchSetGasPrice, + }, + resetGasButtons: () => { + dispatchResetCustomData() + dispatchSetGasPrice(gasButtonInfo[1].priceInHexWei) + dispatchShowGasButtonGroup() + }, + setGasPrice: dispatchSetGasPrice, + } +} diff --git a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.scss b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/pages/send/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.selectors.js new file mode 100644 index 000000000..79c838543 --- /dev/null +++ b/ui/app/pages/send/send-content/send-gas-row/send-gas-row.selectors.js @@ -0,0 +1,19 @@ +const selectors = { + gasFeeIsInError, + getGasLoadingError, + getGasButtonGroupShown, +} + +module.exports = selectors + +function getGasLoadingError (state) { + return state.send.errors.gasLoading +} + +function gasFeeIsInError (state) { + return Boolean(state.send.errors.gasFee) +} + +function getGasButtonGroupShown (state) { + return state.send.gasButtonGroupShown +} diff --git a/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-component.test.js new file mode 100644 index 000000000..0cbc92621 --- /dev/null +++ b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-component.test.js @@ -0,0 +1,104 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import sinon from 'sinon' +import SendGasRow from '../send-gas-row.component.js' + +import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' +import GasFeeDisplay from '../gas-fee-display/gas-fee-display.component' +import GasPriceButtonGroup from '../../../../../components/app/gas-customization/gas-price-button-group' + +const propsMethodSpies = { + showCustomizeGasModal: sinon.spy(), + resetGasButtons: sinon.spy(), +} + +describe('SendGasRow Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(, { context: { t: str => str + '_t', metricsEvent: () => ({}) } }) + }) + + afterEach(() => { + propsMethodSpies.resetGasButtons.resetHistory() + }) + + describe('render', () => { + it('should render a SendRowWrapper component', () => { + assert.equal(wrapper.find(SendRowWrapper).length, 1) + }) + + it('should pass the correct props to SendRowWrapper', () => { + const { + label, + showError, + errorType, + } = wrapper.find(SendRowWrapper).props() + + assert.equal(label, 'transactionFee_t:') + assert.equal(showError, 'mockGasFeeError') + assert.equal(errorType, 'gasFee') + }) + + it('should render a GasFeeDisplay as a child of the SendRowWrapper', () => { + assert(wrapper.find(SendRowWrapper).childAt(0).is(GasFeeDisplay)) + }) + + it('should render the GasFeeDisplay with the correct props', () => { + const { + conversionRate, + convertedCurrency, + gasLoadingError, + gasTotal, + onReset, + } = wrapper.find(SendRowWrapper).childAt(0).props() + assert.equal(conversionRate, 20) + assert.equal(convertedCurrency, 'mockConvertedCurrency') + assert.equal(gasLoadingError, false) + assert.equal(gasTotal, 'mockGasTotal') + assert.equal(propsMethodSpies.resetGasButtons.callCount, 0) + onReset() + assert.equal(propsMethodSpies.resetGasButtons.callCount, 1) + }) + + it('should render the GasPriceButtonGroup if gasButtonGroupShown is true', () => { + wrapper.setProps({ gasButtonGroupShown: true }) + const rendered = wrapper.find(SendRowWrapper).childAt(0) + assert.equal(rendered.children().length, 2) + + const gasPriceButtonGroup = rendered.childAt(0) + assert(gasPriceButtonGroup.is(GasPriceButtonGroup)) + assert(gasPriceButtonGroup.hasClass('gas-price-button-group--small')) + assert.equal(gasPriceButtonGroup.props().showCheck, false) + assert.equal(gasPriceButtonGroup.props().someGasPriceButtonGroupProp, 'foo') + assert.equal(gasPriceButtonGroup.props().anotherGasPriceButtonGroupProp, 'bar') + }) + + it('should render an advanced options button if gasButtonGroupShown is true', () => { + wrapper.setProps({ gasButtonGroupShown: true }) + const rendered = wrapper.find(SendRowWrapper).childAt(0) + assert.equal(rendered.children().length, 2) + + const advancedOptionsButton = rendered.childAt(1) + assert.equal(advancedOptionsButton.text(), 'advancedOptions_t') + + assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) + advancedOptionsButton.props().onClick() + assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) + }) + }) +}) diff --git a/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js new file mode 100644 index 000000000..ddc6ea985 --- /dev/null +++ b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-container.test.js @@ -0,0 +1,200 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps +let mergeProps + +const actionSpies = { + showModal: sinon.spy(), + setGasPrice: sinon.spy(), + setGasTotal: sinon.spy(), + setGasLimit: sinon.spy(), +} + +const sendDuckSpies = { + showGasButtonGroup: sinon.spy(), +} + +const gasDuckSpies = { + resetCustomData: sinon.spy(), + setCustomGasPrice: sinon.spy(), + setCustomGasLimit: sinon.spy(), +} + +proxyquire('../send-gas-row.container.js', { + 'react-redux': { + connect: (ms, md, mp) => { + mapStateToProps = ms + mapDispatchToProps = md + mergeProps = mp + return () => ({}) + }, + }, + '../../../../selectors/selectors': { + getCurrentEthBalance: (s) => `mockCurrentEthBalance:${s}`, + getAdvancedInlineGasShown: (s) => `mockAdvancedInlineGasShown:${s}`, + getSelectedToken: () => false, + }, + '../../send.selectors.js': { + getConversionRate: (s) => `mockConversionRate:${s}`, + getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, + getGasTotal: (s) => `mockGasTotal:${s}`, + getGasPrice: (s) => `mockGasPrice:${s}`, + getGasLimit: (s) => `mockGasLimit:${s}`, + getSendAmount: (s) => `mockSendAmount:${s}`, + }, + '../../send.utils.js': { + isBalanceSufficient: ({ + amount, + gasTotal, + balance, + conversionRate, + }) => `${amount}:${gasTotal}:${balance}:${conversionRate}`, + calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice, + }, + './send-gas-row.selectors.js': { + getGasLoadingError: (s) => `mockGasLoadingError:${s}`, + gasFeeIsInError: (s) => `mockGasFeeError:${s}`, + getGasButtonGroupShown: (s) => `mockGetGasButtonGroupShown:${s}`, + }, + '../../../../store/actions': actionSpies, + '../../../../selectors/custom-gas': { + getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${s}`, + getRenderableEstimateDataForSmallButtonsFromGWEI: (s) => `mockGasButtonInfo:${s}`, + getDefaultActiveButtonIndex: (gasButtonInfo, gasPrice) => gasButtonInfo.length + gasPrice.length, + }, + '../../../../ducks/send/send.duck': sendDuckSpies, + '../../../../ducks/gas/gas.duck': gasDuckSpies, +}) + +describe('send-gas-row container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState'), { + conversionRate: 'mockConversionRate:mockState', + convertedCurrency: 'mockConvertedCurrency:mockState', + gasTotal: 'mockGasTotal:mockState', + gasFeeError: 'mockGasFeeError:mockState', + gasLoadingError: 'mockGasLoadingError:mockState', + gasPriceButtonGroupProps: { + buttonDataLoading: `mockBasicGasEstimateLoadingStatus:mockState`, + defaultActiveButtonIndex: 1, + newActiveButtonIndex: 49, + gasButtonInfo: `mockGasButtonInfo:mockState`, + }, + gasButtonGroupShown: `mockGetGasButtonGroupShown:mockState`, + advancedInlineGasShown: 'mockAdvancedInlineGasShown:mockState', + gasLimit: 'mockGasLimit:mockState', + gasPrice: 'mockGasPrice:mockState', + insufficientBalance: false, + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + actionSpies.setGasTotal.resetHistory() + }) + + describe('showCustomizeGasModal()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.showCustomizeGasModal() + assert(dispatchSpy.calledOnce) + assert.deepEqual( + actionSpies.showModal.getCall(0).args[0], + { name: 'CUSTOMIZE_GAS', hideBasic: true } + ) + }) + }) + + describe('setGasPrice()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.setGasPrice('mockNewPrice', 'mockLimit') + assert(dispatchSpy.calledThrice) + assert(actionSpies.setGasPrice.calledOnce) + assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'mockNewPrice') + assert.equal(gasDuckSpies.setCustomGasPrice.getCall(0).args[0], 'mockNewPrice') + assert(actionSpies.setGasTotal.calledOnce) + assert.equal(actionSpies.setGasTotal.getCall(0).args[0], 'mockLimitmockNewPrice') + }) + }) + + describe('setGasLimit()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.setGasLimit('mockNewLimit', 'mockPrice') + assert(dispatchSpy.calledThrice) + assert(actionSpies.setGasLimit.calledOnce) + assert.equal(actionSpies.setGasLimit.getCall(0).args[0], 'mockNewLimit') + assert.equal(gasDuckSpies.setCustomGasLimit.getCall(0).args[0], 'mockNewLimit') + assert(actionSpies.setGasTotal.calledOnce) + assert.equal(actionSpies.setGasTotal.getCall(0).args[0], 'mockNewLimitmockPrice') + }) + }) + + describe('showGasButtonGroup()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.showGasButtonGroup() + assert(dispatchSpy.calledOnce) + assert(sendDuckSpies.showGasButtonGroup.calledOnce) + }) + }) + + describe('resetCustomData()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.resetCustomData() + assert(dispatchSpy.calledOnce) + assert(gasDuckSpies.resetCustomData.calledOnce) + }) + }) + + }) + + describe('mergeProps', () => { + let stateProps + let dispatchProps + let ownProps + + beforeEach(() => { + stateProps = { + gasPriceButtonGroupProps: { + someGasPriceButtonGroupProp: 'foo', + anotherGasPriceButtonGroupProp: 'bar', + }, + someOtherStateProp: 'baz', + } + dispatchProps = { + setGasPrice: sinon.spy(), + someOtherDispatchProp: sinon.spy(), + } + ownProps = { someOwnProp: 123 } + }) + + it('should return the expected props when isConfirm is true', () => { + const result = mergeProps(stateProps, dispatchProps, ownProps) + + assert.equal(result.someOtherStateProp, 'baz') + assert.equal(result.gasPriceButtonGroupProps.someGasPriceButtonGroupProp, 'foo') + assert.equal(result.gasPriceButtonGroupProps.anotherGasPriceButtonGroupProp, 'bar') + assert.equal(result.someOwnProp, 123) + + assert.equal(dispatchProps.setGasPrice.callCount, 0) + result.gasPriceButtonGroupProps.handleGasPriceSelection() + assert.equal(dispatchProps.setGasPrice.callCount, 1) + + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0) + result.someOtherDispatchProp() + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1) + }) + }) + +}) diff --git a/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js new file mode 100644 index 000000000..bd3c9a257 --- /dev/null +++ b/ui/app/pages/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js @@ -0,0 +1,62 @@ +import assert from 'assert' +import { + gasFeeIsInError, + getGasLoadingError, + getGasButtonGroupShown, +} from '../send-gas-row.selectors.js' + +describe('send-gas-row selectors', () => { + + describe('getGasLoadingError()', () => { + it('should return send.errors.gasLoading', () => { + const state = { + send: { + errors: { + gasLoading: 'abc', + }, + }, + } + + assert.equal(getGasLoadingError(state), 'abc') + }) + }) + + describe('gasFeeIsInError()', () => { + it('should return true if send.errors.gasFee is truthy', () => { + const state = { + send: { + errors: { + gasFee: 'def', + }, + }, + } + + assert.equal(gasFeeIsInError(state), true) + }) + + it('should return false send.errors.gasFee is falsely', () => { + const state = { + send: { + errors: { + gasFee: null, + }, + }, + } + + assert.equal(gasFeeIsInError(state), false) + }) + }) + + describe('getGasButtonGroupShown()', () => { + it('should return send.gasButtonGroupShown', () => { + const state = { + send: { + gasButtonGroupShown: 'foobar', + }, + } + + assert.equal(getGasButtonGroupShown(state), 'foobar') + }) + }) + +}) diff --git a/ui/app/pages/send/send-content/send-hex-data-row/index.js b/ui/app/pages/send/send-content/send-hex-data-row/index.js new file mode 100644 index 000000000..08c341067 --- /dev/null +++ b/ui/app/pages/send/send-content/send-hex-data-row/index.js @@ -0,0 +1 @@ +export { default } from './send-hex-data-row.container' diff --git a/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js new file mode 100644 index 000000000..62a74a77b --- /dev/null +++ b/ui/app/pages/send/send-content/send-hex-data-row/send-hex-data-row.component.js @@ -0,0 +1,42 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper' + +export default class SendHexDataRow extends Component { + static propTypes = { + data: PropTypes.string, + inError: PropTypes.bool, + updateSendHexData: PropTypes.func.isRequired, + updateGas: PropTypes.func.isRequired, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + onInput = (event) => { + const {updateSendHexData, updateGas} = this.props + const data = event.target.value.replace(/\n/g, '') || null + updateSendHexData(data) + updateGas({ data }) + } + + render () { + const {inError} = this.props + const {t} = this.context + + return ( + +