diff options
Diffstat (limited to 'ui/app/components')
616 files changed, 11128 insertions, 9260 deletions
diff --git a/ui/app/components/account-export.js b/ui/app/components/account-export.js deleted file mode 100644 index 865207487..000000000 --- a/ui/app/components/account-export.js +++ /dev/null @@ -1,138 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const PropTypes = require('prop-types') -const inherits = require('util').inherits -const exportAsFile = require('../util').exportAsFile -const copyToClipboard = require('copy-to-clipboard') -const actions = require('../actions') -const ethUtil = require('ethereumjs-util') -const connect = require('react-redux').connect - -ExportAccountView.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps)(ExportAccountView) - - -inherits(ExportAccountView, Component) -function ExportAccountView () { - Component.call(this) -} - -function mapStateToProps (state) { - return { - warning: state.appState.warning, - } -} - -ExportAccountView.prototype.render = function () { - const state = this.props - const accountDetail = state.accountDetail - const nickname = state.identities[state.address].name - - if (!accountDetail) return h('div') - const accountExport = accountDetail.accountExport - - const notExporting = accountExport === 'none' - const exportRequested = accountExport === 'requested' - const accountExported = accountExport === 'completed' - - if (notExporting) return h('div') - - if (exportRequested) { - const warning = this.context.t('exportPrivateKeyWarning') - return ( - h('div', { - style: { - display: 'inline-block', - textAlign: 'center', - }, - }, - [ - h('div', { - key: 'exporting', - style: { - margin: '0 20px', - }, - }, [ - h('p.error', warning), - h('input#exportAccount.sizing-input', { - type: 'password', - placeholder: this.context.t('confirmPassword').toLowerCase(), - onKeyPress: this.onExportKeyPress.bind(this), - style: { - position: 'relative', - top: '1.5px', - marginBottom: '7px', - }, - }), - ]), - h('div', { - key: 'buttons', - style: { - margin: '0 20px', - }, - }, - [ - h('button', { - onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), - style: { - marginRight: '10px', - }, - }, this.context.t('submit')), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, this.context.t('cancel')), - ]), - (this.props.warning) && ( - h('span.error', { - style: { - margin: '20px', - }, - }, this.props.warning.split('-')) - ), - ]) - ) - } - - if (accountExported) { - const plainKey = ethUtil.stripHexPrefix(accountDetail.privateKey) - - return h('div.privateKey', { - style: { - margin: '0 20px', - }, - }, [ - h('label', this.context.t('copyPrivateKey') + ':'), - h('p.error.cursor-pointer', { - style: { - textOverflow: 'ellipsis', - overflow: 'hidden', - webkitUserSelect: 'text', - maxWidth: '275px', - }, - onClick: function (event) { - copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) - }, - }, plainKey), - h('button', { - onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), - }, this.context.t('done')), - h('button', { - style: { - marginLeft: '10px', - }, - onClick: () => exportAsFile(`MetaMask ${nickname} Private Key`, plainKey), - }, this.context.t('saveAsFile')), - ]) - } -} - -ExportAccountView.prototype.onExportKeyPress = function (event) { - if (event.key !== 'Enter') return - event.preventDefault() - - const input = document.getElementById('exportAccount').value - this.props.dispatch(actions.exportAccount(input, this.props.address)) -} diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js deleted file mode 100644 index 94eae8d07..000000000 --- a/ui/app/components/account-menu/index.js +++ /dev/null @@ -1,248 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const connect = require('react-redux').connect -const { compose } = require('recompose') -const { withRouter } = require('react-router-dom') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const actions = require('../../actions') -const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') -const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') -const { getEnvironmentType } = require('../../../../app/scripts/lib/util') -const Tooltip = require('../tooltip') -import Identicon from '../identicon' -import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' -import { PRIMARY } from '../../constants/common' - -const { - SETTINGS_ROUTE, - INFO_ROUTE, - NEW_ACCOUNT_ROUTE, - IMPORT_ACCOUNT_ROUTE, - CONNECT_HARDWARE_ROUTE, - DEFAULT_ROUTE, -} = require('../../routes') - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(AccountMenu) - -AccountMenu.contextTypes = { - t: PropTypes.func, -} - -inherits(AccountMenu, Component) -function AccountMenu () { Component.call(this) } - -function mapStateToProps (state) { - return { - selectedAddress: state.metamask.selectedAddress, - isAccountMenuOpen: state.metamask.isAccountMenuOpen, - keyrings: state.metamask.keyrings, - identities: state.metamask.identities, - accounts: state.metamask.accounts, - } -} - -function mapDispatchToProps (dispatch) { - return { - toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()), - showAccountDetail: address => { - dispatch(actions.showAccountDetail(address)) - dispatch(actions.hideSidebar()) - dispatch(actions.toggleAccountMenu()) - }, - lockMetamask: () => { - dispatch(actions.lockMetamask()) - dispatch(actions.hideWarning()) - dispatch(actions.hideSidebar()) - dispatch(actions.toggleAccountMenu()) - }, - showConfigPage: () => { - dispatch(actions.showConfigPage()) - dispatch(actions.hideSidebar()) - dispatch(actions.toggleAccountMenu()) - }, - showInfoPage: () => { - dispatch(actions.showInfoPage()) - dispatch(actions.hideSidebar()) - dispatch(actions.toggleAccountMenu()) - }, - showRemoveAccountConfirmationModal: (identity) => { - return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity })) - }, - } -} - -AccountMenu.prototype.render = function () { - const { - isAccountMenuOpen, - toggleAccountMenu, - lockMetamask, - history, - } = this.props - - return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [ - h(CloseArea, { onClick: toggleAccountMenu }), - h(Item, { - className: 'account-menu__header', - }, [ - this.context.t('myAccounts'), - h('button.account-menu__logout-button', { - onClick: () => { - lockMetamask() - history.push(DEFAULT_ROUTE) - }, - }, this.context.t('logout')), - ]), - h(Divider), - h('div.account-menu__accounts', this.renderAccounts()), - h(Divider), - h(Item, { - onClick: () => { - toggleAccountMenu() - history.push(NEW_ACCOUNT_ROUTE) - }, - icon: h('img.account-menu__item-icon', { src: 'images/plus-btn-white.svg' }), - text: this.context.t('createAccount'), - }), - h(Item, { - onClick: () => { - toggleAccountMenu() - history.push(IMPORT_ACCOUNT_ROUTE) - }, - icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }), - text: this.context.t('importAccount'), - }), - h(Item, { - onClick: () => { - toggleAccountMenu() - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE) - } else { - history.push(CONNECT_HARDWARE_ROUTE) - } - }, - icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }), - text: this.context.t('connectHardwareWallet'), - }), - h(Divider), - h(Item, { - onClick: () => { - toggleAccountMenu() - history.push(INFO_ROUTE) - }, - icon: h('img', { src: 'images/mm-info-icon.svg' }), - text: this.context.t('infoHelp'), - }), - h(Item, { - onClick: () => { - toggleAccountMenu() - history.push(SETTINGS_ROUTE) - }, - icon: h('img.account-menu__item-icon', { src: 'images/settings.svg' }), - text: this.context.t('settings'), - }), - ]) -} - -AccountMenu.prototype.renderAccounts = function () { - const { - identities, - accounts, - selectedAddress, - keyrings, - showAccountDetail, - } = this.props - - const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), []) - return accountOrder.filter(address => !!identities[address]).map((address) => { - - const identity = identities[address] - const isSelected = identity.address === selectedAddress - - const balanceValue = accounts[address] ? accounts[address].balance : '' - const simpleAddress = identity.address.substring(2).toLowerCase() - - const keyring = keyrings.find((kr) => { - return kr.accounts.includes(simpleAddress) || - kr.accounts.includes(identity.address) - }) - - return h( - 'div.account-menu__account.menu__item--clickable', - { onClick: () => showAccountDetail(identity.address) }, - [ - h('div.account-menu__check-mark', [ - isSelected ? h('div.account-menu__check-mark-icon') : null, - ]), - - h( - Identicon, - { - address: identity.address, - diameter: 24, - }, - ), - - h('div.account-menu__account-info', [ - h('div.account-menu__name', identity.name || ''), - h(UserPreferencedCurrencyDisplay, { - className: 'account-menu__balance', - value: balanceValue, - type: PRIMARY, - }), - ]), - - this.renderKeyringType(keyring), - this.renderRemoveAccount(keyring, identity), - ], - ) - }) -} - -AccountMenu.prototype.renderRemoveAccount = function (keyring, identity) { - // Any account that's not from the HD wallet Keyring can be removed - const type = keyring.type - const isRemovable = type !== 'HD Key Tree' - if (isRemovable) { - return h(Tooltip, { - title: this.context.t('removeAccount'), - position: 'bottom', - }, [ - h('a.remove-account-icon', { - onClick: (e) => this.removeAccount(e, identity), - }, ''), - ]) - } - return null -} - -AccountMenu.prototype.removeAccount = function (e, identity) { - e.preventDefault() - e.stopPropagation() - const { showRemoveAccountConfirmationModal } = this.props - showRemoveAccountConfirmationModal(identity) -} - -AccountMenu.prototype.renderKeyringType = function (keyring) { - try { // Sometimes keyrings aren't loaded yet: - const type = keyring.type - let label - switch (type) { - case 'Trezor Hardware': - case 'Ledger Hardware': - label = this.context.t('hardware') - break - case 'Simple Key Pair': - label = this.context.t('imported') - break - default: - label = '' - } - - return label !== '' ? h('.keyring-label.allcaps', label) : null - - } catch (e) { return } -} diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/app/account-dropdowns.js index 06376e48b..e02d17e54 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/app/account-dropdowns.js @@ -1,15 +1,15 @@ const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') -const actions = require('../actions') +const actions = require('../../store/actions') const genAccountLink = require('etherscan-link').createAccountLink const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem const copyToClipboard = require('copy-to-clipboard') -const { checksumAddress } = require('../util') +const { checksumAddress } = require('../../helpers/utils/util') -import Identicon from './identicon' +import Identicon from '../ui/identicon' class AccountDropdowns extends Component { constructor (props) { @@ -233,6 +233,7 @@ class AccountDropdowns extends Component { } render () { + const { metricsEvent } = this.context const { style, enableAccountsSelector, enableAccountOptions } = this.props const { optionsMenuActive, accountSelectorActive } = this.state @@ -272,6 +273,17 @@ class AccountDropdowns extends Component { fontSize: '1.8em', }, onClick: (event) => { + metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'userClick', + name: 'accountsOpenedMenu', + }, + pageOpts: { + section: 'header', + component: 'accountDropdownIcon', + }, + }) event.stopPropagation() this.setState({ accountSelectorActive: false, @@ -318,6 +330,7 @@ const mapDispatchToProps = (dispatch) => { AccountDropdowns.contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } module.exports = { diff --git a/ui/app/components/app/account-menu/account-menu.component.js b/ui/app/components/app/account-menu/account-menu.component.js new file mode 100644 index 000000000..972ea492e --- /dev/null +++ b/ui/app/components/app/account-menu/account-menu.component.js @@ -0,0 +1,340 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import debounce from 'lodash.debounce' +import { Menu, Item, Divider, CloseArea } from '../dropdowns/components/menu' +import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' +import Tooltip from '../../ui/tooltip' +import Identicon from '../../ui/identicon' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' +import { PRIMARY } from '../../../helpers/constants/common' +import { + SETTINGS_ROUTE, + INFO_ROUTE, + NEW_ACCOUNT_ROUTE, + IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, + DEFAULT_ROUTE, +} from '../../../helpers/constants/routes' + +export default class AccountMenu extends PureComponent { + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + static propTypes = { + accounts: PropTypes.object, + history: PropTypes.object, + identities: PropTypes.object, + isAccountMenuOpen: PropTypes.bool, + prevIsAccountMenuOpen: PropTypes.bool, + keyrings: PropTypes.array, + lockMetamask: PropTypes.func, + selectedAddress: PropTypes.string, + showAccountDetail: PropTypes.func, + showRemoveAccountConfirmationModal: PropTypes.func, + toggleAccountMenu: PropTypes.func, + } + + state = { + atAccountListBottom: false, + } + + componentDidUpdate (prevProps) { + const { prevIsAccountMenuOpen } = prevProps + const { isAccountMenuOpen } = this.props + + if (!prevIsAccountMenuOpen && isAccountMenuOpen) { + this.setAtAccountListBottom() + } + } + + renderAccounts () { + const { + identities, + accounts, + selectedAddress, + keyrings, + showAccountDetail, + } = this.props + + const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), []) + + return accountOrder.filter(address => !!identities[address]).map(address => { + const identity = identities[address] + const isSelected = identity.address === selectedAddress + + const balanceValue = accounts[address] ? accounts[address].balance : '' + const simpleAddress = identity.address.substring(2).toLowerCase() + + const keyring = keyrings.find(kr => { + return kr.accounts.includes(simpleAddress) || kr.accounts.includes(identity.address) + }) + + return ( + <div + className="account-menu__account menu__item--clickable" + onClick={() => { + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Main Menu', + name: 'Switched Account', + }, + }) + showAccountDetail(identity.address) + }} + key={identity.address} + > + <div className="account-menu__check-mark"> + { isSelected && <div className="account-menu__check-mark-icon" /> } + </div> + <Identicon + address={identity.address} + diameter={24} + /> + <div className="account-menu__account-info"> + <div className="account-menu__name"> + { identity.name || '' } + </div> + <UserPreferencedCurrencyDisplay + className="account-menu__balance" + value={balanceValue} + type={PRIMARY} + /> + </div> + { this.renderKeyringType(keyring) } + { this.renderRemoveAccount(keyring, identity) } + </div> + ) + }) + } + + renderRemoveAccount (keyring, identity) { + const { t } = this.context + // Any account that's not from the HD wallet Keyring can be removed + const { type } = keyring + const isRemovable = type !== 'HD Key Tree' + + return isRemovable && ( + <Tooltip + title={t('removeAccount')} + position="bottom" + > + <a + className="remove-account-icon" + onClick={e => this.removeAccount(e, identity)} + /> + </Tooltip> + ) + } + + removeAccount (e, identity) { + e.preventDefault() + e.stopPropagation() + const { showRemoveAccountConfirmationModal } = this.props + showRemoveAccountConfirmationModal(identity) + } + + renderKeyringType (keyring) { + const { t } = this.context + + // Sometimes keyrings aren't loaded yet + if (!keyring) { + return null + } + + const { type } = keyring + let label + + switch (type) { + case 'Trezor Hardware': + case 'Ledger Hardware': + label = t('hardware') + break + case 'Simple Key Pair': + label = t('imported') + break + } + + return label && ( + <div className="keyring-label allcaps"> + { label } + </div> + ) + } + + setAtAccountListBottom = () => { + const target = document.querySelector('.account-menu__accounts') + const { scrollTop, offsetHeight, scrollHeight } = target + const atAccountListBottom = scrollTop + offsetHeight >= scrollHeight + this.setState({ atAccountListBottom }) + } + + onScroll = debounce(this.setAtAccountListBottom, 25) + + handleScrollDown = e => { + e.stopPropagation() + const target = document.querySelector('.account-menu__accounts') + const { scrollHeight } = target + target.scroll({ left: 0, top: scrollHeight, behavior: 'smooth' }) + this.setAtAccountListBottom() + } + + renderScrollButton () { + const { accounts } = this.props + const { atAccountListBottom } = this.state + + return !atAccountListBottom && Object.keys(accounts).length > 3 && ( + <div + className="account-menu__scroll-button" + onClick={this.handleScrollDown} + > + <img + src="./images/icons/down-arrow.svg" + width={28} + height={28} + /> + </div> + ) + } + + render () { + const { t } = this.context + const { + isAccountMenuOpen, + toggleAccountMenu, + lockMetamask, + history, + } = this.props + const { metricsEvent } = this.context + + return ( + <Menu + className="account-menu" + isShowing={isAccountMenuOpen} + > + <CloseArea onClick={toggleAccountMenu} /> + <Item className="account-menu__header"> + { t('myAccounts') } + <button + className="account-menu__logout-button" + onClick={() => { + lockMetamask() + history.push(DEFAULT_ROUTE) + }} + > + { t('logout') } + </button> + </Item> + <Divider /> + <div className="account-menu__accounts-container"> + <div + className="account-menu__accounts" + onScroll={this.onScroll} + > + { this.renderAccounts() } + </div> + { this.renderScrollButton() } + </div> + <Divider /> + <Item + onClick={() => { + toggleAccountMenu() + metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Main Menu', + name: 'Clicked Create Account', + }, + }) + history.push(NEW_ACCOUNT_ROUTE) + }} + icon={ + <img + className="account-menu__item-icon" + src="images/plus-btn-white.svg" + /> + } + text={t('createAccount')} + /> + <Item + onClick={() => { + toggleAccountMenu() + metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Main Menu', + name: 'Clicked Import Account', + }, + }) + history.push(IMPORT_ACCOUNT_ROUTE) + }} + icon={ + <img + className="account-menu__item-icon" + src="images/import-account.svg" + /> + } + text={t('importAccount')} + /> + <Item + onClick={() => { + toggleAccountMenu() + metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Main Menu', + name: 'Clicked Connect Hardware', + }, + }) + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { + global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE) + } else { + history.push(CONNECT_HARDWARE_ROUTE) + } + }} + icon={ + <img + className="account-menu__item-icon" + src="images/connect-icon.svg" + /> + } + text={t('connectHardwareWallet')} + /> + <Divider /> + <Item + onClick={() => { + toggleAccountMenu() + history.push(INFO_ROUTE) + }} + icon={ + <img src="images/mm-info-icon.svg" /> + } + text={t('infoHelp')} + /> + <Item + onClick={() => { + toggleAccountMenu() + history.push(SETTINGS_ROUTE) + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Main Menu', + name: 'Opened Settings', + }, + }) + }} + icon={ + <img + className="account-menu__item-icon" + src="images/settings.svg" + /> + } + text={t('settings')} + /> + </Menu> + ) + } +} diff --git a/ui/app/components/app/account-menu/account-menu.container.js b/ui/app/components/app/account-menu/account-menu.container.js new file mode 100644 index 000000000..ae2e28e76 --- /dev/null +++ b/ui/app/components/app/account-menu/account-menu.container.js @@ -0,0 +1,62 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import { withRouter } from 'react-router-dom' +import { + toggleAccountMenu, + showAccountDetail, + hideSidebar, + lockMetamask, + hideWarning, + showConfigPage, + showInfoPage, + showModal, +} from '../../../store/actions' +import { getMetaMaskAccounts } from '../../../selectors/selectors' +import AccountMenu from './account-menu.component' + +function mapStateToProps (state) { + const { metamask: { selectedAddress, isAccountMenuOpen, keyrings, identities } } = state + + return { + selectedAddress, + isAccountMenuOpen, + keyrings, + identities, + accounts: getMetaMaskAccounts(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + toggleAccountMenu: () => dispatch(toggleAccountMenu()), + showAccountDetail: address => { + dispatch(showAccountDetail(address)) + dispatch(hideSidebar()) + dispatch(toggleAccountMenu()) + }, + lockMetamask: () => { + dispatch(lockMetamask()) + dispatch(hideWarning()) + dispatch(hideSidebar()) + dispatch(toggleAccountMenu()) + }, + showConfigPage: () => { + dispatch(showConfigPage()) + dispatch(hideSidebar()) + dispatch(toggleAccountMenu()) + }, + showInfoPage: () => { + dispatch(showInfoPage()) + dispatch(hideSidebar()) + dispatch(toggleAccountMenu()) + }, + showRemoveAccountConfirmationModal: identity => { + return dispatch(showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity })) + }, + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(AccountMenu) diff --git a/ui/app/components/app/account-menu/index.js b/ui/app/components/app/account-menu/index.js new file mode 100644 index 000000000..b2b4e4c6f --- /dev/null +++ b/ui/app/components/app/account-menu/index.js @@ -0,0 +1 @@ +export { default } from './account-menu.container' diff --git a/ui/app/components/app/account-menu/index.scss b/ui/app/components/app/account-menu/index.scss new file mode 100644 index 000000000..9a61bf887 --- /dev/null +++ b/ui/app/components/app/account-menu/index.scss @@ -0,0 +1,177 @@ +.account-menu { + position: fixed; + z-index: 100; + top: 58px; + width: 310px; + + @media screen and (max-width: 575px) { + right: calc(((100vw - 100%) / 2) + 8px); + } + + @media screen and (min-width: 576px) { + right: calc((100vw - 85vw) / 2); + } + + @media screen and (min-width: 769px) { + right: calc((100vw - 80vw) / 2); + } + + @media screen and (min-width: 1281px) { + right: calc((100vw - 65vw) / 2); + } + + &__icon { + margin-left: 20px; + cursor: pointer; + + &--disabled { + cursor: initial; + } + } + + &__header { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + align-items: center; + } + + &__logout-button { + border: 1px solid $dusty-gray; + background-color: transparent; + color: $white; + border-radius: 4px; + font-size: 12px; + line-height: 23px; + padding: 0 24px; + } + + &__item-icon { + width: 16px; + height: 16px; + } + + &__accounts { + display: flex; + flex-flow: column nowrap; + overflow-y: auto; + max-height: 256px; + position: relative; + z-index: 200; + + &::-webkit-scrollbar { + display: none; + } + + @media screen and (max-width: 575px) { + max-height: 228px; + } + + .keyring-label { + margin-top: 5px; + background-color: $dusty-gray; + color: $black; + font-weight: normal; + letter-spacing: .5px; + } + } + + &__account { + display: flex; + flex-flow: row nowrap; + padding: 16px 14px; + flex: 0 0 auto; + + @media screen and (max-width: 575px) { + padding: 12px 14px; + } + + .remove-account-icon { + width: 15px; + margin-left: 10px; + height: 15px; + } + + &:hover { + .remove-account-icon::after { + content: '\00D7'; + font-size: 25px; + color: $white; + cursor: pointer; + position: absolute; + margin-top: -5px; + } + } + } + + &__account-info { + flex: 1 0 auto; + display: flex; + flex-flow: column nowrap; + } + + &__check-mark { + width: 14px; + margin-right: 12px; + flex: 0 0 auto; + } + + &__check-mark-icon { + background-image: url("images/check-white.svg"); + height: 18px; + width: 18px; + background-repeat: no-repeat; + background-position: center; + background-size: contain; + margin: 3px 0; + } + + .identicon { + margin: 0 12px 0 0; + flex: 0 0 auto; + } + + &__name { + color: $white; + font-size: 18px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 200px; + } + + &__balance { + color: $dusty-gray; + font-size: 14px; + } + + &__action { + font-size: 16px; + line-height: 18px; + cursor: pointer; + } + + &__accounts-container { + position: relative; + } + + &__scroll-button { + position: absolute; + bottom: 12px; + right: 12px; + height: 28px; + width: 28px; + border-radius: 14px; + background: #3f3f3f; + z-index: 201; + cursor: pointer; + opacity: .8; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + opacity: 1; + } + } +} diff --git a/ui/app/components/account-panel.js b/ui/app/components/app/account-panel.js index a379ed3ac..79882f34a 100644 --- a/ui/app/components/account-panel.js +++ b/ui/app/components/app/account-panel.js @@ -1,9 +1,9 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') -import Identicon from './identicon' -const formatBalance = require('../util').formatBalance -const addressSummary = require('../util').addressSummary +import Identicon from '../ui/identicon' +const formatBalance = require('../../helpers/utils/util').formatBalance +const addressSummary = require('../../helpers/utils/util').addressSummary module.exports = AccountPanel diff --git a/ui/app/components/add-token-button/add-token-button.component.js b/ui/app/components/app/add-token-button/add-token-button.component.js index 10887aed8..10887aed8 100644 --- a/ui/app/components/add-token-button/add-token-button.component.js +++ b/ui/app/components/app/add-token-button/add-token-button.component.js diff --git a/ui/app/components/add-token-button/index.js b/ui/app/components/app/add-token-button/index.js index 15c4fe6ca..15c4fe6ca 100644 --- a/ui/app/components/add-token-button/index.js +++ b/ui/app/components/app/add-token-button/index.js diff --git a/ui/app/components/add-token-button/index.scss b/ui/app/components/app/add-token-button/index.scss index 39f404716..39f404716 100644 --- a/ui/app/components/add-token-button/index.scss +++ b/ui/app/components/app/add-token-button/index.scss diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app/app-header/app-header.component.js index c82dc1de9..343e0daab 100644 --- a/ui/app/components/app-header/app-header.component.js +++ b/ui/app/components/app/app-header/app-header.component.js @@ -1,20 +1,13 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { matchPath } from 'react-router-dom' -import Identicon from '../identicon' - -const { - ENVIRONMENT_TYPE_NOTIFICATION, - ENVIRONMENT_TYPE_POPUP, -} = require('../../../../app/scripts/lib/enums') -const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('../../routes') +import Identicon from '../../ui/identicon' +import { DEFAULT_ROUTE } from '../../../helpers/constants/routes' const NetworkIndicator = require('../network') export default class AppHeader extends PureComponent { static propTypes = { history: PropTypes.object, - location: PropTypes.object, network: PropTypes.string, provider: PropTypes.object, networkDropdownOpen: PropTypes.bool, @@ -23,10 +16,14 @@ export default class AppHeader extends PureComponent { toggleAccountMenu: PropTypes.func, selectedAddress: PropTypes.string, isUnlocked: PropTypes.bool, + hideNetworkIndicator: PropTypes.bool, + disabled: PropTypes.bool, + isAccountMenuOpen: PropTypes.bool, } static contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } handleNetworkIndicatorClick (event) { @@ -35,28 +32,40 @@ export default class AppHeader extends PureComponent { const { networkDropdownOpen, showNetworkDropdown, hideNetworkDropdown } = this.props - return networkDropdownOpen === false - ? showNetworkDropdown() - : hideNetworkDropdown() - } - - isConfirming () { - const { location } = this.props - - return Boolean(matchPath(location.pathname, { - path: CONFIRM_TRANSACTION_ROUTE, exact: false, - })) + if (networkDropdownOpen === false) { + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Opened Network Menu', + }, + }) + showNetworkDropdown() + } else { + hideNetworkDropdown() + } } renderAccountMenu () { - const { isUnlocked, toggleAccountMenu, selectedAddress } = this.props + const { isUnlocked, toggleAccountMenu, selectedAddress, disabled, isAccountMenuOpen } = this.props return isUnlocked && ( <div className={classnames('account-menu__icon', { - 'account-menu__icon--disabled': this.isConfirming(), + 'account-menu__icon--disabled': disabled, })} - onClick={() => this.isConfirming() || toggleAccountMenu()} + onClick={() => { + if (!disabled) { + !isAccountMenuOpen && this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Opened Main Menu', + }, + }) + toggleAccountMenu() + } + }} > <Identicon address={selectedAddress} @@ -66,38 +75,16 @@ export default class AppHeader extends PureComponent { ) } - hideAppHeader () { - const { location } = this.props - - const isInitializing = Boolean(matchPath(location.pathname, { - path: INITIALIZE_ROUTE, exact: false, - })) - - if (isInitializing) { - return true - } - - if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) { - return true - } - - if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP && this.isConfirming()) { - return true - } - } - render () { const { + history, network, provider, - history, isUnlocked, + hideNetworkIndicator, + disabled, } = this.props - if (this.hideAppHeader()) { - return null - } - return ( <div className={classnames('app-header', { 'app-header--back-drop': isUnlocked })}> @@ -108,7 +95,7 @@ export default class AppHeader extends PureComponent { > <img className="app-header__metafox-logo app-header__metafox-logo--horizontal" - src="/images/logo/metamask-logo-horizontal-beta.svg" + src="/images/logo/metamask-logo-horizontal.svg" height={30} /> <img @@ -119,14 +106,18 @@ export default class AppHeader extends PureComponent { /> </div> <div className="app-header__account-menu-container"> - <div className="app-header__network-component-wrapper"> - <NetworkIndicator - network={network} - provider={provider} - onClick={event => this.handleNetworkIndicatorClick(event)} - disabled={this.isConfirming()} - /> - </div> + { + !hideNetworkIndicator && ( + <div className="app-header__network-component-wrapper"> + <NetworkIndicator + network={network} + provider={provider} + onClick={event => this.handleNetworkIndicatorClick(event)} + disabled={disabled} + /> + </div> + ) + } { this.renderAccountMenu() } </div> </div> diff --git a/ui/app/components/app-header/app-header.container.js b/ui/app/components/app/app-header/app-header.container.js index 30d3f8cc4..b67338245 100644 --- a/ui/app/components/app-header/app-header.container.js +++ b/ui/app/components/app/app-header/app-header.container.js @@ -3,7 +3,7 @@ import { withRouter } from 'react-router-dom' import { compose } from 'recompose' import AppHeader from './app-header.component' -const actions = require('../../actions') +const actions = require('../../../store/actions') const mapStateToProps = state => { const { appState, metamask } = state @@ -13,6 +13,7 @@ const mapStateToProps = state => { provider, selectedAddress, isUnlocked, + isAccountMenuOpen, } = metamask return { @@ -21,6 +22,7 @@ const mapStateToProps = state => { provider, selectedAddress, isUnlocked, + isAccountMenuOpen, } } diff --git a/ui/app/components/app-header/index.js b/ui/app/components/app/app-header/index.js index 6de2f9c78..6de2f9c78 100644 --- a/ui/app/components/app-header/index.js +++ b/ui/app/components/app/app-header/index.js diff --git a/ui/app/components/app-header/index.scss b/ui/app/components/app/app-header/index.scss index 325844af5..325844af5 100644 --- a/ui/app/components/app-header/index.scss +++ b/ui/app/components/app/app-header/index.scss diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/app/bn-as-decimal-input.js index 9a033f893..9a033f893 100644 --- a/ui/app/components/bn-as-decimal-input.js +++ b/ui/app/components/app/bn-as-decimal-input.js diff --git a/ui/app/components/coinbase-form.js b/ui/app/components/app/coinbase-form.js index d5915292e..24d287604 100644 --- a/ui/app/components/coinbase-form.js +++ b/ui/app/components/app/coinbase-form.js @@ -3,7 +3,7 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const actions = require('../actions') +const actions = require('../../store/actions') CoinbaseForm.contextTypes = { t: PropTypes.func, diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/app/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js index c7262d2a9..18571eccb 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js @@ -2,7 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' -import { PRIMARY, SECONDARY } from '../../../constants/common' +import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common' const ConfirmDetailRow = props => { const { diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.js b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.js index 056afff04..056afff04 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/index.js +++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.js diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss index 580a41fde..1672ef8c6 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss +++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/index.scss @@ -43,4 +43,8 @@ font-size: .625rem; } } + + .advanced-gas-inputs__gas-edit-rows { + margin-bottom: 16px; + } } diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js b/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js index c8507985d..c8507985d 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js +++ b/ui/app/components/app/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index 1dca81560..8a5f90c76 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -1,9 +1,9 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { Tabs, Tab } from '../../tabs' -import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from './' -import ErrorMessage from '../../error-message' +import { Tabs, Tab } from '../../../ui/tabs' +import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from '.' +import ErrorMessage from '../../../ui/error-message' export default class ConfirmPageContainerContent extends Component { static propTypes = { diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js index 89ceb015f..0cc4d8262 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import Identicon from '../../../identicon' +import Identicon from '../../../../ui/identicon' const ConfirmPageContainerSummary = props => { const { diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js index ed1b28cf2..ed1b28cf2 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.js diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss index 7f0f5d37a..7f0f5d37a 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/index.scss diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js index 79901c8fc..79901c8fc 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/confirm-page-container-warning.component.js diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js index 6e48bd144..6e48bd144 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.js diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss index 50545a1a2..50545a1a2 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-warning/index.scss diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.js index 4dfd89d92..4dfd89d92 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/index.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.js diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss index 698e624f4..602a46848 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/index.scss +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss @@ -1,6 +1,6 @@ -@import './confirm-page-container-warning/index'; +@import 'confirm-page-container-warning/index'; -@import './confirm-page-container-summary/index'; +@import 'confirm-page-container-summary/index'; .confirm-page-container-content { overflow-y: auto; @@ -52,6 +52,10 @@ &__gas-fee { border-bottom: 1px solid $geyser; + + .advanced-gas-inputs__gas-edit-rows { + margin-bottom: 16px; + } } &__function-type { diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js index e6fe8f82c..84ca40da5 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import { ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_NOTIFICATION, -} from '../../../../../app/scripts/lib/enums' +} from '../../../../../../app/scripts/lib/enums' import NetworkDisplay from '../../network-display' export default class ConfirmPageContainer extends Component { diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.js index 71feb6931..71feb6931 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-header/index.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.js diff --git a/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss index 43e1e4427..be77edbdf 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-header/index.scss +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss @@ -7,7 +7,7 @@ display: flex; justify-content: space-between; border-bottom: 1px solid $geyser; - padding: 13px 13px 13px 24px; + padding: 4px 13px 4px 13px; flex: 0 0 auto; } diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js new file mode 100755 index 000000000..8327f997b --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/confirm-page-container-navigation.component.js @@ -0,0 +1,69 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const ConfirmPageContainerNavigation = props => { + const { onNextTx, totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation, firstTx, lastTx, ofText, requestsWaitingText } = props + + return ( + <div className="confirm-page-container-navigation" + style={{ + display: showNavigation ? 'flex' : 'none', + }} + > + <div className="confirm-page-container-navigation__container" + style={{ + visibility: prevTxId ? 'initial' : 'hidden', + }}> + <div + className="confirm-page-container-navigation__arrow" + onClick={() => onNextTx(firstTx)}> + <img src="/images/double-arrow.svg" /> + </div> + <div + className="confirm-page-container-navigation__arrow" + onClick={() => onNextTx(prevTxId)}> + <img src="/images/single-arrow.svg" /> + </div> + </div> + <div className="confirm-page-container-navigation__textcontainer"> + <div className="confirm-page-container-navigation__navtext"> + {positionOfCurrentTx} {ofText} {totalTx} + </div> + <div className="confirm-page-container-navigation__longtext"> + {requestsWaitingText} + </div> + </div> + <div + className="confirm-page-container-navigation__container" + style={{ + visibility: nextTxId ? 'initial' : 'hidden', + }}> + <div + className="confirm-page-container-navigation__arrow" + onClick={() => onNextTx(nextTxId)}> + <img className="confirm-page-container-navigation__imageflip" src="/images/single-arrow.svg" /> + </div> + <div + className="confirm-page-container-navigation__arrow" + onClick={() => onNextTx(lastTx)}> + <img className="confirm-page-container-navigation__imageflip" src="/images/double-arrow.svg" /> + </div> + </div> + </div> + ) +} + +ConfirmPageContainerNavigation.propTypes = { + totalTx: PropTypes.number, + positionOfCurrentTx: PropTypes.number, + onNextTx: PropTypes.func, + nextTxId: PropTypes.string, + prevTxId: PropTypes.string, + showNavigation: PropTypes.bool, + firstTx: PropTypes.string, + lastTx: PropTypes.string, + ofText: PropTypes.string, + requestsWaitingText: PropTypes.string, +} + +export default ConfirmPageContainerNavigation diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.js b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.js new file mode 100755 index 000000000..d97c1b447 --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.js @@ -0,0 +1 @@ +export { default } from './confirm-page-container-navigation.component' diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.scss b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.scss new file mode 100755 index 000000000..0cf184c60 --- /dev/null +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-navigation/index.scss @@ -0,0 +1,54 @@ +.confirm-page-container-navigation { + display: flex; + justify-content: space-between; + font: inherit; + padding: 4px 10px 4px 10px; + border-bottom: 1px solid $geyser; + flex: 0 0 auto; + + &__container { + display: flex; + } + + &__arrow { + cursor: pointer; + display: flex; + padding-left: 5px; + padding-right: 5px; + } + + &__arrow:hover { + -webkit-transform: scale(1.1); + -moz-transform: scale(1.1); + -o-transform: scale(1.1); + transform: scale(1.1); + } + + &__arrow:active { + -webkit-transform: scale(0.95); + -moz-transform: scale(0.95); + -o-transform: scale(0.95); + transform: scale(0.95); + } + + &__textcontainer { + text-align: center; + } + + &__navtext { + font-size: 9px; + font-weight: bold; + } + + &__longtext { + color: $oslo-gray; + font-size: 8px; + } + + &__imageflip { + -webkit-transform: scaleX(-1); + -moz-transform: scaleX(-1); + -o-transform: scaleX(-1); + transform: scaleX(-1); + } +}
\ No newline at end of file diff --git a/ui/app/components/confirm-page-container/confirm-page-container.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js index 8b2e47cbb..326e4f83e 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js @@ -1,8 +1,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import SenderToRecipient from '../sender-to-recipient' -import { PageContainerFooter } from '../page-container' -import { ConfirmPageContainerHeader, ConfirmPageContainerContent } from './' +import SenderToRecipient from '../../ui/sender-to-recipient' +import { PageContainerFooter } from '../../ui/page-container' +import { ConfirmPageContainerHeader, ConfirmPageContainerContent, ConfirmPageContainerNavigation } from '.' export default class ConfirmPageContainer extends Component { static contextTypes = { @@ -43,6 +43,17 @@ export default class ConfirmPageContainer extends Component { summaryComponent: PropTypes.node, warning: PropTypes.string, unapprovedTxCount: PropTypes.number, + // Navigation + totalTx: PropTypes.number, + positionOfCurrentTx: PropTypes.number, + nextTxId: PropTypes.string, + prevTxId: PropTypes.string, + showNavigation: PropTypes.bool, + onNextTx: PropTypes.func, + firstTx: PropTypes.string, + lastTx: PropTypes.string, + ofText: PropTypes.string, + requestsWaitingText: PropTypes.string, // Footer onCancelAll: PropTypes.func, onCancel: PropTypes.func, @@ -79,11 +90,33 @@ export default class ConfirmPageContainer extends Component { unapprovedTxCount, assetImage, warning, + totalTx, + positionOfCurrentTx, + nextTxId, + prevTxId, + showNavigation, + onNextTx, + firstTx, + lastTx, + ofText, + requestsWaitingText, } = this.props const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress) return ( <div className="page-container"> + <ConfirmPageContainerNavigation + totalTx={totalTx} + positionOfCurrentTx={positionOfCurrentTx} + nextTxId={nextTxId} + prevTxId={prevTxId} + showNavigation={showNavigation} + onNextTx={(txId) => onNextTx(txId)} + firstTx={firstTx} + lastTx={lastTx} + ofText={ofText} + requestsWaitingText={requestsWaitingText} + /> <ConfirmPageContainerHeader showEdit={showEdit} onEdit={() => onEdit()} diff --git a/ui/app/components/confirm-page-container/index.js b/ui/app/components/app/confirm-page-container/index.js index ee88aa5d3..28b17614e 100644 --- a/ui/app/components/confirm-page-container/index.js +++ b/ui/app/components/app/confirm-page-container/index.js @@ -1,6 +1,8 @@ export { default } from './confirm-page-container.component' export { default as ConfirmPageContainerHeader } from './confirm-page-container-header' export { default as ConfirmDetailRow } from './confirm-detail-row' +export { default as ConfirmPageContainerNavigation } from './confirm-page-container-navigation' + export { default as ConfirmPageContainerContent, ConfirmPageContainerSummary, diff --git a/ui/app/components/app/confirm-page-container/index.scss b/ui/app/components/app/confirm-page-container/index.scss new file mode 100644 index 000000000..c0277eff5 --- /dev/null +++ b/ui/app/components/app/confirm-page-container/index.scss @@ -0,0 +1,7 @@ +@import 'confirm-page-container-content/index'; + +@import 'confirm-page-container-header/index'; + +@import 'confirm-detail-row/index'; + +@import 'confirm-page-container-navigation/index'; diff --git a/ui/app/components/copyable.js b/ui/app/components/app/copyable.js index ad504deb8..6869d674d 100644 --- a/ui/app/components/copyable.js +++ b/ui/app/components/app/copyable.js @@ -3,7 +3,7 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits -const Tooltip = require('./tooltip') +const Tooltip = require('../ui/tooltip') const copyToClipboard = require('copy-to-clipboard') const connect = require('react-redux').connect diff --git a/ui/app/components/customize-gas-modal/gas-modal-card.js b/ui/app/components/app/customize-gas-modal/gas-modal-card.js index 23754d819..23754d819 100644 --- a/ui/app/components/customize-gas-modal/gas-modal-card.js +++ b/ui/app/components/app/customize-gas-modal/gas-modal-card.js diff --git a/ui/app/components/customize-gas-modal/gas-slider.js b/ui/app/components/app/customize-gas-modal/gas-slider.js index 69fd6f985..69fd6f985 100644 --- a/ui/app/components/customize-gas-modal/gas-slider.js +++ b/ui/app/components/app/customize-gas-modal/gas-slider.js diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/app/customize-gas-modal/index.js index e67fbe45b..dca77bb00 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/app/customize-gas-modal/index.js @@ -3,15 +3,16 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const actions = require('../../actions') +const BigNumber = require('bignumber.js') +const actions = require('../../../store/actions') const GasModalCard = require('./gas-modal-card') -import Button from '../button' +import Button from '../../ui/button' const ethUtil = require('ethereumjs-util') import { updateSendErrors, -} from '../../ducks/send.duck' +} from '../../../ducks/send/send.duck' const { MIN_GAS_PRICE_DEC, @@ -29,7 +30,7 @@ const { conversionGreaterThan, conversionMax, subtractCurrencies, -} = require('../../conversion-util') +} = require('../../../helpers/utils/conversion-util') const { getGasIsLoading, @@ -41,7 +42,7 @@ const { getCurrentAccountWithSendEtherInfo, getSelectedTokenToFiatRate, getSendMaxModeState, -} = require('../../selectors') +} = require('../../../selectors/selectors') const { getGasPrice, @@ -112,6 +113,7 @@ function CustomizeGasModal (props) { CustomizeGasModal.contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal) @@ -148,6 +150,7 @@ CustomizeGasModal.prototype.componentWillReceiveProps = function (nextProps) { } CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { + const { metricsEvent } = this.context const { setGasPrice, setGasLimit, @@ -159,6 +162,9 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { updateSendAmount, updateSendErrors, } = this.props + const { + originalState, + } = this.state if (maxModeOn && !selectedToken) { const maxAmount = subtractCurrencies( @@ -169,6 +175,22 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { updateSendAmount(maxAmount) } + metricsEvent({ + eventOpts: { + category: 'Activation', + action: 'userCloses', + name: 'closeCustomizeGas', + }, + pageOpts: { + section: 'customizeGasModal', + component: 'customizeGasSaveButton', + }, + customVariables: { + gasPriceChange: (new BigNumber(ethUtil.addHexPrefix(gasPrice))).minus(new BigNumber(ethUtil.addHexPrefix(originalState.gasPrice))).toString(10), + gasLimitChange: (new BigNumber(ethUtil.addHexPrefix(gasLimit))).minus(new BigNumber(ethUtil.addHexPrefix(originalState.gasLimit))).toString(10), + }, + }) + setGasPrice(ethUtil.addHexPrefix(gasPrice)) setGasLimit(ethUtil.addHexPrefix(gasLimit)) setGasTotal(ethUtil.addHexPrefix(gasTotal)) diff --git a/ui/app/components/dropdowns/account-details-dropdown.js b/ui/app/components/app/dropdowns/account-details-dropdown.js index 7476cfdd9..3d4598946 100644 --- a/ui/app/components/dropdowns/account-details-dropdown.js +++ b/ui/app/components/app/dropdowns/account-details-dropdown.js @@ -3,13 +3,14 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const actions = require('../../actions') -const { getSelectedIdentity } = require('../../selectors') -const genAccountLink = require('../../../lib/account-link.js') +const actions = require('../../../store/actions') +const { getSelectedIdentity } = require('../../../selectors/selectors') +const genAccountLink = require('../../../../lib/account-link.js') const { Menu, Item, CloseArea } = require('./components/menu') AccountDetailsDropdown.contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsDropdown) @@ -72,6 +73,13 @@ AccountDetailsDropdown.prototype.render = function () { h(Item, { onClick: (e) => { e.stopPropagation() + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Account Options', + name: 'Clicked Expand View', + }, + }) global.platform.openExtensionInBrowser() this.props.onClose() }, @@ -82,6 +90,13 @@ AccountDetailsDropdown.prototype.render = function () { onClick: (e) => { e.stopPropagation() showAccountDetailModal() + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Account Options', + name: 'Viewed Account Details', + }, + }) this.props.onClose() }, text: this.context.t('accountDetails'), @@ -90,6 +105,13 @@ AccountDetailsDropdown.prototype.render = function () { h(Item, { onClick: (e) => { e.stopPropagation() + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Account Options', + name: 'Clicked View on Etherscan', + }, + }) viewOnEtherscan(address, network) this.props.onClose() }, diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/app/dropdowns/components/account-dropdowns.js index e6b3e0c0c..c603a9a9f 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/app/dropdowns/components/account-dropdowns.js @@ -1,15 +1,15 @@ const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') -const actions = require('../../../actions') -const genAccountLink = require('../../../../lib/account-link.js') +const actions = require('../../../../store/actions') +const genAccountLink = require('../../../../../lib/account-link.js') const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem -import Identicon from '../../identicon' -const { checksumAddress } = require('../../../util') +import Identicon from '../../../ui/identicon' +const { checksumAddress } = require('../../../../helpers/utils/util') const copyToClipboard = require('copy-to-clipboard') -const { formatBalance } = require('../../../util') +const { formatBalance } = require('../../../../helpers/utils/util') class AccountDropdowns extends Component { diff --git a/ui/app/components/dropdowns/components/dropdown.js b/ui/app/components/app/dropdowns/components/dropdown.js index 149f063a7..149f063a7 100644 --- a/ui/app/components/dropdowns/components/dropdown.js +++ b/ui/app/components/app/dropdowns/components/dropdown.js diff --git a/ui/app/components/dropdowns/components/menu.js b/ui/app/components/app/dropdowns/components/menu.js index f6d8a139e..f6d8a139e 100644 --- a/ui/app/components/dropdowns/components/menu.js +++ b/ui/app/components/app/dropdowns/components/menu.js diff --git a/ui/app/components/app/dropdowns/components/network-dropdown-icon.js b/ui/app/components/app/dropdowns/components/network-dropdown-icon.js new file mode 100644 index 000000000..d4a2c2ff7 --- /dev/null +++ b/ui/app/components/app/dropdowns/components/network-dropdown-icon.js @@ -0,0 +1,47 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') + + +inherits(NetworkDropdownIcon, Component) +module.exports = NetworkDropdownIcon + +function NetworkDropdownIcon () { + Component.call(this) +} + +NetworkDropdownIcon.prototype.render = function () { + const { + backgroundColor, + isSelected, + innerBorder = 'none', + diameter = '12', + loading, + } = this.props + + return loading + ? h('span.pointer.network-indicator', { + style: { + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + }, + }, [ + h('img', { + style: { + width: '27px', + }, + src: 'images/loading.svg', + }), + ]) + : h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {}, + h('div', { + style: { + background: backgroundColor, + border: innerBorder, + height: `${diameter}px`, + width: `${diameter}px`, + }, + }) + ) +} diff --git a/ui/app/components/dropdowns/index.js b/ui/app/components/app/dropdowns/index.js index 605507058..605507058 100644 --- a/ui/app/components/dropdowns/index.js +++ b/ui/app/components/app/dropdowns/index.js diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js index d4cc695a6..3d9037a06 100644 --- a/ui/app/components/dropdowns/network-dropdown.js +++ b/ui/app/components/app/dropdowns/network-dropdown.js @@ -5,12 +5,12 @@ const inherits = require('util').inherits const connect = require('react-redux').connect const { withRouter } = require('react-router-dom') const { compose } = require('recompose') -const actions = require('../../actions') +const actions = require('../../../store/actions') const Dropdown = require('./components/dropdown').Dropdown const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem const NetworkDropdownIcon = require('./components/network-dropdown-icon') const R = require('ramda') -const { SETTINGS_ROUTE } = require('../../routes') +const { ADVANCED_ROUTE } = require('../../../helpers/constants/routes') // classes from nodes of the toggle element. const notToggleElementClassnames = [ @@ -60,6 +60,7 @@ function NetworkDropdown () { NetworkDropdown.contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } module.exports = compose( @@ -120,7 +121,7 @@ NetworkDropdown.prototype.render = function () { { key: 'main', closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => props.setProviderType('mainnet'), + onClick: () => this.handleClick('mainnet'), style: { ...dropdownMenuItemStyle, borderColor: '#038789' }, }, [ @@ -142,7 +143,7 @@ NetworkDropdown.prototype.render = function () { { key: 'ropsten', closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => props.setProviderType('ropsten'), + onClick: () => this.handleClick('ropsten'), style: dropdownMenuItemStyle, }, [ @@ -164,7 +165,7 @@ NetworkDropdown.prototype.render = function () { { key: 'kovan', closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => props.setProviderType('kovan'), + onClick: () => this.handleClick('kovan'), style: dropdownMenuItemStyle, }, [ @@ -186,7 +187,7 @@ NetworkDropdown.prototype.render = function () { { key: 'rinkeby', closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => props.setProviderType('rinkeby'), + onClick: () => this.handleClick('rinkeby'), style: dropdownMenuItemStyle, }, [ @@ -208,7 +209,7 @@ NetworkDropdown.prototype.render = function () { { key: 'default', closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => props.setProviderType('localhost'), + onClick: () => this.handleClick('localhost'), style: dropdownMenuItemStyle, }, [ @@ -232,7 +233,7 @@ NetworkDropdown.prototype.render = function () { DropdownMenuItem, { closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => this.props.history.push(SETTINGS_ROUTE), + onClick: () => this.props.history.push(ADVANCED_ROUTE), style: dropdownMenuItemStyle, }, [ @@ -252,6 +253,23 @@ NetworkDropdown.prototype.render = function () { ]) } +NetworkDropdown.prototype.handleClick = function (newProviderType) { + const { provider: { type: providerType }, setProviderType } = this.props + const { metricsEvent } = this.context + + metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Switched Networks', + }, + customVariables: { + fromNetwork: providerType, + toNetwork: newProviderType, + }, + }) + setProviderType(newProviderType) +} NetworkDropdown.prototype.getNetworkName = function () { const { provider } = this.props @@ -277,7 +295,6 @@ NetworkDropdown.prototype.getNetworkName = function () { NetworkDropdown.prototype.renderCommonRpc = function (rpcListDetail, provider) { const props = this.props const reversedRpcListDetail = rpcListDetail.slice().reverse() - const network = props.network return reversedRpcListDetail.map((entry) => { const rpc = entry.rpcUrl @@ -288,7 +305,7 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcListDetail, provider) { if ((rpc === 'http://localhost:8545') || currentRpcTarget) { return null } else { - const chainId = entry.chainId || network + const chainId = entry.chainId return h( DropdownMenuItem, { diff --git a/ui/app/components/dropdowns/simple-dropdown.js b/ui/app/components/app/dropdowns/simple-dropdown.js index bba088ed1..bba088ed1 100644 --- a/ui/app/components/dropdowns/simple-dropdown.js +++ b/ui/app/components/app/dropdowns/simple-dropdown.js diff --git a/ui/app/components/dropdowns/tests/dropdown.test.js b/ui/app/components/app/dropdowns/tests/dropdown.test.js index 2b026589a..2b026589a 100644 --- a/ui/app/components/dropdowns/tests/dropdown.test.js +++ b/ui/app/components/app/dropdowns/tests/dropdown.test.js diff --git a/ui/app/components/dropdowns/tests/menu.test.js b/ui/app/components/app/dropdowns/tests/menu.test.js index 9f5f13f00..9f5f13f00 100644 --- a/ui/app/components/dropdowns/tests/menu.test.js +++ b/ui/app/components/app/dropdowns/tests/menu.test.js diff --git a/ui/app/components/dropdowns/tests/network-dropdown-icon.test.js b/ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js index 67b192c11..67b192c11 100644 --- a/ui/app/components/dropdowns/tests/network-dropdown-icon.test.js +++ b/ui/app/components/app/dropdowns/tests/network-dropdown-icon.test.js diff --git a/ui/app/components/dropdowns/tests/network-dropdown.test.js b/ui/app/components/app/dropdowns/tests/network-dropdown.test.js index 88ad56851..91e7899a7 100644 --- a/ui/app/components/dropdowns/tests/network-dropdown.test.js +++ b/ui/app/components/app/dropdowns/tests/network-dropdown.test.js @@ -1,7 +1,7 @@ import React from 'react' import assert from 'assert' import { createMockStore } from 'redux-test-utils' -import { mountWithRouter } from '../../../../../test/lib/render-helpers' +import { mountWithRouter } from '../../../../../../test/lib/render-helpers' import NetworkDropdown from '../network-dropdown' import { DropdownMenuItem } from '../components/dropdown' import NetworkDropdownIcon from '../components/network-dropdown-icon' diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/app/dropdowns/token-menu-dropdown.js index 8a072b1bc..e2730aea2 100644 --- a/ui/app/components/dropdowns/token-menu-dropdown.js +++ b/ui/app/components/app/dropdowns/token-menu-dropdown.js @@ -3,7 +3,7 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const actions = require('../../actions') +const actions = require('../../../store/actions') const genAccountLink = require('etherscan-link').createAccountLink const { Menu, Item, CloseArea } = require('./components/menu') diff --git a/ui/app/components/ens-input.js b/ui/app/components/app/ens-input.js index f538fd555..274058a1b 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/app/ens-input.js @@ -12,7 +12,7 @@ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' const connect = require('react-redux').connect const ToAutoComplete = require('./send/to-autocomplete').default const log = require('loglevel') -const { isValidENSAddress } = require('../util') +const { isValidENSAddress } = require('../../helpers/utils/util') EnsInput.contextTypes = { t: PropTypes.func, @@ -128,7 +128,7 @@ EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { } if (prevState && ensResolution && this.props.onChange && ensResolution !== prevState.ensResolution) { - this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError }) + this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError, toWarning: state.toWarning }) } } diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js new file mode 100644 index 000000000..95894140c --- /dev/null +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js @@ -0,0 +1,156 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import debounce from 'lodash.debounce' + +export default class AdvancedTabContent extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + updateCustomGasPrice: PropTypes.func, + updateCustomGasLimit: PropTypes.func, + customGasPrice: PropTypes.number, + customGasLimit: PropTypes.number, + insufficientBalance: PropTypes.bool, + customPriceIsSafe: PropTypes.bool, + isSpeedUp: PropTypes.bool, + showGasPriceInfoModal: PropTypes.func, + showGasLimitInfoModal: PropTypes.func, + } + + debouncedGasLimitReset = debounce((dVal) => { + if (dVal < 21000) { + this.props.updateCustomGasLimit(21000) + } + }, 1000, { trailing: true }) + + onChangeGasLimit = (val) => { + this.props.updateCustomGasLimit(val) + this.debouncedGasLimitReset(val) + } + + gasInputError ({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) { + const { t } = this.context + let errorText + let errorType + let isInError = true + + + if (insufficientBalance) { + errorText = t('insufficientBalance') + errorType = 'error' + } else if (labelKey === 'gasPrice' && isSpeedUp && value === 0) { + errorText = t('zeroGasPriceOnSpeedUpError') + errorType = 'error' + } else if (labelKey === 'gasPrice' && !customPriceIsSafe) { + errorText = t('gasPriceExtremelyLow') + errorType = 'warning' + } else { + isInError = false + } + + return { + isInError, + errorText, + errorType, + } + } + + gasInput ({ labelKey, value, onChange, insufficientBalance, showGWEI, customPriceIsSafe, isSpeedUp }) { + const { + isInError, + errorText, + errorType, + } = this.gasInputError({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) + + return ( + <div className="advanced-gas-inputs__gas-edit-row__input-wrapper"> + <input + className={classnames('advanced-gas-inputs__gas-edit-row__input', { + 'advanced-gas-inputs__gas-edit-row__input--error': isInError && errorType === 'error', + 'advanced-gas-inputs__gas-edit-row__input--warning': isInError && errorType === 'warning', + })} + type="number" + value={value} + onChange={event => onChange(Number(event.target.value))} + /> + <div className={classnames('advanced-gas-inputs__gas-edit-row__input-arrows', { + 'advanced-gas-inputs__gas-edit-row__input--error': isInError && errorType === 'error', + 'advanced-gas-inputs__gas-edit-row__input--warning': isInError && errorType === 'warning', + })}> + <div + className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap" + onClick={() => onChange(value + 1)} + > + <i className="fa fa-sm fa-angle-up" /> + </div> + <div + className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap" + onClick={() => onChange(Math.max(value - 1, 0))} + > + <i className="fa fa-sm fa-angle-down" /> + </div> + </div> + { isInError + ? <div className={`advanced-gas-inputs__gas-edit-row__${errorType}-text`}> + { errorText } + </div> + : null } + </div> + ) + } + + infoButton (onClick) { + return <i className="fa fa-info-circle" onClick={onClick} /> + } + + renderGasEditRow (gasInputArgs) { + return ( + <div className="advanced-gas-inputs__gas-edit-row"> + <div className="advanced-gas-inputs__gas-edit-row__label"> + { this.context.t(gasInputArgs.labelKey) } + { this.infoButton(() => gasInputArgs.infoOnClick()) } + </div> + { this.gasInput(gasInputArgs) } + </div> + ) + } + + render () { + const { + customGasPrice, + updateCustomGasPrice, + customGasLimit, + insufficientBalance, + customPriceIsSafe, + isSpeedUp, + showGasPriceInfoModal, + showGasLimitInfoModal, + } = this.props + + return ( + <div className="advanced-gas-inputs__gas-edit-rows"> + { this.renderGasEditRow({ + labelKey: 'gasPrice', + value: customGasPrice, + onChange: updateCustomGasPrice, + insufficientBalance, + customPriceIsSafe, + showGWEI: true, + isSpeedUp, + infoOnClick: showGasPriceInfoModal, + }) } + { this.renderGasEditRow({ + labelKey: 'gasLimit', + value: customGasLimit, + onChange: this.onChangeGasLimit, + insufficientBalance, + customPriceIsSafe, + infoOnClick: showGasLimitInfoModal, + }) } + </div> + ) + } +} diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js new file mode 100644 index 000000000..90fef1a1b --- /dev/null +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js @@ -0,0 +1,38 @@ +import { connect } from 'react-redux' +import { showModal } from '../../../../store/actions' +import { + decGWEIToHexWEI, + decimalToHex, + hexWEIToDecGWEI, +} from '../../../../helpers/utils/conversions.util' +import AdvancedGasInputs from './advanced-gas-inputs.component' + +function convertGasPriceForInputs (gasPriceInHexWEI) { + return Number(hexWEIToDecGWEI(gasPriceInHexWEI)) +} + +function convertGasLimitForInputs (gasLimitInHexWEI) { + return parseInt(gasLimitInHexWEI, 16) +} + +const mapDispatchToProps = dispatch => { + return { + showGasPriceInfoModal: modalName => dispatch(showModal({ name: 'GAS_PRICE_INFO_MODAL' })), + showGasLimitInfoModal: modalName => dispatch(showModal({ name: 'GAS_LIMIT_INFO_MODAL' })), + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const {customGasPrice, customGasLimit, updateCustomGasPrice, updateCustomGasLimit} = ownProps + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + customGasPrice: convertGasPriceForInputs(customGasPrice), + customGasLimit: convertGasLimitForInputs(customGasLimit), + updateCustomGasPrice: (price) => updateCustomGasPrice(decGWEIToHexWEI(price)), + updateCustomGasLimit: (limit) => updateCustomGasLimit(decimalToHex(limit)), + } +} + +export default connect(null, mapDispatchToProps, mergeProps)(AdvancedGasInputs) diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/index.js b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.js new file mode 100644 index 000000000..bd8abaa3e --- /dev/null +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.js @@ -0,0 +1 @@ +export { default } from './advanced-gas-inputs.container' diff --git a/ui/app/components/app/gas-customization/advanced-gas-inputs/index.scss b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.scss new file mode 100644 index 000000000..50953cbe5 --- /dev/null +++ b/ui/app/components/app/gas-customization/advanced-gas-inputs/index.scss @@ -0,0 +1,133 @@ +.advanced-gas-inputs { + &__gas-edit-rows { + display: flex; + flex-flow: row; + justify-content: space-between; + } + + &__gas-edit-row { + display: flex; + flex-flow: column; + width: 47.5%; + + &__label { + color: #313B5E; + font-size: 12px; + display: flex; + justify-content: space-between; + align-items: center; + + @media screen and (max-width: 576px) { + font-size: 10px; + } + + .fa-info-circle { + color: $silver; + margin-left: 10px; + cursor: pointer; + } + + .fa-info-circle:hover { + color: $mid-gray; + } + } + + &__error-text { + font-size: 12px; + color: red; + } + + &__warning-text { + font-size: 12px; + color: orange; + } + + &__input-wrapper { + position: relative; + } + + &__input { + border: 1px solid $dusty-gray; + border-radius: 4px; + color: $mid-gray; + font-size: 16px; + height: 24px; + width: 100%; + padding-left: 8px; + padding-top: 2px; + margin-top: 7px; + } + + &__input--error { + border: 1px solid $red; + } + + &__input--warning { + border: 1px solid $orange; + } + + &__input-arrows { + position: absolute; + top: 7px; + right: 0px; + width: 17px; + height: 24px; + border: 1px solid #dadada; + border-top-right-radius: 4px; + display: flex; + flex-direction: column; + color: #9b9b9b; + font-size: .8em; + border-bottom-right-radius: 4px; + cursor: pointer; + + &__i-wrap { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + cursor: pointer; + } + + &__i-wrap:hover { + background: #4EADE7; + color: $white; + } + + i:hover { + background: #4EADE7; + } + + i { + font-size: 10px; + } + } + + &__input-arrows--error { + border: 1px solid $red; + } + + &__input-arrows--warning { + border: 1px solid $orange; + } + + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + &__gwei-symbol { + position: absolute; + top: 8px; + right: 10px; + color: $dusty-gray; + } + } +}
\ No newline at end of file diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js new file mode 100644 index 000000000..ad8628621 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js @@ -0,0 +1,226 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import Loading from '../../../../ui/loading-screen' +import GasPriceChart from '../../gas-price-chart' +import debounce from 'lodash.debounce' + +export default class AdvancedTabContent extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + updateCustomGasPrice: PropTypes.func, + updateCustomGasLimit: PropTypes.func, + customGasPrice: PropTypes.number, + customGasLimit: PropTypes.number, + gasEstimatesLoading: PropTypes.bool, + millisecondsRemaining: PropTypes.number, + transactionFee: PropTypes.string, + timeRemaining: PropTypes.string, + gasChartProps: PropTypes.object, + insufficientBalance: PropTypes.bool, + customPriceIsSafe: PropTypes.bool, + isSpeedUp: PropTypes.bool, + isEthereumNetwork: PropTypes.bool, + } + + constructor (props) { + super(props) + + this.debouncedGasLimitReset = debounce((dVal) => { + if (dVal < 21000) { + props.updateCustomGasLimit(21000) + } + }, 1000, { trailing: true }) + this.onChangeGasLimit = (val) => { + props.updateCustomGasLimit(val) + this.debouncedGasLimitReset(val) + } + } + + gasInputError ({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) { + const { t } = this.context + let errorText + let errorType + let isInError = true + + + if (insufficientBalance) { + errorText = t('insufficientBalance') + errorType = 'error' + } else if (labelKey === 'gasPrice' && isSpeedUp && value === 0) { + errorText = t('zeroGasPriceOnSpeedUpError') + errorType = 'error' + } else if (labelKey === 'gasPrice' && !customPriceIsSafe) { + errorText = t('gasPriceExtremelyLow') + errorType = 'warning' + } else { + isInError = false + } + + return { + isInError, + errorText, + errorType, + } + } + + gasInput ({ labelKey, value, onChange, insufficientBalance, showGWEI, customPriceIsSafe, isSpeedUp }) { + const { + isInError, + errorText, + errorType, + } = this.gasInputError({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) + + return ( + <div className="advanced-tab__gas-edit-row__input-wrapper"> + <input + className={classnames('advanced-tab__gas-edit-row__input', { + 'advanced-tab__gas-edit-row__input--error': isInError && errorType === 'error', + 'advanced-tab__gas-edit-row__input--warning': isInError && errorType === 'warning', + })} + type="number" + value={value} + onChange={event => onChange(Number(event.target.value))} + /> + <div className={classnames('advanced-tab__gas-edit-row__input-arrows', { + 'advanced-tab__gas-edit-row__input--error': isInError && errorType === 'error', + 'advanced-tab__gas-edit-row__input--warning': isInError && errorType === 'warning', + })}> + <div + className="advanced-tab__gas-edit-row__input-arrows__i-wrap" + onClick={() => onChange(value + 1)} + > + <i className="fa fa-sm fa-angle-up" /> + </div> + <div + className="advanced-tab__gas-edit-row__input-arrows__i-wrap" + onClick={() => onChange(Math.max(value - 1, 0))} + > + <i className="fa fa-sm fa-angle-down" /> + </div> + </div> + { isInError + ? <div className={`advanced-tab__gas-edit-row__${errorType}-text`}> + { errorText } + </div> + : null } + </div> + ) + } + + infoButton (onClick) { + return <i className="fa fa-info-circle" onClick={onClick} /> + } + + renderDataSummary (transactionFee, timeRemaining) { + return ( + <div className="advanced-tab__transaction-data-summary"> + <div className="advanced-tab__transaction-data-summary__titles"> + <span>{ this.context.t('newTransactionFee') }</span> + <span>~{ this.context.t('transactionTime') }</span> + </div> + <div className="advanced-tab__transaction-data-summary__container"> + <div className="advanced-tab__transaction-data-summary__fee"> + {transactionFee} + </div> + <div className="time-remaining">{timeRemaining}</div> + </div> + </div> + ) + } + + renderGasEditRow (gasInputArgs) { + return ( + <div className="advanced-tab__gas-edit-row"> + <div className="advanced-tab__gas-edit-row__label"> + { this.context.t(gasInputArgs.labelKey) } + { this.infoButton(() => {}) } + </div> + { this.gasInput(gasInputArgs) } + </div> + ) + } + + renderGasEditRows ({ + customGasPrice, + updateCustomGasPrice, + customGasLimit, + updateCustomGasLimit, + insufficientBalance, + customPriceIsSafe, + isSpeedUp, + }) { + return ( + <div className="advanced-tab__gas-edit-rows"> + { this.renderGasEditRow({ + labelKey: 'gasPrice', + value: customGasPrice, + onChange: updateCustomGasPrice, + insufficientBalance, + customPriceIsSafe, + showGWEI: true, + isSpeedUp, + }) } + { this.renderGasEditRow({ + labelKey: 'gasLimit', + value: customGasLimit, + onChange: this.onChangeGasLimit, + insufficientBalance, + customPriceIsSafe, + }) } + </div> + ) + } + + render () { + const { t } = this.context + const { + updateCustomGasPrice, + updateCustomGasLimit, + timeRemaining, + customGasPrice, + customGasLimit, + insufficientBalance, + gasChartProps, + gasEstimatesLoading, + customPriceIsSafe, + isSpeedUp, + transactionFee, + isEthereumNetwork, + } = this.props + + return ( + <div className="advanced-tab"> + { this.renderDataSummary(transactionFee, timeRemaining) } + <div className="advanced-tab__fee-chart"> + { this.renderGasEditRows({ + customGasPrice, + updateCustomGasPrice, + customGasLimit, + updateCustomGasLimit, + insufficientBalance, + customPriceIsSafe, + isSpeedUp, + }) } + { isEthereumNetwork + ? <div> + <div className="advanced-tab__fee-chart__title">{ t('liveGasPricePredictions') }</div> + {!gasEstimatesLoading + ? <GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} /> + : <Loading /> + } + <div className="advanced-tab__fee-chart__speed-buttons"> + <span>{ t('slower') }</span> + <span>{ t('faster') }</span> + </div> + </div> + : <div className="advanced-tab__fee-chart__title">{ t('chartOnlyAvailableEth') }</div> + } + </div> + </div> + ) + } +} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.js new file mode 100644 index 000000000..492037f25 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.js @@ -0,0 +1 @@ +export { default } from './advanced-tab-content.component' diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss new file mode 100644 index 000000000..20a503018 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/index.scss @@ -0,0 +1,207 @@ +@import './time-remaining/index'; + +.advanced-tab { + display: flex; + flex-flow: column; + + &__transaction-data-summary, + &__fee-chart-title { + padding-left: 24px; + padding-right: 24px; + } + + &__transaction-data-summary { + display: flex; + flex-flow: column; + color: $mid-gray; + margin-top: 12px; + padding-left: 18px; + padding-right: 18px; + + &__titles, + &__container { + display: flex; + flex-flow: row; + justify-content: space-between; + font-size: 12px; + color: #888EA3; + } + + &__container { + font-size: 16px; + margin-top: 0px; + } + + &__fee { + font-size: 16px; + color: #313A5E; + } + } + + &__fee-chart { + margin-top: 8px; + height: 265px; + background: #F8F9FB; + border-bottom: 1px solid #d2d8dd; + border-top: 1px solid #d2d8dd; + position: relative; + + &__title { + font-size: 12px; + color: #313A5E; + margin-left: 22px; + } + + &__speed-buttons { + position: absolute; + bottom: 13px; + display: flex; + justify-content: space-between; + padding-left: 20px; + padding-right: 19px; + width: 100%; + font-size: 10px; + color: #888EA3; + } + + .loading-overlay { + height: auto; + } + } + + &__slider-container { + padding-left: 27px; + padding-right: 27px; + } + + &__gas-edit-rows { + height: 73px; + display: flex; + flex-flow: row; + justify-content: space-between; + margin-left: 20px; + margin-right: 10px; + margin-top: 9px; + } + + &__gas-edit-row { + display: flex; + flex-flow: column; + + &__label { + color: #313B5E; + font-size: 14px; + display: flex; + justify-content: space-between; + align-items: center; + + .fa-info-circle { + color: $silver; + margin-left: 10px; + cursor: pointer; + } + + .fa-info-circle:hover { + color: $mid-gray; + } + } + + &__error-text { + font-size: 12px; + color: red; + } + + &__warning-text { + font-size: 12px; + color: orange; + } + + &__input-wrapper { + position: relative; + } + + &__input { + border: 1px solid $dusty-gray; + border-radius: 4px; + color: $mid-gray; + font-size: 16px; + height: 24px; + width: 155px; + padding-left: 8px; + padding-top: 2px; + margin-top: 7px; + } + + &__input--error { + border: 1px solid $red; + } + + &__input--warning { + border: 1px solid $orange; + } + + &__input-arrows { + position: absolute; + top: 7px; + right: 0px; + width: 17px; + height: 24px; + border: 1px solid #dadada; + border-top-right-radius: 4px; + display: flex; + flex-direction: column; + color: #9b9b9b; + font-size: .8em; + border-bottom-right-radius: 4px; + cursor: pointer; + + &__i-wrap { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + cursor: pointer; + } + + &__i-wrap:hover { + background: #4EADE7; + color: $white; + } + + i:hover { + background: #4EADE7; + } + + i { + font-size: 10px; + } + } + + &__input-arrows--error { + border: 1px solid $red; + } + + &__input-arrows--warning { + border: 1px solid $orange; + } + + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + &__gwei-symbol { + position: absolute; + top: 8px; + right: 10px; + color: $dusty-gray; + } + } +}
\ No newline at end of file diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js new file mode 100644 index 000000000..5f7d90922 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/tests/advanced-tab-content-component.test.js @@ -0,0 +1,365 @@ +import React from 'react' +import assert from 'assert' +import shallow from '../../../../../../../lib/shallow-with-context' +import sinon from 'sinon' +import AdvancedTabContent from '../advanced-tab-content.component.js' + +import GasPriceChart from '../../../gas-price-chart' +import Loading from '../../../../../ui/loading-screen' + +const propsMethodSpies = { + updateCustomGasPrice: sinon.spy(), + updateCustomGasLimit: sinon.spy(), +} + +sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRow') +sinon.spy(AdvancedTabContent.prototype, 'gasInput') +sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRows') +sinon.spy(AdvancedTabContent.prototype, 'renderDataSummary') +sinon.spy(AdvancedTabContent.prototype, 'gasInputError') + +describe('AdvancedTabContent Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(<AdvancedTabContent + updateCustomGasPrice={propsMethodSpies.updateCustomGasPrice} + updateCustomGasLimit={propsMethodSpies.updateCustomGasLimit} + customGasPrice={11} + customGasLimit={23456} + timeRemaining={21500} + transactionFee={'$0.25'} + insufficientBalance={false} + customPriceIsSafe={true} + isSpeedUp={false} + isEthereumNetwork={true} + />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) + }) + + afterEach(() => { + propsMethodSpies.updateCustomGasPrice.resetHistory() + propsMethodSpies.updateCustomGasLimit.resetHistory() + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + AdvancedTabContent.prototype.gasInput.resetHistory() + AdvancedTabContent.prototype.renderGasEditRows.resetHistory() + AdvancedTabContent.prototype.renderDataSummary.resetHistory() + }) + + describe('render()', () => { + it('should render the advanced-tab root node', () => { + assert(wrapper.hasClass('advanced-tab')) + }) + + it('should render the expected four children of the advanced-tab div', () => { + const advancedTabChildren = wrapper.children() + assert.equal(advancedTabChildren.length, 2) + + assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary')) + assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart')) + + const feeChartDiv = advancedTabChildren.at(1) + + assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows')) + assert(feeChartDiv.childAt(1).childAt(0).hasClass('advanced-tab__fee-chart__title')) + assert(feeChartDiv.childAt(1).childAt(1).is(GasPriceChart)) + assert(feeChartDiv.childAt(1).childAt(2).hasClass('advanced-tab__fee-chart__speed-buttons')) + }) + + it('should render a loading component instead of the chart if gasEstimatesLoading is true', () => { + wrapper.setProps({ gasEstimatesLoading: true }) + const advancedTabChildren = wrapper.children() + assert.equal(advancedTabChildren.length, 2) + + assert(advancedTabChildren.at(0).hasClass('advanced-tab__transaction-data-summary')) + assert(advancedTabChildren.at(1).hasClass('advanced-tab__fee-chart')) + + const feeChartDiv = advancedTabChildren.at(1) + + assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows')) + assert(feeChartDiv.childAt(1).childAt(0).hasClass('advanced-tab__fee-chart__title')) + assert(feeChartDiv.childAt(1).childAt(1).is(Loading)) + assert(feeChartDiv.childAt(1).childAt(2).hasClass('advanced-tab__fee-chart__speed-buttons')) + }) + + it('should call renderDataSummary with the expected params', () => { + assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) + const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall(0).args + assert.deepEqual(renderDataSummaryArgs, ['$0.25', 21500]) + }) + + it('should call renderGasEditRows with the expected params', () => { + assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1) + const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args + assert.deepEqual(renderGasEditRowArgs, [{ + customGasPrice: 11, + customGasLimit: 23456, + insufficientBalance: false, + customPriceIsSafe: true, + updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice, + updateCustomGasLimit: propsMethodSpies.updateCustomGasLimit, + isSpeedUp: false, + }]) + }) + }) + + describe('renderDataSummary()', () => { + let dataSummary + + beforeEach(() => { + dataSummary = shallow(wrapper.instance().renderDataSummary('mockTotalFee', 'mockMsRemaining')) + }) + + it('should render the transaction-data-summary root node', () => { + assert(dataSummary.hasClass('advanced-tab__transaction-data-summary')) + }) + + it('should render titles of the data', () => { + const titlesNode = dataSummary.children().at(0) + assert(titlesNode.hasClass('advanced-tab__transaction-data-summary__titles')) + assert.equal(titlesNode.children().at(0).text(), 'newTransactionFee') + assert.equal(titlesNode.children().at(1).text(), '~transactionTime') + }) + + it('should render the data', () => { + const dataNode = dataSummary.children().at(1) + assert(dataNode.hasClass('advanced-tab__transaction-data-summary__container')) + assert.equal(dataNode.children().at(0).text(), 'mockTotalFee') + assert(dataNode.children().at(1).hasClass('time-remaining')) + assert.equal(dataNode.children().at(1).text(), 'mockMsRemaining') + }) + }) + + describe('renderGasEditRow()', () => { + let gasEditRow + + beforeEach(() => { + AdvancedTabContent.prototype.gasInput.resetHistory() + gasEditRow = shallow(wrapper.instance().renderGasEditRow({ + labelKey: 'mockLabelKey', + someArg: 'argA', + })) + }) + + it('should render the gas-edit-row root node', () => { + assert(gasEditRow.hasClass('advanced-tab__gas-edit-row')) + }) + + it('should render a label and an input', () => { + const gasEditRowChildren = gasEditRow.children() + assert.equal(gasEditRowChildren.length, 2) + assert(gasEditRowChildren.at(0).hasClass('advanced-tab__gas-edit-row__label')) + assert(gasEditRowChildren.at(1).hasClass('advanced-tab__gas-edit-row__input-wrapper')) + }) + + it('should render the label key and info button', () => { + const gasRowLabelChildren = gasEditRow.children().at(0).children() + assert.equal(gasRowLabelChildren.length, 2) + assert(gasRowLabelChildren.at(0), 'mockLabelKey') + assert(gasRowLabelChildren.at(1).hasClass('fa-info-circle')) + }) + + it('should call this.gasInput with the correct args', () => { + const gasInputSpyArgs = AdvancedTabContent.prototype.gasInput.args + assert.deepEqual(gasInputSpyArgs[0], [ { labelKey: 'mockLabelKey', someArg: 'argA' } ]) + }) + }) + + describe('renderGasEditRows()', () => { + let gasEditRows + let tempOnChangeGasLimit + + beforeEach(() => { + tempOnChangeGasLimit = wrapper.instance().onChangeGasLimit + wrapper.instance().onChangeGasLimit = () => 'mockOnChangeGasLimit' + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + gasEditRows = shallow(wrapper.instance().renderGasEditRows( + 'mockGasPrice', + () => 'mockUpdateCustomGasPriceReturn', + 'mockGasLimit', + () => 'mockUpdateCustomGasLimitReturn', + false + )) + }) + + afterEach(() => { + wrapper.instance().onChangeGasLimit = tempOnChangeGasLimit + }) + + it('should render the gas-edit-rows root node', () => { + assert(gasEditRows.hasClass('advanced-tab__gas-edit-rows')) + }) + + it('should render two rows', () => { + const gasEditRowsChildren = gasEditRows.children() + assert.equal(gasEditRowsChildren.length, 2) + assert(gasEditRowsChildren.at(0).hasClass('advanced-tab__gas-edit-row')) + assert(gasEditRowsChildren.at(1).hasClass('advanced-tab__gas-edit-row')) + }) + + it('should call this.renderGasEditRow twice, with the expected args', () => { + const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args + assert.equal(renderGasEditRowSpyArgs.length, 2) + assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [{ + labelKey: 'gasPrice', + value: 'mockGasLimit', + onChange: () => 'mockOnChangeGasLimit', + insufficientBalance: false, + customPriceIsSafe: true, + showGWEI: true, + }].map(String)) + assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [{ + labelKey: 'gasPrice', + value: 'mockGasPrice', + onChange: () => 'mockUpdateCustomGasPriceReturn', + insufficientBalance: false, + customPriceIsSafe: true, + showGWEI: true, + }].map(String)) + }) + }) + + describe('infoButton()', () => { + let infoButton + + beforeEach(() => { + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + infoButton = shallow(wrapper.instance().infoButton(() => 'mockOnClickReturn')) + }) + + it('should render the i element', () => { + assert(infoButton.hasClass('fa-info-circle')) + }) + + it('should pass the onClick argument to the i tag onClick prop', () => { + assert(infoButton.props().onClick(), 'mockOnClickReturn') + }) + }) + + describe('gasInput()', () => { + let gasInput + + beforeEach(() => { + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + AdvancedTabContent.prototype.gasInputError.resetHistory() + gasInput = shallow(wrapper.instance().gasInput({ + labelKey: 'gasPrice', + value: 321, + onChange: value => value + 7, + insufficientBalance: false, + showGWEI: true, + customPriceIsSafe: true, + isSpeedUp: false, + })) + }) + + it('should render the input-wrapper root node', () => { + assert(gasInput.hasClass('advanced-tab__gas-edit-row__input-wrapper')) + }) + + it('should render two children, including an input', () => { + assert.equal(gasInput.children().length, 2) + assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input')) + }) + + it('should call the passed onChange method with the value of the input onChange event', () => { + const inputOnChange = gasInput.find('input').props().onChange + assert.equal(inputOnChange({ target: { value: 8} }), 15) + }) + + it('should have two input arrows', () => { + const upArrow = gasInput.find('.fa-angle-up') + assert.equal(upArrow.length, 1) + const downArrow = gasInput.find('.fa-angle-down') + assert.equal(downArrow.length, 1) + }) + + it('should call onChange with the value incremented decremented when its onchange method is called', () => { + const upArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(0) + assert.equal(upArrow.props().onClick(), 329) + const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1) + assert.equal(downArrow.props().onClick(), 327) + }) + + it('should call gasInputError with the expected params', () => { + assert.equal(AdvancedTabContent.prototype.gasInputError.callCount, 1) + const gasInputErrorArgs = AdvancedTabContent.prototype.gasInputError.getCall(0).args + assert.deepEqual(gasInputErrorArgs, [{ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: true, + value: 321, + isSpeedUp: false, + }]) + }) + }) + + describe('gasInputError()', () => { + let gasInputError + + beforeEach(() => { + AdvancedTabContent.prototype.renderGasEditRow.resetHistory() + gasInputError = wrapper.instance().gasInputError({ + labelKey: '', + insufficientBalance: false, + customPriceIsSafe: true, + isSpeedUp: false, + }) + }) + + it('should return an insufficientBalance error', () => { + const gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: true, + customPriceIsSafe: true, + isSpeedUp: false, + value: 1, + }) + assert.deepEqual(gasInputError, { + isInError: true, + errorText: 'insufficientBalance', + errorType: 'error', + }) + }) + + it('should return a zero gas on retry error', () => { + const gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: false, + isSpeedUp: true, + value: 0, + }) + assert.deepEqual(gasInputError, { + isInError: true, + errorText: 'zeroGasPriceOnSpeedUpError', + errorType: 'error', + }) + }) + + it('should return a low gas warning', () => { + const gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: false, + isSpeedUp: false, + value: 1, + }) + assert.deepEqual(gasInputError, { + isInError: true, + errorText: 'gasPriceExtremelyLow', + errorType: 'warning', + }) + }) + + it('should return isInError false if there is no error', () => { + gasInputError = wrapper.instance().gasInputError({ + labelKey: 'gasPrice', + insufficientBalance: false, + customPriceIsSafe: true, + value: 1, + }) + assert.equal(gasInputError.isInError, false) + }) + }) + +}) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js new file mode 100644 index 000000000..61b681e1a --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.js @@ -0,0 +1 @@ +export { default } from './time-remaining.component' diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss new file mode 100644 index 000000000..e2115af7f --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/index.scss @@ -0,0 +1,17 @@ +.time-remaining { + color: #313A5E; + font-size: 16px; + + .minutes-num, .seconds-num { + font-size: 16px; + } + + .seconds-num { + margin-left: 7px; + font-size: 16px; + } + + .minutes-label, .seconds-label { + font-size: 16px; + } +}
\ No newline at end of file diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js new file mode 100644 index 000000000..17f0345d5 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/tests/time-remaining-component.test.js @@ -0,0 +1,30 @@ +import React from 'react' +import assert from 'assert' +import shallow from '../../../../../../../../lib/shallow-with-context' +import TimeRemaining from '../time-remaining.component.js' + +describe('TimeRemaining Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(<TimeRemaining + milliseconds={495000} + />) + }) + + describe('render()', () => { + it('should render the time-remaining root node', () => { + assert(wrapper.hasClass('time-remaining')) + }) + + it('should render minutes and seconds numbers and labels', () => { + const timeRemainingChildren = wrapper.children() + assert.equal(timeRemainingChildren.length, 4) + assert.equal(timeRemainingChildren.at(0).text(), 8) + assert.equal(timeRemainingChildren.at(1).text(), 'minutesShorthand') + assert.equal(timeRemainingChildren.at(2).text(), 15) + assert.equal(timeRemainingChildren.at(3).text(), 'secondsShorthand') + }) + }) + +}) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js new file mode 100644 index 000000000..826d41f9c --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.component.js @@ -0,0 +1,33 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { getTimeBreakdown } from './time-remaining.utils' + +export default class TimeRemaining extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + milliseconds: PropTypes.number, + } + + render () { + const { + milliseconds, + } = this.props + + const { + minutes, + seconds, + } = getTimeBreakdown(milliseconds) + + return ( + <div className="time-remaining"> + <span className="minutes-num">{minutes}</span> + <span className="minutes-label">{this.context.t('minutesShorthand')}</span> + <span className="seconds-num">{seconds}</span> + <span className="seconds-label">{this.context.t('secondsShorthand')}</span> + </div> + ) + } +} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js new file mode 100644 index 000000000..cf43e0acb --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/time-remaining/time-remaining.utils.js @@ -0,0 +1,11 @@ +function getTimeBreakdown (milliseconds) { + return { + hours: Math.floor(milliseconds / 3600000), + minutes: Math.floor((milliseconds % 3600000) / 60000), + seconds: Math.floor((milliseconds % 60000) / 1000), + } +} + +module.exports = { + getTimeBreakdown, +} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js new file mode 100644 index 000000000..5f3925fa5 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/basic-tab-content.component.js @@ -0,0 +1,35 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Loading from '../../../../ui/loading-screen' +import GasPriceButtonGroup from '../../gas-price-button-group' + +export default class BasicTabContent extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + gasPriceButtonGroupProps: PropTypes.object, + } + + render () { + const { t } = this.context + const { gasPriceButtonGroupProps } = this.props + + return ( + <div className="basic-tab-content"> + <div className="basic-tab-content__title">{ t('estimatedProcessingTimes') }</div> + <div className="basic-tab-content__blurb">{ t('selectAHigherGasFee') }</div> + {!gasPriceButtonGroupProps.loading + ? <GasPriceButtonGroup + className="gas-price-button-group--alt" + showCheck={true} + {...gasPriceButtonGroupProps} + /> + : <Loading /> + } + <div className="basic-tab-content__footer-blurb">{ t('acceleratingATransaction') }</div> + </div> + ) + } +} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.js b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.js new file mode 100644 index 000000000..078d50fce --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.js @@ -0,0 +1 @@ +export { default } from './basic-tab-content.component' diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.scss new file mode 100644 index 000000000..e34e4e328 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/index.scss @@ -0,0 +1,28 @@ +.basic-tab-content { + display: flex; + flex-direction: column; + align-items: flex-start; + padding-left: 21px; + height: 324px; + background: #F5F7F8; + border-bottom: 1px solid #d2d8dd; + + &__title { + margin-top: 19px; + font-size: 16px; + color: $black; + } + + &__blurb { + font-size: 12px; + color: $black; + margin-top: 5px; + margin-bottom: 15px; + } + + &__footer-blurb { + font-size: 12px; + color: #979797; + margin-top: 15px; + } +} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js new file mode 100644 index 000000000..0989ac677 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/basic-tab-content/tests/basic-tab-content-component.test.js @@ -0,0 +1,82 @@ +import React from 'react' +import assert from 'assert' +import shallow from '../../../../../../../lib/shallow-with-context' +import BasicTabContent from '../basic-tab-content.component' + +import GasPriceButtonGroup from '../../../gas-price-button-group' +import Loading from '../../../../../ui/loading-screen' + +const mockGasPriceButtonGroupProps = { + buttonDataLoading: false, + className: 'gas-price-button-group', + gasButtonInfo: [ + { + feeInPrimaryCurrency: '$0.52', + feeInSecondaryCurrency: '0.0048 ETH', + timeEstimate: '~ 1 min 0 sec', + priceInHexWei: '0xa1b2c3f', + }, + { + feeInPrimaryCurrency: '$0.39', + feeInSecondaryCurrency: '0.004 ETH', + timeEstimate: '~ 1 min 30 sec', + priceInHexWei: '0xa1b2c39', + }, + { + feeInPrimaryCurrency: '$0.30', + feeInSecondaryCurrency: '0.00354 ETH', + timeEstimate: '~ 2 min 1 sec', + priceInHexWei: '0xa1b2c30', + }, + ], + handleGasPriceSelection: newPrice => console.log('NewPrice: ', newPrice), + noButtonActiveByDefault: true, + showCheck: true, +} + +describe('BasicTabContent Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(<BasicTabContent + gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} + />) + }) + + describe('render', () => { + it('should have a title', () => { + assert(wrapper.find('.basic-tab-content').childAt(0).hasClass('basic-tab-content__title')) + }) + + it('should render a GasPriceButtonGroup compenent', () => { + assert.equal(wrapper.find(GasPriceButtonGroup).length, 1) + }) + + it('should pass correct props to GasPriceButtonGroup', () => { + const { + buttonDataLoading, + className, + gasButtonInfo, + handleGasPriceSelection, + noButtonActiveByDefault, + showCheck, + } = wrapper.find(GasPriceButtonGroup).props() + assert.equal(wrapper.find(GasPriceButtonGroup).length, 1) + assert.equal(buttonDataLoading, mockGasPriceButtonGroupProps.buttonDataLoading) + assert.equal(className, mockGasPriceButtonGroupProps.className) + assert.equal(noButtonActiveByDefault, mockGasPriceButtonGroupProps.noButtonActiveByDefault) + assert.equal(showCheck, mockGasPriceButtonGroupProps.showCheck) + assert.deepEqual(gasButtonInfo, mockGasPriceButtonGroupProps.gasButtonInfo) + assert.equal(JSON.stringify(handleGasPriceSelection), JSON.stringify(mockGasPriceButtonGroupProps.handleGasPriceSelection)) + }) + + it('should render a loading component instead of the GasPriceButtonGroup if gasPriceButtonGroupProps.loading is true', () => { + wrapper.setProps({ + gasPriceButtonGroupProps: { ...mockGasPriceButtonGroupProps, loading: true }, + }) + + assert.equal(wrapper.find(GasPriceButtonGroup).length, 0) + assert.equal(wrapper.find(Loading).length, 1) + }) + }) +}) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js new file mode 100644 index 000000000..d242f59f5 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js @@ -0,0 +1,186 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import PageContainer from '../../../ui/page-container' +import { Tabs, Tab } from '../../../ui/tabs' +import AdvancedTabContent from './advanced-tab-content' +import BasicTabContent from './basic-tab-content' + +export default class GasModalPageContainer extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + hideModal: PropTypes.func, + hideBasic: PropTypes.bool, + updateCustomGasPrice: PropTypes.func, + updateCustomGasLimit: PropTypes.func, + customGasPrice: PropTypes.number, + customGasLimit: PropTypes.number, + fetchBasicGasAndTimeEstimates: PropTypes.func, + fetchGasEstimates: PropTypes.func, + gasPriceButtonGroupProps: PropTypes.object, + infoRowProps: PropTypes.shape({ + originalTotalFiat: PropTypes.string, + originalTotalEth: PropTypes.string, + newTotalFiat: PropTypes.string, + newTotalEth: PropTypes.string, + }), + onSubmit: PropTypes.func, + customModalGasPriceInHex: PropTypes.string, + customModalGasLimitInHex: PropTypes.string, + cancelAndClose: PropTypes.func, + transactionFee: PropTypes.string, + blockTime: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), + customPriceIsSafe: PropTypes.bool, + isSpeedUp: PropTypes.bool, + disableSave: PropTypes.bool, + } + + state = {} + + componentDidMount () { + const promise = this.props.hideBasic + ? Promise.resolve(this.props.blockTime) + : this.props.fetchBasicGasAndTimeEstimates() + .then(basicEstimates => basicEstimates.blockTime) + + promise + .then(blockTime => { + this.props.fetchGasEstimates(blockTime) + }) + } + + renderBasicTabContent (gasPriceButtonGroupProps) { + return ( + <BasicTabContent + gasPriceButtonGroupProps={gasPriceButtonGroupProps} + /> + ) + } + + renderAdvancedTabContent ({ + convertThenUpdateCustomGasPrice, + convertThenUpdateCustomGasLimit, + customGasPrice, + customGasLimit, + newTotalFiat, + gasChartProps, + currentTimeEstimate, + insufficientBalance, + gasEstimatesLoading, + customPriceIsSafe, + isSpeedUp, + transactionFee, + }) { + return ( + <AdvancedTabContent + updateCustomGasPrice={convertThenUpdateCustomGasPrice} + updateCustomGasLimit={convertThenUpdateCustomGasLimit} + customGasPrice={customGasPrice} + customGasLimit={customGasLimit} + timeRemaining={currentTimeEstimate} + transactionFee={transactionFee} + totalFee={newTotalFiat} + gasChartProps={gasChartProps} + insufficientBalance={insufficientBalance} + gasEstimatesLoading={gasEstimatesLoading} + customPriceIsSafe={customPriceIsSafe} + isSpeedUp={isSpeedUp} + /> + ) + } + + renderInfoRows (newTotalFiat, newTotalEth, sendAmount, transactionFee) { + return ( + <div className="gas-modal-content__info-row-wrapper"> + <div className="gas-modal-content__info-row"> + <div className="gas-modal-content__info-row__send-info"> + <span className="gas-modal-content__info-row__send-info__label">{this.context.t('sendAmount')}</span> + <span className="gas-modal-content__info-row__send-info__value">{sendAmount}</span> + </div> + <div className="gas-modal-content__info-row__transaction-info"> + <span className={'gas-modal-content__info-row__transaction-info__label'}>{this.context.t('transactionFee')}</span> + <span className="gas-modal-content__info-row__transaction-info__value">{transactionFee}</span> + </div> + <div className="gas-modal-content__info-row__total-info"> + <span className="gas-modal-content__info-row__total-info__label">{this.context.t('newTotal')}</span> + <span className="gas-modal-content__info-row__total-info__value">{newTotalEth}</span> + </div> + <div className="gas-modal-content__info-row__fiat-total-info"> + <span className="gas-modal-content__info-row__fiat-total-info__value">{newTotalFiat}</span> + </div> + </div> + </div> + ) + } + + renderTabs ({ + originalTotalFiat, + originalTotalEth, + newTotalFiat, + newTotalEth, + sendAmount, + transactionFee, + }, + { + gasPriceButtonGroupProps, + hideBasic, + ...advancedTabProps + }) { + let tabsToRender = [ + { name: 'basic', content: this.renderBasicTabContent(gasPriceButtonGroupProps) }, + { name: 'advanced', content: this.renderAdvancedTabContent({ transactionFee, ...advancedTabProps }) }, + ] + + if (hideBasic) { + tabsToRender = tabsToRender.slice(1) + } + + return ( + <Tabs> + {tabsToRender.map(({ name, content }, i) => <Tab name={this.context.t(name)} key={`gas-modal-tab-${i}`}> + <div className="gas-modal-content"> + { content } + { this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) } + </div> + </Tab> + )} + </Tabs> + ) + } + + render () { + const { + cancelAndClose, + infoRowProps, + onSubmit, + customModalGasPriceInHex, + customModalGasLimitInHex, + disableSave, + ...tabProps + } = this.props + + return ( + <div className="gas-modal-page-container"> + <PageContainer + title={this.context.t('customGas')} + subtitle={this.context.t('customGasSubTitle')} + tabsComponent={this.renderTabs(infoRowProps, tabProps)} + disabled={disableSave} + onCancel={() => cancelAndClose()} + onClose={() => cancelAndClose()} + onSubmit={() => { + onSubmit(customModalGasLimitInHex, customModalGasPriceInHex) + }} + submitText={this.context.t('save')} + headerCloseText={this.context.t('close')} + hideCancel={true} + /> + </div> + ) + } +} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js new file mode 100644 index 000000000..d541056f4 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js @@ -0,0 +1,295 @@ +import { connect } from 'react-redux' +import { pipe, partialRight } from 'ramda' +import GasModalPageContainer from './gas-modal-page-container.component' +import { + hideModal, + setGasLimit, + setGasPrice, + createSpeedUpTransaction, + hideSidebar, +} from '../../../../store/actions' +import { + setCustomGasPrice, + setCustomGasLimit, + resetCustomData, + setCustomTimeEstimate, + fetchGasEstimates, + fetchBasicGasAndTimeEstimates, +} from '../../../../ducks/gas/gas.duck' +import { + hideGasButtonGroup, +} from '../../../../ducks/send/send.duck' +import { + updateGasAndCalculate, +} from '../../../../ducks/confirm-transaction/confirm-transaction.duck' +import { + conversionRateSelector as getConversionRate, + getCurrentCurrency, + getCurrentEthBalance, + getIsMainnet, + getSelectedToken, + isEthereumNetwork, + preferencesSelector, +} from '../../../../selectors/selectors.js' +import { + formatTimeEstimate, + getFastPriceEstimateInHexWEI, + getBasicGasEstimateLoadingStatus, + getGasEstimatesLoadingStatus, + getCustomGasLimit, + getCustomGasPrice, + getDefaultActiveButtonIndex, + getEstimatedGasPrices, + getEstimatedGasTimes, + getRenderableBasicEstimateData, + getBasicGasEstimateBlockTime, + isCustomPriceSafe, +} from '../../../../selectors/custom-gas' +import { + submittedPendingTransactionsSelector, +} from '../../../../selectors/transactions' +import { + formatCurrency, +} from '../../../../helpers/utils/confirm-tx.util' +import { + addHexWEIsToDec, + decEthToConvertedCurrency as ethTotalToConvertedCurrency, + decGWEIToHexWEI, + hexWEIToDecGWEI, +} from '../../../../helpers/utils/conversions.util' +import { + formatETHFee, +} from '../../../../helpers/utils/formatters' +import { + calcGasTotal, + isBalanceSufficient, +} from '../../send/send.utils' +import { addHexPrefix } from 'ethereumjs-util' +import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils' + +const mapStateToProps = (state, ownProps) => { + const { transaction = {} } = ownProps + const buttonDataLoading = getBasicGasEstimateLoadingStatus(state) + const gasEstimatesLoading = getGasEstimatesLoadingStatus(state) + + const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, transaction.id) + const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice + const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit + const gasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) + + const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex) + + const gasButtonInfo = getRenderableBasicEstimateData(state, customModalGasLimitInHex) + + const currentCurrency = getCurrentCurrency(state) + const conversionRate = getConversionRate(state) + + const newTotalFiat = addHexWEIsToRenderableFiat(value, customGasTotal, currentCurrency, conversionRate) + + const hideBasic = state.appState.modal.modalState.props.hideBasic + + const customGasPrice = calcCustomGasPrice(customModalGasPriceInHex) + + const gasPrices = getEstimatedGasPrices(state) + const estimatedTimes = getEstimatedGasTimes(state) + const balance = getCurrentEthBalance(state) + + const { showFiatInTestnets } = preferencesSelector(state) + const isMainnet = getIsMainnet(state) + const showFiat = Boolean(isMainnet || showFiatInTestnets) + + const insufficientBalance = !isBalanceSufficient({ + amount: value, + gasTotal, + balance, + conversionRate, + }) + + return { + hideBasic, + isConfirm: isConfirm(state), + customModalGasPriceInHex, + customModalGasLimitInHex, + customGasPrice, + customGasLimit: calcCustomGasLimit(customModalGasLimitInHex), + newTotalFiat, + currentTimeEstimate: getRenderableTimeEstimate(customGasPrice, gasPrices, estimatedTimes), + blockTime: getBasicGasEstimateBlockTime(state), + customPriceIsSafe: isCustomPriceSafe(state), + gasPriceButtonGroupProps: { + buttonDataLoading, + defaultActiveButtonIndex: getDefaultActiveButtonIndex(gasButtonInfo, customModalGasPriceInHex), + gasButtonInfo, + }, + gasChartProps: { + currentPrice: customGasPrice, + gasPrices, + estimatedTimes, + gasPricesMax: gasPrices[gasPrices.length - 1], + estimatedTimesMax: estimatedTimes[0], + }, + infoRowProps: { + originalTotalFiat: addHexWEIsToRenderableFiat(value, gasTotal, currentCurrency, conversionRate), + originalTotalEth: addHexWEIsToRenderableEth(value, gasTotal), + newTotalFiat: showFiat ? newTotalFiat : '', + newTotalEth: addHexWEIsToRenderableEth(value, customGasTotal), + transactionFee: addHexWEIsToRenderableEth('0x0', customGasTotal), + sendAmount: addHexWEIsToRenderableEth(value, '0x0'), + }, + isSpeedUp: transaction.status === 'submitted', + txId: transaction.id, + insufficientBalance, + gasEstimatesLoading, + isMainnet, + isEthereumNetwork: isEthereumNetwork(state), + } +} + +const mapDispatchToProps = dispatch => { + const updateCustomGasPrice = newPrice => dispatch(setCustomGasPrice(addHexPrefix(newPrice))) + + return { + cancelAndClose: () => { + dispatch(resetCustomData()) + dispatch(hideModal()) + }, + hideModal: () => dispatch(hideModal()), + updateCustomGasPrice, + convertThenUpdateCustomGasPrice: newPrice => updateCustomGasPrice(decGWEIToHexWEI(newPrice)), + convertThenUpdateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit.toString(16)))), + setGasData: (newLimit, newPrice) => { + dispatch(setGasLimit(newLimit)) + dispatch(setGasPrice(newPrice)) + }, + updateConfirmTxGasAndCalculate: (gasLimit, gasPrice) => { + updateCustomGasPrice(gasPrice) + dispatch(setCustomGasLimit(addHexPrefix(gasLimit.toString(16)))) + return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) + }, + createSpeedUpTransaction: (txId, gasPrice) => { + return dispatch(createSpeedUpTransaction(txId, gasPrice)) + }, + hideGasButtonGroup: () => dispatch(hideGasButtonGroup()), + setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)), + hideSidebar: () => dispatch(hideSidebar()), + fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), + fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { gasPriceButtonGroupProps, isConfirm, txId, isSpeedUp, insufficientBalance, customGasPrice } = stateProps + const { + updateCustomGasPrice: dispatchUpdateCustomGasPrice, + hideGasButtonGroup: dispatchHideGasButtonGroup, + setGasData: dispatchSetGasData, + updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate, + createSpeedUpTransaction: dispatchCreateSpeedUpTransaction, + hideSidebar: dispatchHideSidebar, + cancelAndClose: dispatchCancelAndClose, + hideModal: dispatchHideModal, + ...otherDispatchProps + } = dispatchProps + + return { + ...stateProps, + ...otherDispatchProps, + ...ownProps, + onSubmit: (gasLimit, gasPrice) => { + if (isConfirm) { + dispatchUpdateConfirmTxGasAndCalculate(gasLimit, gasPrice) + dispatchHideModal() + } else if (isSpeedUp) { + dispatchCreateSpeedUpTransaction(txId, gasPrice) + dispatchHideSidebar() + dispatchCancelAndClose() + } else { + dispatchSetGasData(gasLimit, gasPrice) + dispatchHideGasButtonGroup() + dispatchCancelAndClose() + } + }, + gasPriceButtonGroupProps: { + ...gasPriceButtonGroupProps, + handleGasPriceSelection: dispatchUpdateCustomGasPrice, + }, + cancelAndClose: () => { + dispatchCancelAndClose() + if (isSpeedUp) { + dispatchHideSidebar() + } + }, + disableSave: insufficientBalance || (isSpeedUp && customGasPrice === 0), + } +} + +export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(GasModalPageContainer) + +function isConfirm (state) { + return Boolean(Object.keys(state.confirmTransaction.txData).length) +} + +function calcCustomGasPrice (customGasPriceInHex) { + return Number(hexWEIToDecGWEI(customGasPriceInHex)) +} + +function calcCustomGasLimit (customGasLimitInHex) { + return parseInt(customGasLimitInHex, 16) +} + +function getTxParams (state, transactionId) { + const { confirmTransaction: { txData }, metamask: { send } } = state + const pendingTransactions = submittedPendingTransactionsSelector(state) + const pendingTransaction = pendingTransactions.find(({ id }) => id === transactionId) + const { txParams: pendingTxParams } = pendingTransaction || {} + return txData.txParams || pendingTxParams || { + from: send.from, + gas: send.gasLimit || '0x5208', + gasPrice: send.gasPrice || getFastPriceEstimateInHexWEI(state, true), + to: send.to, + value: getSelectedToken(state) ? '0x0' : send.amount, + } +} + +function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) { + return pipe( + addHexWEIsToDec, + formatETHFee + )(aHexWEI, bHexWEI) +} + +function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conversionRate) { + return pipe( + addHexWEIsToDec, + partialRight(ethTotalToConvertedCurrency, [convertedCurrency, conversionRate]), + partialRight(formatCurrency, [convertedCurrency]), + )(aHexWEI, bHexWEI) +} + +function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) { + const minGasPrice = gasPrices[0] + const maxGasPrice = gasPrices[gasPrices.length - 1] + let priceForEstimation = currentGasPrice + if (currentGasPrice < minGasPrice) { + priceForEstimation = minGasPrice + } else if (currentGasPrice > maxGasPrice) { + priceForEstimation = maxGasPrice + } + + const { + closestLowerValueIndex, + closestHigherValueIndex, + closestHigherValue, + closestLowerValue, + } = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation }) + + const newTimeEstimate = extrapolateY({ + higherY: estimatedTimes[closestHigherValueIndex], + lowerY: estimatedTimes[closestLowerValueIndex], + higherX: closestHigherValue, + lowerX: closestLowerValue, + xForExtrapolation: priceForEstimation, + }) + + return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice) +} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/index.js b/ui/app/components/app/gas-customization/gas-modal-page-container/index.js new file mode 100644 index 000000000..ec0ebad22 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/index.js @@ -0,0 +1 @@ +export { default } from './gas-modal-page-container.container' diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss b/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss new file mode 100644 index 000000000..b9e0f59c4 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/index.scss @@ -0,0 +1,146 @@ +@import './advanced-tab-content/index'; +@import './basic-tab-content/index'; + +.gas-modal-page-container { + .page-container { + max-width: 391px; + min-height: 585px; + overflow-y: initial; + + @media screen and (max-width: $break-small) { + &__content { + display: flex; + overflow-y: initial; + } + } + + &__header { + padding: 0px; + padding-top: 16px; + + &--no-padding-bottom { + padding-bottom: 0; + } + } + + &__footer { + header { + padding-top: 12px; + padding-bottom: 12px; + } + } + + &__header-close-text { + font-size: 14px; + color: #4EADE7; + position: absolute; + top: 16px; + right: 16px; + cursor: pointer; + overflow: hidden; + } + + &__title { + color: $black; + font-size: 16px; + font-weight: 500; + line-height: 16px; + display: flex; + justify-content: center; + align-items: flex-start; + margin-right: 0; + } + + &__subtitle { + display: none; + } + + &__tabs { + margin-top: 0px; + } + + &__tab { + width: 100%; + font-size: 14px; + + &:last-of-type { + margin-right: 0; + } + + &--selected { + color: $curious-blue; + border-bottom: 2px solid $curious-blue; + } + } + } +} + +.gas-modal-content { + @media screen and (max-width: $break-small) { + width: 100%; + } + + &__basic-tab { + height: 219px; + } + + + &__info-row, &__info-row--fade { + width: 100%; + background: $polar; + padding: 15px 21px; + display: flex; + flex-flow: column; + color: $scorpion; + font-size: 12px; + + @media screen and (max-width: $break-small) { + padding: 4px 21px; + } + + &__send-info, &__transaction-info, &__total-info, &__fiat-total-info { + display: flex; + flex-flow: row; + justify-content: space-between; + } + + &__fiat-total-info { + justify-content: flex-end; + } + + &__total-info { + &__label { + font-size: 16px; + + @media screen and (max-width: $break-small) { + font-size: 14px; + } + } + + &__value { + font-size: 16px; + font-weight: bold; + + @media screen and (max-width: $break-small) { + font-size: 14px; + } + } + } + + &__transaction-info, &__send-info { + &__label { + font-size: 12px; + } + + &__value { + font-size: 14px; + } + } + } + + &__info-row--fade { + background: white; + color: $dusty-gray; + border-top: 1px solid $mischka; + } +} diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js new file mode 100644 index 000000000..7557eefe5 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-component.test.js @@ -0,0 +1,274 @@ +import React from 'react' +import assert from 'assert' +import shallow from '../../../../../../lib/shallow-with-context' +import sinon from 'sinon' +import GasModalPageContainer from '../gas-modal-page-container.component.js' +import timeout from '../../../../../../lib/test-timeout' + +import PageContainer from '../../../../ui/page-container' + +import { Tab } from '../../../../ui/tabs' + +const mockBasicGasEstimates = { + blockTime: 'mockBlockTime', +} + +const propsMethodSpies = { + cancelAndClose: sinon.spy(), + onSubmit: sinon.spy(), + fetchBasicGasAndTimeEstimates: sinon.stub().returns(Promise.resolve(mockBasicGasEstimates)), + fetchGasEstimates: sinon.spy(), +} + +const mockGasPriceButtonGroupProps = { + buttonDataLoading: false, + className: 'gas-price-button-group', + gasButtonInfo: [ + { + feeInPrimaryCurrency: '$0.52', + feeInSecondaryCurrency: '0.0048 ETH', + timeEstimate: '~ 1 min 0 sec', + priceInHexWei: '0xa1b2c3f', + }, + { + feeInPrimaryCurrency: '$0.39', + feeInSecondaryCurrency: '0.004 ETH', + timeEstimate: '~ 1 min 30 sec', + priceInHexWei: '0xa1b2c39', + }, + { + feeInPrimaryCurrency: '$0.30', + feeInSecondaryCurrency: '0.00354 ETH', + timeEstimate: '~ 2 min 1 sec', + priceInHexWei: '0xa1b2c30', + }, + ], + handleGasPriceSelection: 'mockSelectionFunction', + noButtonActiveByDefault: true, + showCheck: true, + newTotalFiat: 'mockNewTotalFiat', + newTotalEth: 'mockNewTotalEth', +} +const mockInfoRowProps = { + originalTotalFiat: 'mockOriginalTotalFiat', + originalTotalEth: 'mockOriginalTotalEth', + newTotalFiat: 'mockNewTotalFiat', + newTotalEth: 'mockNewTotalEth', + sendAmount: 'mockSendAmount', + transactionFee: 'mockTransactionFee', +} + +const GP = GasModalPageContainer.prototype +describe('GasModalPageContainer Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(<GasModalPageContainer + cancelAndClose={propsMethodSpies.cancelAndClose} + onSubmit={propsMethodSpies.onSubmit} + fetchBasicGasAndTimeEstimates={propsMethodSpies.fetchBasicGasAndTimeEstimates} + fetchGasEstimates={propsMethodSpies.fetchGasEstimates} + updateCustomGasPrice={() => 'mockupdateCustomGasPrice'} + updateCustomGasLimit={() => 'mockupdateCustomGasLimit'} + customGasPrice={21} + customGasLimit={54321} + gasPriceButtonGroupProps={mockGasPriceButtonGroupProps} + infoRowProps={mockInfoRowProps} + currentTimeEstimate={'1 min 31 sec'} + customGasPriceInHex={'mockCustomGasPriceInHex'} + customGasLimitInHex={'mockCustomGasLimitInHex'} + insufficientBalance={false} + disableSave={false} + />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) + }) + + afterEach(() => { + propsMethodSpies.cancelAndClose.resetHistory() + }) + + describe('componentDidMount', () => { + it('should call props.fetchBasicGasAndTimeEstimates', () => { + propsMethodSpies.fetchBasicGasAndTimeEstimates.resetHistory() + assert.equal(propsMethodSpies.fetchBasicGasAndTimeEstimates.callCount, 0) + wrapper.instance().componentDidMount() + assert.equal(propsMethodSpies.fetchBasicGasAndTimeEstimates.callCount, 1) + }) + + it('should call props.fetchGasEstimates with the block time returned by fetchBasicGasAndTimeEstimates', async () => { + propsMethodSpies.fetchGasEstimates.resetHistory() + assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 0) + wrapper.instance().componentDidMount() + await timeout(250) + assert.equal(propsMethodSpies.fetchGasEstimates.callCount, 1) + assert.equal(propsMethodSpies.fetchGasEstimates.getCall(0).args[0], 'mockBlockTime') + }) + }) + + describe('render', () => { + it('should render a PageContainer compenent', () => { + assert.equal(wrapper.find(PageContainer).length, 1) + }) + + it('should pass correct props to PageContainer', () => { + const { + title, + subtitle, + disabled, + } = wrapper.find(PageContainer).props() + assert.equal(title, 'customGas') + assert.equal(subtitle, 'customGasSubTitle') + assert.equal(disabled, false) + }) + + it('should pass the correct onCancel and onClose methods to PageContainer', () => { + const { + onCancel, + onClose, + } = wrapper.find(PageContainer).props() + assert.equal(propsMethodSpies.cancelAndClose.callCount, 0) + onCancel() + assert.equal(propsMethodSpies.cancelAndClose.callCount, 1) + onClose() + assert.equal(propsMethodSpies.cancelAndClose.callCount, 2) + }) + + it('should pass the correct renderTabs property to PageContainer', () => { + sinon.stub(GP, 'renderTabs').returns('mockTabs') + const renderTabsWrapperTester = shallow(<GasModalPageContainer + fetchBasicGasAndTimeEstimates={propsMethodSpies.fetchBasicGasAndTimeEstimates} + fetchGasEstimates={propsMethodSpies.fetchGasEstimates} + />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }) + const { tabsComponent } = renderTabsWrapperTester.find(PageContainer).props() + assert.equal(tabsComponent, 'mockTabs') + GasModalPageContainer.prototype.renderTabs.restore() + }) + }) + + describe('renderTabs', () => { + beforeEach(() => { + sinon.spy(GP, 'renderBasicTabContent') + sinon.spy(GP, 'renderAdvancedTabContent') + sinon.spy(GP, 'renderInfoRows') + }) + + afterEach(() => { + GP.renderBasicTabContent.restore() + GP.renderAdvancedTabContent.restore() + GP.renderInfoRows.restore() + }) + + it('should render a Tabs component with "Basic" and "Advanced" tabs', () => { + const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, { + gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, + otherProps: 'mockAdvancedTabProps', + }) + const renderedTabs = shallow(renderTabsResult) + assert.equal(renderedTabs.props().className, 'tabs') + + const tabs = renderedTabs.find(Tab) + assert.equal(tabs.length, 2) + + assert.equal(tabs.at(0).props().name, 'basic') + assert.equal(tabs.at(1).props().name, 'advanced') + + assert.equal(tabs.at(0).childAt(0).props().className, 'gas-modal-content') + assert.equal(tabs.at(1).childAt(0).props().className, 'gas-modal-content') + }) + + it('should call renderBasicTabContent and renderAdvancedTabContent with the expected props', () => { + assert.equal(GP.renderBasicTabContent.callCount, 0) + assert.equal(GP.renderAdvancedTabContent.callCount, 0) + + wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' }) + + assert.equal(GP.renderBasicTabContent.callCount, 1) + assert.equal(GP.renderAdvancedTabContent.callCount, 1) + + assert.deepEqual(GP.renderBasicTabContent.getCall(0).args[0], mockGasPriceButtonGroupProps) + assert.deepEqual(GP.renderAdvancedTabContent.getCall(0).args[0], { transactionFee: 'mockTransactionFee', otherProps: 'mockAdvancedTabProps' }) + }) + + it('should call renderInfoRows with the expected props', () => { + assert.equal(GP.renderInfoRows.callCount, 0) + + wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' }) + + assert.equal(GP.renderInfoRows.callCount, 2) + + assert.deepEqual(GP.renderInfoRows.getCall(0).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee']) + assert.deepEqual(GP.renderInfoRows.getCall(1).args, ['mockNewTotalFiat', 'mockNewTotalEth', 'mockSendAmount', 'mockTransactionFee']) + }) + + it('should not render the basic tab if hideBasic is true', () => { + const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, { + gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, + otherProps: 'mockAdvancedTabProps', + hideBasic: true, + }) + + const renderedTabs = shallow(renderTabsResult) + const tabs = renderedTabs.find(Tab) + assert.equal(tabs.length, 1) + assert.equal(tabs.at(0).props().name, 'advanced') + }) + }) + + describe('renderBasicTabContent', () => { + it('should render', () => { + const renderBasicTabContentResult = wrapper.instance().renderBasicTabContent(mockGasPriceButtonGroupProps) + + assert.deepEqual( + renderBasicTabContentResult.props.gasPriceButtonGroupProps, + mockGasPriceButtonGroupProps + ) + }) + }) + + describe('renderAdvancedTabContent', () => { + it('should render with the correct props', () => { + const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent({ + convertThenUpdateCustomGasPrice: () => 'mockConvertThenUpdateCustomGasPrice', + convertThenUpdateCustomGasLimit: () => 'mockConvertThenUpdateCustomGasLimit', + customGasPrice: 123, + customGasLimit: 456, + newTotalFiat: '$0.30', + currentTimeEstimate: '1 min 31 sec', + gasEstimatesLoading: 'mockGasEstimatesLoading', + }) + const advancedTabContentProps = renderAdvancedTabContentResult.props + assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice') + assert.equal(advancedTabContentProps.updateCustomGasLimit(), 'mockConvertThenUpdateCustomGasLimit') + assert.equal(advancedTabContentProps.customGasPrice, 123) + assert.equal(advancedTabContentProps.customGasLimit, 456) + assert.equal(advancedTabContentProps.timeRemaining, '1 min 31 sec') + assert.equal(advancedTabContentProps.totalFee, '$0.30') + assert.equal(advancedTabContentProps.gasEstimatesLoading, 'mockGasEstimatesLoading') + }) + }) + + describe('renderInfoRows', () => { + it('should render the info rows with the passed data', () => { + const baseClassName = 'gas-modal-content__info-row' + const renderedInfoRowsContainer = shallow(wrapper.instance().renderInfoRows( + 'mockNewTotalFiat', + ' mockNewTotalEth', + ' mockSendAmount', + ' mockTransactionFee' + )) + + assert(renderedInfoRowsContainer.childAt(0).hasClass(baseClassName)) + + const renderedInfoRows = renderedInfoRowsContainer.childAt(0).children() + assert.equal(renderedInfoRows.length, 4) + assert(renderedInfoRows.at(0).hasClass(`${baseClassName}__send-info`)) + assert(renderedInfoRows.at(1).hasClass(`${baseClassName}__transaction-info`)) + assert(renderedInfoRows.at(2).hasClass(`${baseClassName}__total-info`)) + assert(renderedInfoRows.at(3).hasClass(`${baseClassName}__fiat-total-info`)) + + assert.equal(renderedInfoRows.at(0).text(), 'sendAmount mockSendAmount') + assert.equal(renderedInfoRows.at(1).text(), 'transactionFee mockTransactionFee') + assert.equal(renderedInfoRows.at(2).text(), 'newTotal mockNewTotalEth') + assert.equal(renderedInfoRows.at(3).text(), 'mockNewTotalFiat') + }) + }) +}) diff --git a/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js new file mode 100644 index 000000000..b9eb67d2b --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js @@ -0,0 +1,431 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +let mapStateToProps +let mapDispatchToProps +let mergeProps + +const actionSpies = { + hideModal: sinon.spy(), + setGasLimit: sinon.spy(), + setGasPrice: sinon.spy(), +} + +const gasActionSpies = { + setCustomGasPrice: sinon.spy(), + setCustomGasLimit: sinon.spy(), + resetCustomData: sinon.spy(), +} + +const confirmTransactionActionSpies = { + updateGasAndCalculate: sinon.spy(), +} + +const sendActionSpies = { + hideGasButtonGroup: sinon.spy(), +} + +proxyquire('../gas-modal-page-container.container.js', { + 'react-redux': { + connect: (ms, md, mp) => { + mapStateToProps = ms + mapDispatchToProps = md + mergeProps = mp + return () => ({}) + }, + }, + '../../../../selectors/custom-gas': { + getBasicGasEstimateLoadingStatus: (s) => `mockBasicGasEstimateLoadingStatus:${Object.keys(s).length}`, + getRenderableBasicEstimateData: (s) => `mockRenderableBasicEstimateData:${Object.keys(s).length}`, + getDefaultActiveButtonIndex: (a, b) => a + b, + }, + '../../../../store/actions': actionSpies, + '../../../../ducks/gas/gas.duck': gasActionSpies, + '../../../../ducks/confirm-transaction/confirm-transaction.duck': confirmTransactionActionSpies, + '../../../../ducks/send/send.duck': sendActionSpies, + '../../../../selectors/selectors.js': { + getCurrentEthBalance: (state) => state.metamask.balance || '0x0', + }, +}) + +describe('gas-modal-page-container container', () => { + + describe('mapStateToProps()', () => { + it('should map the correct properties to props', () => { + const baseMockState = { + appState: { + modal: { + modalState: { + props: { + hideBasic: true, + }, + }, + }, + }, + metamask: { + send: { + gasLimit: '16', + gasPrice: '32', + amount: '64', + }, + currentCurrency: 'abc', + conversionRate: 50, + preferences: { + showFiatInTestnets: false, + }, + provider: { + type: 'mainnet', + }, + }, + gas: { + basicEstimates: { + blockTime: 12, + safeLow: 2, + }, + customData: { + limit: 'aaaaaaaa', + price: 'ffffffff', + }, + gasEstimatesLoading: false, + priceAndTimeEstimates: [ + { gasprice: 3, expectedTime: 31 }, + { gasprice: 4, expectedTime: 62 }, + { gasprice: 5, expectedTime: 93 }, + { gasprice: 6, expectedTime: 124 }, + ], + }, + confirmTransaction: { + txData: { + txParams: { + gas: '0x1600000', + gasPrice: '0x3200000', + value: '0x640000000000000', + }, + }, + }, + } + const baseExpectedResult = { + isConfirm: true, + customGasPrice: 4.294967295, + customGasLimit: 2863311530, + currentTimeEstimate: '~1 min 11 sec', + newTotalFiat: '637.41', + blockTime: 12, + customModalGasLimitInHex: 'aaaaaaaa', + customModalGasPriceInHex: 'ffffffff', + customPriceIsSafe: true, + gasChartProps: { + 'currentPrice': 4.294967295, + estimatedTimes: [31, 62, 93, 124], + estimatedTimesMax: 31, + gasPrices: [3, 4, 5, 6], + gasPricesMax: 6, + }, + gasPriceButtonGroupProps: { + buttonDataLoading: 'mockBasicGasEstimateLoadingStatus:4', + defaultActiveButtonIndex: 'mockRenderableBasicEstimateData:4ffffffff', + gasButtonInfo: 'mockRenderableBasicEstimateData:4', + }, + gasEstimatesLoading: false, + hideBasic: true, + infoRowProps: { + originalTotalFiat: '637.41', + originalTotalEth: '12.748189 ETH', + newTotalFiat: '637.41', + newTotalEth: '12.748189 ETH', + sendAmount: '0.45036 ETH', + transactionFee: '12.297829 ETH', + }, + insufficientBalance: true, + isSpeedUp: false, + txId: 34, + isEthereumNetwork: false, + isMainnet: true, + } + const baseMockOwnProps = { transaction: { id: 34 } } + const tests = [ + { mockState: baseMockState, expectedResult: baseExpectedResult, mockOwnProps: baseMockOwnProps }, + { + mockState: Object.assign({}, baseMockState, { + metamask: { ...baseMockState.metamask, balance: '0xfffffffffffffffffffff' }, + }), + expectedResult: Object.assign({}, baseExpectedResult, { insufficientBalance: false }), + mockOwnProps: baseMockOwnProps, + }, + { + mockState: baseMockState, + mockOwnProps: Object.assign({}, baseMockOwnProps, { + transaction: { id: 34, status: 'submitted' }, + }), + expectedResult: Object.assign({}, baseExpectedResult, { isSpeedUp: true }), + }, + { + mockState: Object.assign({}, baseMockState, { + metamask: { + ...baseMockState.metamask, + preferences: { + ...baseMockState.metamask.preferences, + showFiatInTestnets: false, + }, + provider: { + ...baseMockState.metamask.provider, + type: 'rinkeby', + }, + }, + }), + mockOwnProps: baseMockOwnProps, + expectedResult: { + ...baseExpectedResult, + infoRowProps: { + ...baseExpectedResult.infoRowProps, + newTotalFiat: '', + }, + isMainnet: false, + }, + }, + { + mockState: Object.assign({}, baseMockState, { + metamask: { + ...baseMockState.metamask, + preferences: { + ...baseMockState.metamask.preferences, + showFiatInTestnets: true, + }, + provider: { + ...baseMockState.metamask.provider, + type: 'rinkeby', + }, + }, + }), + mockOwnProps: baseMockOwnProps, + expectedResult: { + ...baseExpectedResult, + isMainnet: false, + }, + }, + { + mockState: Object.assign({}, baseMockState, { + metamask: { + ...baseMockState.metamask, + preferences: { + ...baseMockState.metamask.preferences, + showFiatInTestnets: true, + }, + provider: { + ...baseMockState.metamask.provider, + type: 'mainnet', + }, + }, + }), + mockOwnProps: baseMockOwnProps, + expectedResult: baseExpectedResult, + }, + ] + + let result + tests.forEach(({ mockState, mockOwnProps, expectedResult}) => { + result = mapStateToProps(mockState, mockOwnProps) + assert.deepEqual(result, expectedResult) + }) + }) + + }) + + describe('mapDispatchToProps()', () => { + let dispatchSpy + let mapDispatchToPropsObject + + beforeEach(() => { + dispatchSpy = sinon.spy() + mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) + }) + + afterEach(() => { + actionSpies.hideModal.resetHistory() + gasActionSpies.setCustomGasPrice.resetHistory() + gasActionSpies.setCustomGasLimit.resetHistory() + }) + + describe('hideGasButtonGroup()', () => { + it('should dispatch a hideGasButtonGroup action', () => { + mapDispatchToPropsObject.hideGasButtonGroup() + assert(dispatchSpy.calledOnce) + assert(sendActionSpies.hideGasButtonGroup.calledOnce) + }) + }) + + describe('cancelAndClose()', () => { + it('should dispatch a hideModal action', () => { + mapDispatchToPropsObject.cancelAndClose() + assert(dispatchSpy.calledTwice) + assert(actionSpies.hideModal.calledOnce) + assert(gasActionSpies.resetCustomData.calledOnce) + }) + }) + + describe('updateCustomGasPrice()', () => { + it('should dispatch a setCustomGasPrice action with the arg passed to updateCustomGasPrice hex prefixed', () => { + mapDispatchToPropsObject.updateCustomGasPrice('ffff') + assert(dispatchSpy.calledOnce) + assert(gasActionSpies.setCustomGasPrice.calledOnce) + assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0xffff') + }) + }) + + describe('convertThenUpdateCustomGasPrice()', () => { + it('should dispatch a setCustomGasPrice action with the arg passed to convertThenUpdateCustomGasPrice converted to WEI', () => { + mapDispatchToPropsObject.convertThenUpdateCustomGasPrice('0xffff') + assert(dispatchSpy.calledOnce) + assert(gasActionSpies.setCustomGasPrice.calledOnce) + assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0x3b9a8e653600') + }) + }) + + + describe('convertThenUpdateCustomGasLimit()', () => { + it('should dispatch a setCustomGasLimit action with the arg passed to convertThenUpdateCustomGasLimit converted to hex', () => { + mapDispatchToPropsObject.convertThenUpdateCustomGasLimit(16) + assert(dispatchSpy.calledOnce) + assert(gasActionSpies.setCustomGasLimit.calledOnce) + assert.equal(gasActionSpies.setCustomGasLimit.getCall(0).args[0], '0x10') + }) + }) + + describe('setGasData()', () => { + it('should dispatch a setGasPrice and setGasLimit action with the correct props', () => { + mapDispatchToPropsObject.setGasData('ffff', 'aaaa') + assert(dispatchSpy.calledTwice) + assert(actionSpies.setGasPrice.calledOnce) + assert(actionSpies.setGasLimit.calledOnce) + assert.equal(actionSpies.setGasLimit.getCall(0).args[0], 'ffff') + assert.equal(actionSpies.setGasPrice.getCall(0).args[0], 'aaaa') + }) + }) + + describe('updateConfirmTxGasAndCalculate()', () => { + it('should dispatch a updateGasAndCalculate action with the correct props', () => { + mapDispatchToPropsObject.updateConfirmTxGasAndCalculate('ffff', 'aaaa') + assert.equal(dispatchSpy.callCount, 3) + assert(confirmTransactionActionSpies.updateGasAndCalculate.calledOnce) + assert.deepEqual(confirmTransactionActionSpies.updateGasAndCalculate.getCall(0).args[0], { gasLimit: 'ffff', gasPrice: 'aaaa' }) + }) + }) + + }) + + describe('mergeProps', () => { + let stateProps + let dispatchProps + let ownProps + + beforeEach(() => { + stateProps = { + gasPriceButtonGroupProps: { + someGasPriceButtonGroupProp: 'foo', + anotherGasPriceButtonGroupProp: 'bar', + }, + isConfirm: true, + someOtherStateProp: 'baz', + } + dispatchProps = { + updateCustomGasPrice: sinon.spy(), + hideGasButtonGroup: sinon.spy(), + setGasData: sinon.spy(), + updateConfirmTxGasAndCalculate: sinon.spy(), + someOtherDispatchProp: sinon.spy(), + createSpeedUpTransaction: sinon.spy(), + hideSidebar: sinon.spy(), + hideModal: sinon.spy(), + cancelAndClose: sinon.spy(), + } + ownProps = { someOwnProp: 123 } + }) + + afterEach(() => { + dispatchProps.updateCustomGasPrice.resetHistory() + dispatchProps.hideGasButtonGroup.resetHistory() + dispatchProps.setGasData.resetHistory() + dispatchProps.updateConfirmTxGasAndCalculate.resetHistory() + dispatchProps.someOtherDispatchProp.resetHistory() + dispatchProps.createSpeedUpTransaction.resetHistory() + dispatchProps.hideSidebar.resetHistory() + dispatchProps.hideModal.resetHistory() + }) + it('should return the expected props when isConfirm is true', () => { + const result = mergeProps(stateProps, dispatchProps, ownProps) + + assert.equal(result.isConfirm, true) + 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.updateConfirmTxGasAndCalculate.callCount, 0) + assert.equal(dispatchProps.setGasData.callCount, 0) + assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) + assert.equal(dispatchProps.hideModal.callCount, 0) + + result.onSubmit() + + assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 1) + assert.equal(dispatchProps.setGasData.callCount, 0) + assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) + assert.equal(dispatchProps.hideModal.callCount, 1) + + assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0) + result.gasPriceButtonGroupProps.handleGasPriceSelection() + assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1) + + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0) + result.someOtherDispatchProp() + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1) + }) + + it('should return the expected props when isConfirm is false', () => { + const result = mergeProps(Object.assign({}, stateProps, { isConfirm: false }), dispatchProps, ownProps) + + assert.equal(result.isConfirm, false) + 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.updateConfirmTxGasAndCalculate.callCount, 0) + assert.equal(dispatchProps.setGasData.callCount, 0) + assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) + assert.equal(dispatchProps.cancelAndClose.callCount, 0) + + result.onSubmit('mockNewLimit', 'mockNewPrice') + + assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0) + assert.equal(dispatchProps.setGasData.callCount, 1) + assert.deepEqual(dispatchProps.setGasData.getCall(0).args, ['mockNewLimit', 'mockNewPrice']) + assert.equal(dispatchProps.hideGasButtonGroup.callCount, 1) + assert.equal(dispatchProps.cancelAndClose.callCount, 1) + + assert.equal(dispatchProps.updateCustomGasPrice.callCount, 0) + result.gasPriceButtonGroupProps.handleGasPriceSelection() + assert.equal(dispatchProps.updateCustomGasPrice.callCount, 1) + + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 0) + result.someOtherDispatchProp() + assert.equal(dispatchProps.someOtherDispatchProp.callCount, 1) + }) + + it('should dispatch the expected actions from obSubmit when isConfirm is false and isSpeedUp is true', () => { + const result = mergeProps(Object.assign({}, stateProps, { isSpeedUp: true, isConfirm: false }), dispatchProps, ownProps) + + result.onSubmit() + + assert.equal(dispatchProps.updateConfirmTxGasAndCalculate.callCount, 0) + assert.equal(dispatchProps.setGasData.callCount, 0) + assert.equal(dispatchProps.hideGasButtonGroup.callCount, 0) + assert.equal(dispatchProps.cancelAndClose.callCount, 1) + + assert.equal(dispatchProps.createSpeedUpTransaction.callCount, 1) + assert.equal(dispatchProps.hideSidebar.callCount, 1) + }) + }) + +}) diff --git a/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js b/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js new file mode 100644 index 000000000..0456f5262 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-price-button-group/gas-price-button-group.component.js @@ -0,0 +1,89 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import ButtonGroup from '../../../ui/button-group' +import Button from '../../../ui/button' + +const GAS_OBJECT_PROPTYPES_SHAPE = { + label: PropTypes.string, + feeInPrimaryCurrency: PropTypes.string, + feeInSecondaryCurrency: PropTypes.string, + timeEstimate: PropTypes.string, + priceInHexWei: PropTypes.string, +} + +export default class GasPriceButtonGroup extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + buttonDataLoading: PropTypes.bool, + className: PropTypes.string, + defaultActiveButtonIndex: PropTypes.number, + gasButtonInfo: PropTypes.arrayOf(PropTypes.shape(GAS_OBJECT_PROPTYPES_SHAPE)), + handleGasPriceSelection: PropTypes.func, + newActiveButtonIndex: PropTypes.number, + noButtonActiveByDefault: PropTypes.bool, + showCheck: PropTypes.bool, + } + + renderButtonContent ({ + labelKey, + feeInPrimaryCurrency, + feeInSecondaryCurrency, + timeEstimate, + }, { + className, + showCheck, + }) { + return (<div> + { labelKey && <div className={`${className}__label`}>{ this.context.t(labelKey) }</div> } + { timeEstimate && <div className={`${className}__time-estimate`}>{ timeEstimate }</div> } + { feeInPrimaryCurrency && <div className={`${className}__primary-currency`}>{ feeInPrimaryCurrency }</div> } + { feeInSecondaryCurrency && <div className={`${className}__secondary-currency`}>{ feeInSecondaryCurrency }</div> } + { showCheck && <div className="button-check-wrapper"><i className="fa fa-check fa-sm" /></div> } + </div>) + } + + renderButton ({ + priceInHexWei, + ...renderableGasInfo + }, { + buttonDataLoading, + handleGasPriceSelection, + ...buttonContentPropsAndFlags + }, index) { + return ( + <Button + onClick={() => handleGasPriceSelection(priceInHexWei)} + key={`gas-price-button-${index}`} + > + {this.renderButtonContent(renderableGasInfo, buttonContentPropsAndFlags)} + </Button> + ) + } + + render () { + const { + gasButtonInfo, + defaultActiveButtonIndex = 1, + newActiveButtonIndex, + noButtonActiveByDefault = false, + buttonDataLoading, + ...buttonPropsAndFlags + } = this.props + + return ( + !buttonDataLoading + ? <ButtonGroup + className={buttonPropsAndFlags.className} + defaultActiveButtonIndex={defaultActiveButtonIndex} + newActiveButtonIndex={newActiveButtonIndex} + noButtonActiveByDefault={noButtonActiveByDefault} + > + { gasButtonInfo.map((obj, index) => this.renderButton(obj, buttonPropsAndFlags, index)) } + </ButtonGroup> + : <div className={`${buttonPropsAndFlags.className}__loading-container`}>{ this.context.t('loading') }</div> + ) + } +} diff --git a/ui/app/components/app/gas-customization/gas-price-button-group/index.js b/ui/app/components/app/gas-customization/gas-price-button-group/index.js new file mode 100644 index 000000000..775648330 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-price-button-group/index.js @@ -0,0 +1 @@ +export { default } from './gas-price-button-group.component' diff --git a/ui/app/components/app/gas-customization/gas-price-button-group/index.scss b/ui/app/components/app/gas-customization/gas-price-button-group/index.scss new file mode 100644 index 000000000..cb2f3ecf1 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-price-button-group/index.scss @@ -0,0 +1,238 @@ +.gas-price-button-group { + margin-top: 22px; + display: flex; + justify-content: space-evenly; + width: 100%; + padding-left: 20px; + padding-right: 20px; + + &__primary-currency { + font-size: 18px; + height: 20.5px; + margin-bottom: 7.5px; + } + + &__time-estimate { + margin-top: 5.5px; + color: $silver-chalice; + height: 15.4px; + } + + &__loading-container { + height: 130px; + } + + .button-group__button, .button-group__button--active { + height: 130px; + max-width: 108px; + font-size: 12px; + flex-direction: column; + align-items: center; + display: flex; + padding-top: 17px; + border-radius: 4px; + border: 2px solid $spindle; + background: $white; + color: $scorpion; + + div { + display: flex; + flex-direction: column; + align-items: center; + } + + i { + &:last-child { + display: none; + } + } + } + + .button-group__button--active { + border: 2px solid $curious-blue; + color: $scorpion; + + i { + &:last-child { + display: flex; + color: $curious-blue; + margin-top: 8px + } + } + } +} + +.gas-price-button-group--small { + display: flex; + justify-content: stretch; + + @media screen and (max-width: $break-small) { + max-width: 260px; + } + + &__button-fiat-price { + font-size: 13px; + } + + &__button-label { + font-size: 16px; + } + + &__label { + font-weight: 500; + } + + &__primary-currency { + font-size: 12px; + + @media screen and (max-width: 575px) { + font-size: 10px; + } + } + + &__secondary-currency { + font-size: 12px; + + @media screen and (max-width: 575px) { + font-size: 10px; + } + } + + &__loading-container { + height: 78px; + } + + .button-group__button, .button-group__button--active { + height: 78px; + background: white; + color: $scorpion; + padding-top: 9px; + padding-left: 8.5px; + + @media screen and (max-width: $break-small) { + padding-left: 4px; + } + + div { + display: flex; + flex-flow: column; + align-items: flex-start; + justify-content: flex-start; + } + + i { + &:last-child { + display: none; + } + } + } + + .button-group__button--active { + color: $white; + background: $dodger-blue; + + i { + &:last-child { + display: flex; + color: $curious-blue; + margin-top: 10px + } + } + } +} + +.gas-price-button-group--alt { + display: flex; + justify-content: stretch; + width: 95%; + + &__button-fiat-price { + font-size: 13px; + } + + &__button-label { + font-size: 16px; + } + + &__label { + font-weight: 500; + font-size: 10px; + text-transform: capitalize; + } + + &__primary-currency { + font-size: 11px; + margin-top: 3px; + } + + &__secondary-currency { + font-size: 11px; + } + + &__loading-container { + height: 78px; + } + + &__time-estimate { + font-size: 14px; + font-weight: 500; + margin-top: 4px; + color: $black; + } + + .button-group__button, .button-group__button--active { + height: 78px; + background: white; + color: #2A4055; + width: 108px; + height: 97px; + box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.151579); + border-radius: 6px; + border: none; + + div { + display: flex; + flex-flow: column;; + align-items: flex-start; + justify-content: flex-start; + position: relative; + } + + .button-check-wrapper { + display: none; + } + + &:first-child { + margin-right: 6px; + } + + &:last-child { + margin-left: 6px; + } + } + + .button-group__button--active { + background: #F7FCFF; + border: 2px solid #2C8BDC; + + .button-check-wrapper { + height: 16px; + width: 16px; + border-radius: 8px; + position: absolute; + top: -11px; + right: -10px; + background: #D5ECFA; + display: flex; + flex-flow: row; + justify-content: center; + align-items: center; + } + + i { + display: flex; + color: $curious-blue; + font-size: 12px; + } + } +} diff --git a/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js b/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js new file mode 100644 index 000000000..37840a8a5 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-price-button-group/tests/gas-price-button-group-component.test.js @@ -0,0 +1,233 @@ +import React from 'react' +import assert from 'assert' +import shallow from '../../../../../../lib/shallow-with-context' +import sinon from 'sinon' +import GasPriceButtonGroup from '../gas-price-button-group.component' + +import ButtonGroup from '../../../../ui/button-group' + +const mockGasPriceButtonGroupProps = { + buttonDataLoading: false, + className: 'gas-price-button-group', + gasButtonInfo: [ + { + feeInPrimaryCurrency: '$0.52', + feeInSecondaryCurrency: '0.0048 ETH', + timeEstimate: '~ 1 min 0 sec', + priceInHexWei: '0xa1b2c3f', + }, + { + feeInPrimaryCurrency: '$0.39', + feeInSecondaryCurrency: '0.004 ETH', + timeEstimate: '~ 1 min 30 sec', + priceInHexWei: '0xa1b2c39', + }, + { + feeInPrimaryCurrency: '$0.30', + feeInSecondaryCurrency: '0.00354 ETH', + timeEstimate: '~ 2 min 1 sec', + priceInHexWei: '0xa1b2c30', + }, + ], + handleGasPriceSelection: sinon.spy(), + noButtonActiveByDefault: true, + defaultActiveButtonIndex: 2, + showCheck: true, +} + +const mockButtonPropsAndFlags = Object.assign({}, { + className: mockGasPriceButtonGroupProps.className, + handleGasPriceSelection: mockGasPriceButtonGroupProps.handleGasPriceSelection, + showCheck: mockGasPriceButtonGroupProps.showCheck, +}) + +sinon.spy(GasPriceButtonGroup.prototype, 'renderButton') +sinon.spy(GasPriceButtonGroup.prototype, 'renderButtonContent') + +describe('GasPriceButtonGroup Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(<GasPriceButtonGroup + {...mockGasPriceButtonGroupProps} + />) + }) + + afterEach(() => { + GasPriceButtonGroup.prototype.renderButton.resetHistory() + GasPriceButtonGroup.prototype.renderButtonContent.resetHistory() + mockGasPriceButtonGroupProps.handleGasPriceSelection.resetHistory() + }) + + describe('render', () => { + it('should render a ButtonGroup', () => { + assert(wrapper.is(ButtonGroup)) + }) + + it('should render the correct props on the ButtonGroup', () => { + const { + className, + defaultActiveButtonIndex, + noButtonActiveByDefault, + } = wrapper.props() + assert.equal(className, 'gas-price-button-group') + assert.equal(defaultActiveButtonIndex, 2) + assert.equal(noButtonActiveByDefault, true) + }) + + function renderButtonArgsTest (i, mockButtonPropsAndFlags) { + assert.deepEqual( + GasPriceButtonGroup.prototype.renderButton.getCall(i).args, + [ + Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[i]), + mockButtonPropsAndFlags, + i, + ] + ) + } + + it('should call this.renderButton 3 times, with the correct args', () => { + assert.equal(GasPriceButtonGroup.prototype.renderButton.callCount, 3) + renderButtonArgsTest(0, mockButtonPropsAndFlags) + renderButtonArgsTest(1, mockButtonPropsAndFlags) + renderButtonArgsTest(2, mockButtonPropsAndFlags) + }) + + it('should show loading if buttonDataLoading', () => { + wrapper.setProps({ buttonDataLoading: true }) + assert(wrapper.is('div')) + assert(wrapper.hasClass('gas-price-button-group__loading-container')) + assert.equal(wrapper.text(), 'loading') + }) + }) + + describe('renderButton', () => { + let wrappedRenderButtonResult + + beforeEach(() => { + GasPriceButtonGroup.prototype.renderButtonContent.resetHistory() + const renderButtonResult = GasPriceButtonGroup.prototype.renderButton( + Object.assign({}, mockGasPriceButtonGroupProps.gasButtonInfo[0]), + mockButtonPropsAndFlags + ) + wrappedRenderButtonResult = shallow(renderButtonResult) + }) + + it('should render a button', () => { + assert.equal(wrappedRenderButtonResult.type(), 'button') + }) + + it('should call the correct method when clicked', () => { + assert.equal(mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, 0) + wrappedRenderButtonResult.props().onClick() + assert.equal(mockGasPriceButtonGroupProps.handleGasPriceSelection.callCount, 1) + assert.deepEqual( + mockGasPriceButtonGroupProps.handleGasPriceSelection.getCall(0).args, + [mockGasPriceButtonGroupProps.gasButtonInfo[0].priceInHexWei] + ) + }) + + it('should call this.renderButtonContent with the correct args', () => { + assert.equal(GasPriceButtonGroup.prototype.renderButtonContent.callCount, 1) + const { + feeInPrimaryCurrency, + feeInSecondaryCurrency, + timeEstimate, + } = mockGasPriceButtonGroupProps.gasButtonInfo[0] + const { + showCheck, + className, + } = mockGasPriceButtonGroupProps + assert.deepEqual( + GasPriceButtonGroup.prototype.renderButtonContent.getCall(0).args, + [ + { + feeInPrimaryCurrency, + feeInSecondaryCurrency, + timeEstimate, + }, + { + showCheck, + className, + }, + ] + ) + }) + }) + + describe('renderButtonContent', () => { + it('should render a label if passed a labelKey', () => { + const renderButtonContentResult = wrapper.instance().renderButtonContent({ + labelKey: 'mockLabelKey', + }, { + className: 'someClass', + }) + const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) + assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1) + assert.equal(wrappedRenderButtonContentResult.find('.someClass__label').text(), 'mockLabelKey') + }) + + it('should render a feeInPrimaryCurrency if passed a feeInPrimaryCurrency', () => { + const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({ + feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency', + }, { + className: 'someClass', + }) + const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) + assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1) + assert.equal(wrappedRenderButtonContentResult.find('.someClass__primary-currency').text(), 'mockFeeInPrimaryCurrency') + }) + + it('should render a feeInSecondaryCurrency if passed a feeInSecondaryCurrency', () => { + const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({ + feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency', + }, { + className: 'someClass', + }) + const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) + assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1) + assert.equal(wrappedRenderButtonContentResult.find('.someClass__secondary-currency').text(), 'mockFeeInSecondaryCurrency') + }) + + it('should render a timeEstimate if passed a timeEstimate', () => { + const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({ + timeEstimate: 'mockTimeEstimate', + }, { + className: 'someClass', + }) + const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) + assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1) + assert.equal(wrappedRenderButtonContentResult.find('.someClass__time-estimate').text(), 'mockTimeEstimate') + }) + + it('should render a check if showCheck is true', () => { + const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({}, { + className: 'someClass', + showCheck: true, + }) + const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) + assert.equal(wrappedRenderButtonContentResult.find('.fa-check').length, 1) + }) + + it('should render all elements if all args passed', () => { + const renderButtonContentResult = wrapper.instance().renderButtonContent({ + labelKey: 'mockLabel', + feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency', + feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency', + timeEstimate: 'mockTimeEstimate', + }, { + className: 'someClass', + showCheck: true, + }) + const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) + assert.equal(wrappedRenderButtonContentResult.children().length, 5) + }) + + + it('should render no elements if all args passed', () => { + const renderButtonContentResult = GasPriceButtonGroup.prototype.renderButtonContent({}, {}) + const wrappedRenderButtonContentResult = shallow(renderButtonContentResult) + assert.equal(wrappedRenderButtonContentResult.children().length, 0) + }) + }) +}) diff --git a/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.component.js b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.component.js new file mode 100644 index 000000000..c0eaf4852 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.component.js @@ -0,0 +1,108 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import * as d3 from 'd3' +import { + generateChart, + getCoordinateData, + handleChartUpdate, + hideDataUI, + setTickPosition, + handleMouseMove, +} from './gas-price-chart.utils.js' + +export default class GasPriceChart extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + gasPrices: PropTypes.array, + estimatedTimes: PropTypes.array, + gasPricesMax: PropTypes.number, + estimatedTimesMax: PropTypes.number, + currentPrice: PropTypes.number, + updateCustomGasPrice: PropTypes.func, + } + + renderChart ({ + currentPrice, + gasPrices, + estimatedTimes, + gasPricesMax, + estimatedTimesMax, + updateCustomGasPrice, + }) { + const chart = generateChart(gasPrices, estimatedTimes, gasPricesMax, estimatedTimesMax, this.context.t) + setTimeout(function () { + setTickPosition('y', 0, -5, 8) + setTickPosition('y', 1, -3, -5) + setTickPosition('x', 0, 3) + setTickPosition('x', 1, 3, -8) + + const { x: domainX } = getCoordinateData('.domain') + const { x: yAxisX } = getCoordinateData('.c3-axis-y-label') + const { x: tickX } = getCoordinateData('.c3-axis-x .tick') + + d3.select('.c3-axis-x .tick').attr('transform', 'translate(' + (domainX - tickX) / 2 + ', 0)') + d3.select('.c3-axis-x-label').attr('transform', 'translate(0,-15)') + d3.select('.c3-axis-y-label').attr('transform', 'translate(' + (domainX - yAxisX - 12) + ', 2) rotate(-90)') + d3.select('.c3-xgrid-focus line').attr('y2', 98) + + d3.select('.c3-chart').on('mouseout', () => { + hideDataUI(chart, '#overlayed-circle') + }) + + d3.select('.c3-chart').on('click', () => { + const { x: newGasPrice } = d3.select('#overlayed-circle').datum() + updateCustomGasPrice(newGasPrice) + }) + + const { x: chartXStart, width: chartWidth } = getCoordinateData('.c3-areas-data1') + + handleChartUpdate({ + chart, + gasPrices, + newPrice: currentPrice, + cssId: '#set-circle', + }) + + d3.select('.c3-chart').on('mousemove', function () { + handleMouseMove({ + xMousePos: d3.event.clientX, + chartXStart, + chartWidth, + gasPrices, + estimatedTimes, + chart, + }) + }) + }, 0) + + this.chart = chart + } + + componentDidUpdate (prevProps) { + const { gasPrices, currentPrice: newPrice } = this.props + + if (prevProps.currentPrice !== newPrice) { + handleChartUpdate({ + chart: this.chart, + gasPrices, + newPrice, + cssId: '#set-circle', + }) + } + } + + componentDidMount () { + this.renderChart(this.props) + } + + render () { + return ( + <div className="gas-price-chart" id="container"> + <div className="gas-price-chart__root" id="chart"></div> + </div> + ) + } +} diff --git a/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js new file mode 100644 index 000000000..f19dafcc1 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js @@ -0,0 +1,354 @@ +import * as d3 from 'd3' +import c3 from 'c3' +import BigNumber from 'bignumber.js' + +const newBigSigDig = n => (new BigNumber(n.toPrecision(15))) +const createOp = (a, b, op) => (newBigSigDig(a))[op](newBigSigDig(b)) +const bigNumMinus = (a = 0, b = 0) => createOp(a, b, 'minus') +const bigNumDiv = (a = 0, b = 1) => createOp(a, b, 'div') + +export function handleMouseMove ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes, chart }) { + const { currentPosValue, newTimeEstimate } = getNewXandTimeEstimate({ + xMousePos, + chartXStart, + chartWidth, + gasPrices, + estimatedTimes, + }) + + if (currentPosValue === null && newTimeEstimate === null) { + hideDataUI(chart, '#overlayed-circle') + return + } + + const indexOfNewCircle = estimatedTimes.length + 1 + const dataUIObj = generateDataUIObj(currentPosValue, indexOfNewCircle, newTimeEstimate) + + chart.internal.overlayPoint(dataUIObj, indexOfNewCircle) + chart.internal.showTooltip([dataUIObj], d3.select('.c3-areas-data1')._groups[0]) + chart.internal.showXGridFocus([dataUIObj]) +} + +export function getCoordinateData (selector) { + const node = d3.select(selector).node() + return node ? node.getBoundingClientRect() : {} +} + +export function generateDataUIObj (x, index, value) { + return { + x, + value, + index, + id: 'data1', + name: 'data1', + } +} + +export function handleChartUpdate ({ chart, gasPrices, newPrice, cssId }) { + const { + closestLowerValueIndex, + closestLowerValue, + closestHigherValueIndex, + closestHigherValue, + } = getAdjacentGasPrices({ gasPrices, priceToPosition: newPrice }) + + if (closestLowerValue && closestHigherValue) { + setSelectedCircle({ + chart, + newPrice, + closestLowerValueIndex, + closestLowerValue, + closestHigherValueIndex, + closestHigherValue, + }) + } else { + hideDataUI(chart, cssId) + } +} + +export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) { + const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition) + const closestHigherValueIndex = gasPrices.findIndex((e, i, a) => e > priceToPosition) + return { + closestLowerValueIndex, + closestHigherValueIndex, + closestHigherValue: gasPrices[closestHigherValueIndex], + closestLowerValue: gasPrices[closestLowerValueIndex], + } +} + +export function extrapolateY ({ higherY = 0, lowerY = 0, higherX = 0, lowerX = 0, xForExtrapolation = 0 }) { + const slope = bigNumMinus(higherY, lowerY).div(bigNumMinus(higherX, lowerX)) + const newTimeEstimate = slope.times(bigNumMinus(higherX, xForExtrapolation)).minus(newBigSigDig(higherY)).negated() + + return newTimeEstimate.toNumber() +} + + +export function getNewXandTimeEstimate ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes }) { + const chartMouseXPos = bigNumMinus(xMousePos, chartXStart) + const posPercentile = bigNumDiv(chartMouseXPos, chartWidth) + + const currentPosValue = (bigNumMinus(gasPrices[gasPrices.length - 1], gasPrices[0])) + .times(newBigSigDig(posPercentile)) + .plus(newBigSigDig(gasPrices[0])) + .toNumber() + + const { + closestLowerValueIndex, + closestLowerValue, + closestHigherValueIndex, + closestHigherValue, + } = getAdjacentGasPrices({ gasPrices, priceToPosition: currentPosValue }) + + return !closestHigherValue || !closestLowerValue + ? { + currentPosValue: null, + newTimeEstimate: null, + } + : { + currentPosValue, + newTimeEstimate: extrapolateY({ + higherY: estimatedTimes[closestHigherValueIndex], + lowerY: estimatedTimes[closestLowerValueIndex], + higherX: closestHigherValue, + lowerX: closestLowerValue, + xForExtrapolation: currentPosValue, + }), + } +} + +export function hideDataUI (chart, dataNodeId) { + const overLayedCircle = d3.select(dataNodeId) + if (!overLayedCircle.empty()) { + overLayedCircle.remove() + } + d3.select('.c3-tooltip-container').style('display', 'none !important') + chart.internal.hideXGridFocus() +} + +export function setTickPosition (axis, n, newPosition, secondNewPosition) { + const positionToShift = axis === 'y' ? 'x' : 'y' + const secondPositionToShift = axis === 'y' ? 'y' : 'x' + d3.select('#chart') + .select(`.c3-axis-${axis}`) + .selectAll('.tick') + .filter((d, i) => i === n) + .select('text') + .attr(positionToShift, 0) + .select('tspan') + .attr(positionToShift, newPosition) + .attr(secondPositionToShift, secondNewPosition || 0) + .style('visibility', 'visible') +} + +export function appendOrUpdateCircle ({ data, itemIndex, cx, cy, cssId, appendOnly }) { + const circle = this.main + .select('.c3-selected-circles' + this.getTargetSelectorSuffix(data.id)) + .selectAll(`.c3-selected-circle-${itemIndex}`) + + if (appendOnly || circle.empty()) { + circle.data([data]) + .enter().append('circle') + .attr('class', () => this.generateClass('c3-selected-circle', itemIndex)) + .attr('id', cssId) + .attr('cx', cx) + .attr('cy', cy) + .attr('stroke', () => this.color(data)) + .attr('r', 6) + } else { + circle.data([data]) + .attr('cx', cx) + .attr('cy', cy) + } +} + +export function setSelectedCircle ({ + chart, + newPrice, + closestLowerValueIndex, + closestLowerValue, + closestHigherValueIndex, + closestHigherValue, +}) { + const numberOfValues = chart.internal.data.xs.data1.length + + const { x: lowerX, y: lowerY } = getCoordinateData(`.c3-circle-${closestLowerValueIndex}`) + let { x: higherX, y: higherY } = getCoordinateData(`.c3-circle-${closestHigherValueIndex}`) + let count = closestHigherValueIndex + 1 + + if (lowerX && higherX) { + while (lowerX === higherX) { + higherX = getCoordinateData(`.c3-circle-${count}`).x + higherY = getCoordinateData(`.c3-circle-${count}`).y + count++ + } + } + + const currentX = bigNumMinus(higherX, lowerX) + .times(bigNumMinus(newPrice, closestLowerValue)) + .div(bigNumMinus(closestHigherValue, closestLowerValue)) + .plus(newBigSigDig(lowerX)) + + const newTimeEstimate = extrapolateY({ higherY, lowerY, higherX, lowerX, xForExtrapolation: currentX }) + + chart.internal.selectPoint( + generateDataUIObj(currentX.toNumber(), numberOfValues, newTimeEstimate), + numberOfValues + ) +} + + +export function generateChart (gasPrices, estimatedTimes, gasPricesMax, estimatedTimesMax) { + const gasPricesMaxPadded = gasPricesMax + 1 + const chart = c3.generate({ + size: { + height: 165, + }, + transition: { + duration: 0, + }, + padding: {left: 20, right: 15, top: 6, bottom: 10}, + data: { + x: 'x', + columns: [ + ['x', ...gasPrices], + ['data1', ...estimatedTimes], + ], + types: { + data1: 'area', + }, + selection: { + enabled: false, + }, + }, + color: { + data1: '#259de5', + }, + axis: { + x: { + min: gasPrices[0], + max: gasPricesMax, + tick: { + values: [Math.floor(gasPrices[0]), Math.ceil(gasPricesMax)], + outer: false, + format: function (val) { return val + ' GWEI' }, + }, + padding: {left: gasPricesMax / 50, right: gasPricesMax / 50}, + label: { + text: 'Gas Price ($)', + position: 'outer-center', + }, + }, + y: { + padding: {top: 7, bottom: 7}, + tick: { + values: [Math.floor(estimatedTimesMax * 0.05), Math.ceil(estimatedTimesMax * 0.97)], + outer: false, + }, + label: { + text: 'Confirmation time (sec)', + position: 'outer-middle', + }, + min: 0, + }, + }, + legend: { + show: false, + }, + grid: { + x: {}, + lines: { + front: false, + }, + }, + point: { + focus: { + expand: { + enabled: false, + r: 3.5, + }, + }, + }, + tooltip: { + format: { + title: (v) => v.toPrecision(4), + }, + contents: function (d) { + const titleFormat = this.config.tooltip_format_title + let text + d.forEach(el => { + if (el && (el.value || el.value === 0) && !text) { + text = "<table class='" + 'custom-tooltip' + "'>" + "<tr><th colspan='2'>" + titleFormat(el.x) + '</th></tr>' + } + }) + return text + '</table>' + "<div class='tooltip-arrow'></div>" + }, + position: function (data) { + if (d3.select('#overlayed-circle').empty()) { + return { top: -100, left: -100 } + } + + const { x: circleX, y: circleY, width: circleWidth } = getCoordinateData('#overlayed-circle') + const { x: chartXStart, y: chartYStart } = getCoordinateData('.c3-chart') + + // TODO: Confirm the below constants work with all data sets and screen sizes + const flipTooltip = circleY - circleWidth < chartYStart + 5 + + d3 + .select('.tooltip-arrow') + .style('margin-top', flipTooltip ? '-16px' : '4px') + + return { + top: bigNumMinus(circleY, chartYStart).minus(19).plus(flipTooltip ? circleWidth + 38 : 0).toNumber(), + left: bigNumMinus(circleX, chartXStart).plus(newBigSigDig(circleWidth)).minus(bigNumDiv(gasPricesMaxPadded, 50)).toNumber(), + } + }, + show: true, + }, + }) + + chart.internal.selectPoint = function (data, itemIndex = (data.index || 0)) { + const { x: chartXStart, y: chartYStart } = getCoordinateData('.c3-areas-data1') + + d3.select('#set-circle').remove() + + appendOrUpdateCircle.bind(this)({ + data, + itemIndex, + cx: () => bigNumMinus(data.x, chartXStart).plus(11).toNumber(), + cy: () => bigNumMinus(data.value, chartYStart).plus(10).toNumber(), + cssId: 'set-circle', + appendOnly: true, + }) + } + + chart.internal.overlayPoint = function (data, itemIndex) { + appendOrUpdateCircle.bind(this)({ + data, + itemIndex, + cx: this.circleX.bind(this), + cy: this.circleY.bind(this), + cssId: 'overlayed-circle', + }) + } + + chart.internal.showTooltip = function (selectedData, element) { + const dataToShow = selectedData.filter((d) => d && (d.value || d.value === 0)) + + if (dataToShow.length) { + this.tooltip.html( + this.config.tooltip_contents.call(this, selectedData, this.axis.getXAxisTickFormat(), this.getYFormat(), this.color) + ).style('display', 'flex') + + // Get tooltip dimensions + const tWidth = this.tooltip.property('offsetWidth') + const tHeight = this.tooltip.property('offsetHeight') + const position = this.config.tooltip_position.call(this, dataToShow, tWidth, tHeight, element) + // Set tooltip + this.tooltip.style('top', position.top + 'px').style('left', position.left + 'px') + } + } + + return chart +} diff --git a/ui/app/components/app/gas-customization/gas-price-chart/index.js b/ui/app/components/app/gas-customization/gas-price-chart/index.js new file mode 100644 index 000000000..9895acb62 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-price-chart/index.js @@ -0,0 +1 @@ +export { default } from './gas-price-chart.component' diff --git a/ui/app/components/app/gas-customization/gas-price-chart/index.scss b/ui/app/components/app/gas-customization/gas-price-chart/index.scss new file mode 100644 index 000000000..097543104 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-price-chart/index.scss @@ -0,0 +1,132 @@ +.gas-price-chart { + display: flex; + position: relative; + justify-content: center; + + &__root { + max-height: 154px; + max-width: 391px; + position: relative; + overflow: hidden; + + @media screen and (max-width: $break-small) { + max-width: 326px; + } + } + + .tick text, .c3-axis-x-label, .c3-axis-y-label { + font-family: Roboto; + font-style: normal; + font-weight: bold; + line-height: normal; + font-size: 8px; + text-align: center; + fill: #9A9CA6 !important; + } + + .c3-tooltip-container { + display: flex; + justify-content: center !important; + align-items: flex-end !important; + } + + .custom-tooltip { + background: rgba(0, 0, 0, 1); + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + border-radius: 3px; + opacity: 1 !important; + height: 21px; + z-index: 1; + } + + .tooltip-arrow { + background: black; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.5); + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + opacity: 1 !important; + width: 9px; + height: 9px; + margin-top: 4px; + } + + .custom-tooltip th { + font-family: Roboto; + font-style: normal; + font-weight: 500; + line-height: normal; + font-size: 10px; + text-align: center; + padding: 3px; + color: #FFFFFF; + } + + .c3-circle { + visibility: hidden; + } + + .c3-selected-circle, .c3-circle._expanded_ { + fill: #FFFFFF !important; + stroke-width: 2.4px !important; + stroke: #2d9fd9 !important; + /* visibility: visible; */ + } + + #set-circle { + fill: #313A5E !important; + stroke: #313A5E !important; + } + + .c3-axis-x-label, .c3-axis-y-label { + font-weight: normal; + } + + .tick text tspan { + visibility: hidden; + } + + .c3-circle { + fill: #2d9fd9 !important; + } + + .c3-line-data1 { + stroke: #2d9fd9 !important; + background: rgba(0,0,0,0) !important; + color: rgba(0,0,0,0) !important; + } + + .c3 path { + fill: none; + } + + .c3 path.c3-area-data1 { + opacity: 1; + fill: #e9edf1 !important; + } + + .c3-xgrid-line line { + stroke: #B8B8B8 !important; + } + + .c3-xgrid-focus { + stroke: #aaa; + } + + .c3-axis-x .domain { + fill: none; + stroke: none; + } + + .c3-axis-y .domain { + fill: none; + stroke: #C8CCD6; + } + + .c3-event-rect { + cursor: pointer; + } +} + +#chart { + background: #F8F9FB +} diff --git a/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js b/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js new file mode 100644 index 000000000..7dec7a85f --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-price-chart/tests/gas-price-chart.component.test.js @@ -0,0 +1,218 @@ +import React from 'react' +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' +import shallow from '../../../../../../lib/shallow-with-context' +import * as d3 from 'd3' + +function timeout (time) { + return new Promise((resolve, reject) => { + setTimeout(resolve, time) + }) +} + +const propsMethodSpies = { + updateCustomGasPrice: sinon.spy(), +} + +const selectReturnSpies = { + empty: sinon.spy(), + remove: sinon.spy(), + style: sinon.spy(), + select: d3.select, + attr: sinon.spy(), + on: sinon.spy(), + datum: sinon.stub().returns({ x: 'mockX' }), +} + +const mockSelectReturn = { + ...d3.select('div'), + node: () => ({ + getBoundingClientRect: () => ({ x: 123, y: 321, width: 400 }), + }), + ...selectReturnSpies, +} + +const gasPriceChartUtilsSpies = { + appendOrUpdateCircle: sinon.spy(), + generateChart: sinon.stub().returns({ mockChart: true }), + generateDataUIObj: sinon.spy(), + getAdjacentGasPrices: sinon.spy(), + getCoordinateData: sinon.stub().returns({ x: 'mockCoordinateX', width: 'mockWidth' }), + getNewXandTimeEstimate: sinon.spy(), + handleChartUpdate: sinon.spy(), + hideDataUI: sinon.spy(), + setSelectedCircle: sinon.spy(), + setTickPosition: sinon.spy(), + handleMouseMove: sinon.spy(), +} + +const testProps = { + gasPrices: [1.5, 2.5, 4, 8], + estimatedTimes: [100, 80, 40, 10], + gasPricesMax: 9, + estimatedTimesMax: '100', + currentPrice: 6, + updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice, +} + +const GasPriceChart = proxyquire('../gas-price-chart.component.js', { + './gas-price-chart.utils.js': gasPriceChartUtilsSpies, + 'd3': { + ...d3, + select: function (...args) { + const result = d3.select(...args) + return result.empty() + ? mockSelectReturn + : result + }, + event: { + clientX: 'mockClientX', + }, + }, +}).default + +sinon.spy(GasPriceChart.prototype, 'renderChart') + +describe('GasPriceChart Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(<GasPriceChart {...testProps} />) + }) + + describe('render()', () => { + it('should render', () => { + assert(wrapper.hasClass('gas-price-chart')) + }) + + it('should render the chart div', () => { + assert(wrapper.childAt(0).hasClass('gas-price-chart__root')) + assert.equal(wrapper.childAt(0).props().id, 'chart') + }) + }) + + describe('componentDidMount', () => { + it('should call this.renderChart with the components props', () => { + assert(GasPriceChart.prototype.renderChart.callCount, 1) + wrapper.instance().componentDidMount() + assert(GasPriceChart.prototype.renderChart.callCount, 2) + assert.deepEqual(GasPriceChart.prototype.renderChart.getCall(1).args, [{...testProps}]) + }) + }) + + describe('componentDidUpdate', () => { + it('should call handleChartUpdate if props.currentPrice has changed', () => { + gasPriceChartUtilsSpies.handleChartUpdate.resetHistory() + wrapper.instance().componentDidUpdate({ currentPrice: 7 }) + assert.equal(gasPriceChartUtilsSpies.handleChartUpdate.callCount, 1) + }) + + it('should call handleChartUpdate with the correct props', () => { + gasPriceChartUtilsSpies.handleChartUpdate.resetHistory() + wrapper.instance().componentDidUpdate({ currentPrice: 7 }) + assert.deepEqual(gasPriceChartUtilsSpies.handleChartUpdate.getCall(0).args, [{ + chart: { mockChart: true }, + gasPrices: [1.5, 2.5, 4, 8], + newPrice: 6, + cssId: '#set-circle', + }]) + }) + + it('should not call handleChartUpdate if props.currentPrice has not changed', () => { + gasPriceChartUtilsSpies.handleChartUpdate.resetHistory() + wrapper.instance().componentDidUpdate({ currentPrice: 6 }) + assert.equal(gasPriceChartUtilsSpies.handleChartUpdate.callCount, 0) + }) + }) + + describe('renderChart', () => { + it('should call setTickPosition 4 times, with the expected props', async () => { + await timeout(0) + gasPriceChartUtilsSpies.setTickPosition.resetHistory() + assert.equal(gasPriceChartUtilsSpies.setTickPosition.callCount, 0) + wrapper.instance().renderChart(testProps) + await timeout(0) + assert.equal(gasPriceChartUtilsSpies.setTickPosition.callCount, 4) + assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(0).args, ['y', 0, -5, 8]) + assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(1).args, ['y', 1, -3, -5]) + assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(2).args, ['x', 0, 3]) + assert.deepEqual(gasPriceChartUtilsSpies.setTickPosition.getCall(3).args, ['x', 1, 3, -8]) + }) + + it('should call handleChartUpdate with the correct props', async () => { + await timeout(0) + gasPriceChartUtilsSpies.handleChartUpdate.resetHistory() + wrapper.instance().renderChart(testProps) + await timeout(0) + assert.deepEqual(gasPriceChartUtilsSpies.handleChartUpdate.getCall(0).args, [{ + chart: { mockChart: true }, + gasPrices: [1.5, 2.5, 4, 8], + newPrice: 6, + cssId: '#set-circle', + }]) + }) + + it('should add three events to the chart', async () => { + await timeout(0) + selectReturnSpies.on.resetHistory() + assert.equal(selectReturnSpies.on.callCount, 0) + wrapper.instance().renderChart(testProps) + await timeout(0) + assert.equal(selectReturnSpies.on.callCount, 3) + + const firstOnEventArgs = selectReturnSpies.on.getCall(0).args + assert.equal(firstOnEventArgs[0], 'mouseout') + const secondOnEventArgs = selectReturnSpies.on.getCall(1).args + assert.equal(secondOnEventArgs[0], 'click') + const thirdOnEventArgs = selectReturnSpies.on.getCall(2).args + assert.equal(thirdOnEventArgs[0], 'mousemove') + }) + + it('should hide the data UI on mouseout', async () => { + await timeout(0) + selectReturnSpies.on.resetHistory() + wrapper.instance().renderChart(testProps) + gasPriceChartUtilsSpies.hideDataUI.resetHistory() + await timeout(0) + const mouseoutEventArgs = selectReturnSpies.on.getCall(0).args + assert.equal(gasPriceChartUtilsSpies.hideDataUI.callCount, 0) + mouseoutEventArgs[1]() + assert.equal(gasPriceChartUtilsSpies.hideDataUI.callCount, 1) + assert.deepEqual(gasPriceChartUtilsSpies.hideDataUI.getCall(0).args, [{ mockChart: true }, '#overlayed-circle']) + }) + + it('should updateCustomGasPrice on click', async () => { + await timeout(0) + selectReturnSpies.on.resetHistory() + wrapper.instance().renderChart(testProps) + propsMethodSpies.updateCustomGasPrice.resetHistory() + await timeout(0) + const mouseoutEventArgs = selectReturnSpies.on.getCall(1).args + assert.equal(propsMethodSpies.updateCustomGasPrice.callCount, 0) + mouseoutEventArgs[1]() + assert.equal(propsMethodSpies.updateCustomGasPrice.callCount, 1) + assert.equal(propsMethodSpies.updateCustomGasPrice.getCall(0).args[0], 'mockX') + }) + + it('should handle mousemove', async () => { + await timeout(0) + selectReturnSpies.on.resetHistory() + wrapper.instance().renderChart(testProps) + gasPriceChartUtilsSpies.handleMouseMove.resetHistory() + await timeout(0) + const mouseoutEventArgs = selectReturnSpies.on.getCall(2).args + assert.equal(gasPriceChartUtilsSpies.handleMouseMove.callCount, 0) + mouseoutEventArgs[1]() + assert.equal(gasPriceChartUtilsSpies.handleMouseMove.callCount, 1) + assert.deepEqual(gasPriceChartUtilsSpies.handleMouseMove.getCall(0).args, [{ + xMousePos: 'mockClientX', + chartXStart: 'mockCoordinateX', + chartWidth: 'mockWidth', + gasPrices: testProps.gasPrices, + estimatedTimes: testProps.estimatedTimes, + chart: { mockChart: true }, + }]) + }) + }) +}) diff --git a/ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js b/ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js new file mode 100644 index 000000000..5836e7dfc --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-slider/gas-slider.component.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class AdvancedTabContent extends Component { + static propTypes = { + onChange: PropTypes.func, + lowLabel: PropTypes.string, + highLabel: PropTypes.string, + value: PropTypes.number, + step: PropTypes.number, + max: PropTypes.number, + min: PropTypes.number, + } + + render () { + const { + onChange, + lowLabel, + highLabel, + value, + step, + max, + min, + } = this.props + + return ( + <div className="gas-slider"> + <input + className="gas-slider__input" + type="range" + step={step} + max={max} + min={min} + value={value} + id="gasSlider" + onChange={event => onChange(event.target.value)} + /> + <div className="gas-slider__bar"> + <div className="gas-slider__colored"/> + </div> + <div className="gas-slider__labels"> + <span>{lowLabel}</span> + <span>{highLabel}</span> + </div> + </div> + ) + } +} diff --git a/ui/app/components/app/gas-customization/gas-slider/index.js b/ui/app/components/app/gas-customization/gas-slider/index.js new file mode 100644 index 000000000..f1752c93f --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-slider/index.js @@ -0,0 +1 @@ +export { default } from './gas-slider.component' diff --git a/ui/app/components/app/gas-customization/gas-slider/index.scss b/ui/app/components/app/gas-customization/gas-slider/index.scss new file mode 100644 index 000000000..e6c734367 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas-slider/index.scss @@ -0,0 +1,54 @@ +.gas-slider { + position: relative; + width: 322px; + + &__input { + width: 322px; + margin-left: -2px; + z-index: 2; + } + + input[type=range] { + -webkit-appearance: none !important; + } + + input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none !important; + height: 34px; + width: 34px; + background-color: $curious-blue; + box-shadow: 0 2px 4px 0 rgba(0,0,0,0.08); + border-radius: 50%; + position: relative; + z-index: 10; + } + + &__bar { + height: 6px; + width: 322px; + background: $alto; + display: flex; + justify-content: space-between; + position: absolute; + top: 16px; + z-index: 0; + border-radius: 4px; + } + + &__colored { + height: 6px; + border-radius: 4px; + margin-left: 102px; + width: 322px; + z-index: 1; + background-color: $blizzard-blue; + } + + &__labels { + display: flex; + justify-content: space-between; + font-size: 12px; + margin-top: -6px; + color: $mid-gray; + } +}
\ No newline at end of file diff --git a/ui/app/components/app/gas-customization/gas.selectors.js b/ui/app/components/app/gas-customization/gas.selectors.js new file mode 100644 index 000000000..89374b5f1 --- /dev/null +++ b/ui/app/components/app/gas-customization/gas.selectors.js @@ -0,0 +1,14 @@ +const selectors = { + getCurrentBlockTime, + getBasicGasEstimateLoadingStatus, +} + +module.exports = selectors + +function getCurrentBlockTime (state) { + return state.gas.currentBlockTime +} + +function getBasicGasEstimateLoadingStatus (state) { + return state.gas.basicEstimateIsLoading +} diff --git a/ui/app/components/app/gas-customization/index.scss b/ui/app/components/app/gas-customization/index.scss new file mode 100644 index 000000000..b06c1d044 --- /dev/null +++ b/ui/app/components/app/gas-customization/index.scss @@ -0,0 +1,7 @@ +@import './gas-slider/index'; + +@import './gas-modal-page-container/index'; + +@import './gas-price-chart/index'; + +@import './advanced-gas-inputs/index'; diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss new file mode 100644 index 000000000..e9bb4ac9f --- /dev/null +++ b/ui/app/components/app/index.scss @@ -0,0 +1,81 @@ +@import 'account-menu/index'; + +@import 'add-token-button/index'; + +@import 'app-header/index'; + +@import '../ui/breadcrumbs/index'; + +@import '../ui/button-group/index'; + +@import '../ui/card/index'; + +@import 'confirm-page-container/index'; + +@import '../ui/currency-input/index'; + +@import '../ui/currency-display/index'; + +@import '../ui/error-message/index'; + +@import '../ui/export-text-container/index'; + +@import '../ui/identicon/index'; + +@import 'info-box/index'; + +@import 'menu-bar/index'; + +@import 'modal/index'; + +@import 'modals/index'; + +@import 'network-display/index'; + +@import '../ui/page-container/index'; + +@import '../../pages/index'; + +@import 'provider-page-container/index'; + +@import 'selected-account/index'; + +@import '../ui/sender-to-recipient/index'; + +@import '../ui/tabs/index'; + +@import '../ui/token-balance/index'; + +@import 'transaction-activity-log/index'; + +@import 'transaction-breakdown/index'; + +@import 'transaction-view/index'; + +@import 'transaction-view-balance/index'; + +@import 'transaction-list/index'; + +@import 'transaction-list-item/index'; + +@import 'transaction-list-item-details/index'; + +@import 'transaction-status/index'; + +@import 'app-header/index'; + +@import 'sidebars/index'; + +@import '../ui/unit-input/index'; + +@import 'gas-customization/gas-modal-page-container/index'; + +@import 'gas-customization/gas-modal-page-container/index'; + +@import 'gas-customization/gas-modal-page-container/index'; + +@import 'gas-customization/index'; + +@import 'gas-customization/gas-price-button-group/index'; + +@import 'ui-migration-annoucement/index'; diff --git a/ui/app/components/info-box/index.js b/ui/app/components/app/info-box/index.js index 6110422ed..6110422ed 100644 --- a/ui/app/components/info-box/index.js +++ b/ui/app/components/app/info-box/index.js diff --git a/ui/app/components/info-box/index.scss b/ui/app/components/app/info-box/index.scss index 8b5626d79..8b5626d79 100644 --- a/ui/app/components/info-box/index.scss +++ b/ui/app/components/app/info-box/index.scss diff --git a/ui/app/components/info-box/info-box.component.js b/ui/app/components/app/info-box/info-box.component.js index 8688b8e8f..8688b8e8f 100644 --- a/ui/app/components/info-box/info-box.component.js +++ b/ui/app/components/app/info-box/info-box.component.js diff --git a/ui/app/components/input-number.js b/ui/app/components/app/input-number.js index eec5e3740..8a6ec725c 100644 --- a/ui/app/components/input-number.js +++ b/ui/app/components/app/input-number.js @@ -6,7 +6,7 @@ const { conversionGTE, conversionLTE, subtractCurrencies, -} = require('../conversion-util') +} = require('../../helpers/utils/conversion-util') module.exports = InputNumber diff --git a/ui/app/components/app/loading-network-screen/index.js b/ui/app/components/app/loading-network-screen/index.js new file mode 100644 index 000000000..726b4b530 --- /dev/null +++ b/ui/app/components/app/loading-network-screen/index.js @@ -0,0 +1 @@ +export { default } from './loading-network-screen.container' diff --git a/ui/app/components/app/loading-network-screen/loading-network-screen.component.js b/ui/app/components/app/loading-network-screen/loading-network-screen.component.js new file mode 100644 index 000000000..348a997c8 --- /dev/null +++ b/ui/app/components/app/loading-network-screen/loading-network-screen.component.js @@ -0,0 +1,138 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Spinner from '../../ui/spinner' +import Button from '../../ui/button' + +export default class LoadingNetworkScreen extends PureComponent { + state = { + showErrorScreen: false, + } + + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + loadingMessage: PropTypes.string, + cancelTime: PropTypes.number, + provider: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + providerId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + showNetworkDropdown: PropTypes.func, + setProviderArgs: PropTypes.array, + lastSelectedProvider: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + setProviderType: PropTypes.func, + isLoadingNetwork: PropTypes.bool, + } + + componentDidMount = () => { + this.cancelCallTimeout = setTimeout(this.cancelCall, this.props.cancelTime || 15000) + } + + getConnectingLabel = function (loadingMessage) { + if (loadingMessage) { + return loadingMessage + } + const { provider, providerId } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = this.context.t('connectingToMainnet') + } else if (providerName === 'ropsten') { + name = this.context.t('connectingToRopsten') + } else if (providerName === 'kovan') { + name = this.context.t('connectingToKovan') + } else if (providerName === 'rinkeby') { + name = this.context.t('connectingToRinkeby') + } else { + name = this.context.t('connectingTo', [providerId]) + } + + return name + } + + renderMessage = () => { + return <span>{ this.getConnectingLabel(this.props.loadingMessage) }</span> + } + + renderLoadingScreenContent = () => { + return <div className="loading-overlay__screen-content"> + <Spinner color="#F7C06C" /> + {this.renderMessage()} + </div> + } + + renderErrorScreenContent = () => { + const { showNetworkDropdown, setProviderArgs, setProviderType } = this.props + + return <div className="loading-overlay__error-screen"> + <span className="loading-overlay__emoji">😞</span> + <span>{ this.context.t('somethingWentWrong') }</span> + <div className="loading-overlay__error-buttons"> + <Button + type="default" + onClick={() => { + window.clearTimeout(this.cancelCallTimeout) + showNetworkDropdown() + }} + > + { this.context.t('switchNetworks') } + </Button> + + <Button + type="primary" + onClick={() => { + this.setState({ showErrorScreen: false }) + setProviderType(...setProviderArgs) + window.clearTimeout(this.cancelCallTimeout) + this.cancelCallTimeout = setTimeout(this.cancelCall, this.props.cancelTime || 15000) + }} + > + { this.context.t('tryAgain') } + </Button> + </div> + </div> + } + + cancelCall = () => { + const { isLoadingNetwork } = this.props + + if (isLoadingNetwork) { + this.setState({ showErrorScreen: true }) + } + } + + componentDidUpdate = (prevProps) => { + const { provider } = this.props + const { provider: prevProvider } = prevProps + if (provider.type !== prevProvider.type) { + window.clearTimeout(this.cancelCallTimeout) + this.setState({ showErrorScreen: false }) + this.cancelCallTimeout = setTimeout(this.cancelCall, this.props.cancelTime || 15000) + } + } + + componentWillUnmount = () => { + window.clearTimeout(this.cancelCallTimeout) + } + + render () { + const { lastSelectedProvider, setProviderType } = this.props + + return ( + <div className="loading-overlay"> + <div + className="page-container__header-close" + onClick={() => setProviderType(lastSelectedProvider || 'ropsten')} + /> + <div className="loading-overlay__container"> + { this.state.showErrorScreen + ? this.renderErrorScreenContent() + : this.renderLoadingScreenContent() + } + </div> + </div> + ) + } +} diff --git a/ui/app/components/app/loading-network-screen/loading-network-screen.container.js b/ui/app/components/app/loading-network-screen/loading-network-screen.container.js new file mode 100644 index 000000000..87f1397ce --- /dev/null +++ b/ui/app/components/app/loading-network-screen/loading-network-screen.container.js @@ -0,0 +1,41 @@ +import { connect } from 'react-redux' +import LoadingNetworkScreen from './loading-network-screen.component' +import actions from '../../../store/actions' +import { getNetworkIdentifier } from '../../../selectors/selectors' + +const mapStateToProps = state => { + const { + loadingMessage, + currentView, + } = state.appState + const { + provider, + lastSelectedProvider, + network, + } = state.metamask + const { rpcTarget, chainId, ticker, nickname, type } = provider + + const setProviderArgs = type === 'rpc' + ? [rpcTarget, chainId, ticker, nickname] + : [provider.type] + + return { + isLoadingNetwork: network === 'loading' && currentView.name !== 'config', + loadingMessage, + lastSelectedProvider, + setProviderArgs, + provider, + providerId: getNetworkIdentifier(state), + } +} + +const mapDispatchToProps = dispatch => { + return { + setProviderType: (type) => { + dispatch(actions.setProviderType(type)) + }, + showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(LoadingNetworkScreen) diff --git a/ui/app/components/menu-bar/index.js b/ui/app/components/app/menu-bar/index.js index c5760847f..c5760847f 100644 --- a/ui/app/components/menu-bar/index.js +++ b/ui/app/components/app/menu-bar/index.js diff --git a/ui/app/components/menu-bar/index.scss b/ui/app/components/app/menu-bar/index.scss index f699f4090..f699f4090 100644 --- a/ui/app/components/menu-bar/index.scss +++ b/ui/app/components/app/menu-bar/index.scss diff --git a/ui/app/components/app/menu-bar/menu-bar.component.js b/ui/app/components/app/menu-bar/menu-bar.component.js new file mode 100644 index 000000000..e37fddda4 --- /dev/null +++ b/ui/app/components/app/menu-bar/menu-bar.component.js @@ -0,0 +1,79 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Tooltip from '../../ui/tooltip' +import SelectedAccount from '../selected-account' +import AccountDetailsDropdown from '../dropdowns/account-details-dropdown.js' + +export default class MenuBar extends PureComponent { + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + static propTypes = { + hideSidebar: PropTypes.func, + sidebarOpen: PropTypes.bool, + showSidebar: PropTypes.func, + } + + state = { accountDetailsMenuOpen: false } + + render () { + const { t } = this.context + const { sidebarOpen, hideSidebar, showSidebar } = this.props + const { accountDetailsMenuOpen } = this.state + + return ( + <div className="menu-bar"> + <Tooltip + title={t('menu')} + position="bottom" + > + <div + className="fa fa-bars menu-bar__sidebar-button" + onClick={() => { + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Opened Hamburger', + }, + }) + sidebarOpen ? hideSidebar() : showSidebar() + }} + /> + </Tooltip> + <SelectedAccount /> + + <Tooltip + title={t('accountOptions')} + position="bottom" + > + <div + className="fa fa-ellipsis-h fa-lg menu-bar__open-in-browser" + onClick={() => { + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Opened Account Options', + }, + }) + this.setState({ accountDetailsMenuOpen: true }) + }} + > + </div> + </Tooltip> + + { + accountDetailsMenuOpen && ( + <AccountDetailsDropdown + className="menu-bar__account-details-dropdown" + onClose={() => this.setState({ accountDetailsMenuOpen: false })} + /> + ) + } + </div> + ) + } +} diff --git a/ui/app/components/menu-bar/menu-bar.container.js b/ui/app/components/app/menu-bar/menu-bar.container.js index ae32882ae..059263ff3 100644 --- a/ui/app/components/menu-bar/menu-bar.container.js +++ b/ui/app/components/app/menu-bar/menu-bar.container.js @@ -1,13 +1,13 @@ import { connect } from 'react-redux' +import { WALLET_VIEW_SIDEBAR } from '../sidebars/sidebar.constants' import MenuBar from './menu-bar.component' -import { showSidebar, hideSidebar } from '../../actions' +import { showSidebar, hideSidebar } from '../../../store/actions' const mapStateToProps = state => { - const { appState: { sidebar: { isOpen }, isMascara } } = state + const { appState: { sidebar: { isOpen } } } = state return { sidebarOpen: isOpen, - isMascara, } } @@ -16,7 +16,7 @@ const mapDispatchToProps = dispatch => { showSidebar: () => { dispatch(showSidebar({ transitionName: 'sidebar-right', - type: 'wallet-view', + type: WALLET_VIEW_SIDEBAR, })) }, hideSidebar: () => dispatch(hideSidebar()), diff --git a/ui/app/components/menu-droppo.js b/ui/app/components/app/menu-droppo.js index c80bee2be..c80bee2be 100644 --- a/ui/app/components/menu-droppo.js +++ b/ui/app/components/app/menu-droppo.js diff --git a/ui/app/components/modal/index.js b/ui/app/components/app/modal/index.js index 58309abbe..58309abbe 100644 --- a/ui/app/components/modal/index.js +++ b/ui/app/components/app/modal/index.js diff --git a/ui/app/components/modal/index.scss b/ui/app/components/app/modal/index.scss index 2beb14633..ec67d15fd 100644 --- a/ui/app/components/modal/index.scss +++ b/ui/app/components/app/modal/index.scss @@ -1,4 +1,4 @@ -@import './modal-content/index'; +@import 'modal-content/index'; .modal-container { width: 100%; diff --git a/ui/app/components/modal/modal-content/index.js b/ui/app/components/app/modal/modal-content/index.js index 733cfb3b8..733cfb3b8 100644 --- a/ui/app/components/modal/modal-content/index.js +++ b/ui/app/components/app/modal/modal-content/index.js diff --git a/ui/app/components/modal/modal-content/index.scss b/ui/app/components/app/modal/modal-content/index.scss index 560505b84..560505b84 100644 --- a/ui/app/components/modal/modal-content/index.scss +++ b/ui/app/components/app/modal/modal-content/index.scss diff --git a/ui/app/components/modal/modal-content/modal-content.component.js b/ui/app/components/app/modal/modal-content/modal-content.component.js index ecec0ee5b..ecec0ee5b 100644 --- a/ui/app/components/modal/modal-content/modal-content.component.js +++ b/ui/app/components/app/modal/modal-content/modal-content.component.js diff --git a/ui/app/components/modal/modal-content/tests/modal-content.component.test.js b/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js index 17af09f45..17af09f45 100644 --- a/ui/app/components/modal/modal-content/tests/modal-content.component.test.js +++ b/ui/app/components/app/modal/modal-content/tests/modal-content.component.test.js diff --git a/ui/app/components/modal/modal.component.js b/ui/app/components/app/modal/modal.component.js index 2a75b559b..49e131b3c 100644 --- a/ui/app/components/modal/modal.component.js +++ b/ui/app/components/app/modal/modal.component.js @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import Button from '../button' +import Button from '../../ui/button' export default class Modal extends PureComponent { static propTypes = { @@ -12,6 +12,7 @@ export default class Modal extends PureComponent { onSubmit: PropTypes.func, submitType: PropTypes.string, submitText: PropTypes.string, + submitDisabled: PropTypes.bool, // Cancel button (left button) onCancel: PropTypes.func, cancelType: PropTypes.string, @@ -31,6 +32,7 @@ export default class Modal extends PureComponent { onSubmit, submitType, submitText, + submitDisabled, onCancel, cancelType, cancelText, @@ -69,6 +71,7 @@ export default class Modal extends PureComponent { <Button type={submitType} onClick={onSubmit} + disabled={submitDisabled} className="modal-container__footer-button" > { submitText } diff --git a/ui/app/components/modal/tests/modal.component.test.js b/ui/app/components/app/modal/tests/modal.component.test.js index 8cce1a808..a13d7c06a 100644 --- a/ui/app/components/modal/tests/modal.component.test.js +++ b/ui/app/components/app/modal/tests/modal.component.test.js @@ -1,9 +1,9 @@ import React from 'react' import assert from 'assert' -import { shallow } from 'enzyme' +import { mount, shallow } from 'enzyme' import sinon from 'sinon' import Modal from '../modal.component' -import Button from '../../button' +import Button from '../../../ui/button' describe('Modal Component', () => { it('should render a modal with a submit button', () => { @@ -100,4 +100,34 @@ describe('Modal Component', () => { assert.equal(handleCancel.callCount, 1) assert.equal(handleSubmit.callCount, 0) }) + + it('should disable the submit button if submitDisabled is true', () => { + const handleCancel = sinon.spy() + const handleSubmit = sinon.spy() + const wrapper = mount( + <Modal + onCancel={handleCancel} + cancelText="Cancel" + onSubmit={handleSubmit} + submitText="Submit" + submitDisabled={true} + headerText="My Header" + onClose={handleCancel} + /> + ) + + const buttons = wrapper.find(Button) + assert.equal(buttons.length, 2) + const cancelButton = buttons.at(0) + const submitButton = buttons.at(1) + + assert.equal(handleCancel.callCount, 0) + cancelButton.simulate('click') + assert.equal(handleCancel.callCount, 1) + + assert.equal(submitButton.props().disabled, true) + assert.equal(handleSubmit.callCount, 0) + submitButton.simulate('click') + assert.equal(handleSubmit.callCount, 0) + }) }) diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/app/modals/account-details-modal.js index 248ffe008..94ed04df9 100644 --- a/ui/app/components/modals/account-details-modal.js +++ b/ui/app/components/app/modals/account-details-modal.js @@ -3,14 +3,14 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const actions = require('../../actions') +const actions = require('../../../store/actions') const AccountModalContainer = require('./account-modal-container') -const { getSelectedIdentity } = require('../../selectors') -const genAccountLink = require('../../../lib/account-link.js') -const QrView = require('../qr-code') -const EditableLabel = require('../editable-label') +const { getSelectedIdentity } = require('../../../selectors/selectors') +const genAccountLink = require('../../../../lib/account-link.js') +const QrView = require('../../ui/qr-code') +const EditableLabel = require('../../ui/editable-label') -import Button from '../button' +import Button from '../../ui/button' function mapStateToProps (state) { return { @@ -77,6 +77,7 @@ AccountDetailsModal.prototype.render = function () { h(QrView, { Qr: { data: address, + network: network, }, }), diff --git a/ui/app/components/modals/account-modal-container.js b/ui/app/components/app/modals/account-modal-container.js index 2a6c655e1..b7ae0b5b8 100644 --- a/ui/app/components/modals/account-modal-container.js +++ b/ui/app/components/app/modals/account-modal-container.js @@ -3,9 +3,9 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const actions = require('../../actions') -const { getSelectedIdentity } = require('../../selectors') -import Identicon from '../identicon' +const actions = require('../../../store/actions') +const { getSelectedIdentity } = require('../../../selectors/selectors') +import Identicon from '../../ui/identicon' function mapStateToProps (state, ownProps) { return { diff --git a/ui/app/components/modals/buy-options-modal.js b/ui/app/components/app/modals/buy-options-modal.js index c70510b5f..2df20e65c 100644 --- a/ui/app/components/modals/buy-options-modal.js +++ b/ui/app/components/app/modals/buy-options-modal.js @@ -3,8 +3,8 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const actions = require('../../actions') -const { getNetworkDisplayName } = require('../../../../app/scripts/controllers/network/util') +const actions = require('../../../store/actions') +const { getNetworkDisplayName } = require('../../../../../app/scripts/controllers/network/util') function mapStateToProps (state) { return { diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js index b973f221c..beebb7ed7 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display' -import { PRIMARY, SECONDARY } from '../../../../constants/common' +import { PRIMARY, SECONDARY } from '../../../../../helpers/constants/common' export default class CancelTransaction extends PureComponent { static propTypes = { diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.js index 1a9ae2e07..1a9ae2e07 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.js +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.js diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss index ce81dd448..ce81dd448 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/index.scss diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js index 014815503..014815503 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction.component.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.component.js index 8b00cb9b9..6bab5ec1f 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction.component.js +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.component.js @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import Modal from '../../modal' import CancelTransactionGasFee from './cancel-transaction-gas-fee' -import { SUBMITTED_STATUS } from '../../../constants/transactions' +import { SUBMITTED_STATUS } from '../../../../helpers/constants/transactions' export default class CancelTransaction extends PureComponent { static contextTypes = { @@ -17,6 +17,10 @@ export default class CancelTransaction extends PureComponent { newGasFee: PropTypes.string, } + state = { + busy: false, + } + componentDidUpdate () { const { transactionStatus, showTransactionConfirmedModal } = this.props @@ -29,8 +33,10 @@ export default class CancelTransaction extends PureComponent { handleSubmit = async () => { const { createCancelTransaction, hideModal } = this.props + this.setState({ busy: true }) + await createCancelTransaction() - hideModal() + this.setState({ busy: false }, () => hideModal()) } handleCancel = () => { @@ -40,6 +46,7 @@ export default class CancelTransaction extends PureComponent { render () { const { t } = this.context const { newGasFee } = this.props + const { busy } = this.state return ( <Modal @@ -50,6 +57,7 @@ export default class CancelTransaction extends PureComponent { submitText={t('yesLetsTry')} cancelText={t('nevermind')} submitType="secondary" + submitDisabled={busy} > <div> <div className="cancel-transaction__title"> diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.container.js index eede8b1ee..6959889d9 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction.container.js +++ b/ui/app/components/app/modals/cancel-transaction/cancel-transaction.container.js @@ -1,11 +1,11 @@ import { connect } from 'react-redux' import { compose } from 'recompose' import ethUtil from 'ethereumjs-util' -import { multiplyCurrencies } from '../../../conversion-util' -import withModalProps from '../../../higher-order-components/with-modal-props' +import { multiplyCurrencies } from '../../../../helpers/utils/conversion-util' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' import CancelTransaction from './cancel-transaction.component' -import { showModal, createCancelTransaction } from '../../../actions' -import { getHexGasTotal } from '../../../helpers/confirm-transaction/util' +import { showModal, createCancelTransaction } from '../../../../store/actions' +import { getHexGasTotal } from '../../../../helpers/utils/confirm-tx.util' const mapStateToProps = (state, ownProps) => { const { metamask } = state @@ -28,31 +28,29 @@ const mapStateToProps = (state, ownProps) => { transactionId, transactionStatus, originalGasPrice, + defaultNewGasPrice, newGasFee, } } const mapDispatchToProps = dispatch => { return { - createCancelTransaction: txId => dispatch(createCancelTransaction(txId)), + createCancelTransaction: (txId, customGasPrice) => { + return dispatch(createCancelTransaction(txId, customGasPrice)) + }, showTransactionConfirmedModal: () => dispatch(showModal({ name: 'TRANSACTION_CONFIRMED' })), } } const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { transactionId, ...restStateProps } = stateProps - const { - createCancelTransaction: dispatchCreateCancelTransaction, - ...restDispatchProps - } = dispatchProps + const { transactionId, defaultNewGasPrice, ...restStateProps } = stateProps + const { createCancelTransaction, ...restDispatchProps } = dispatchProps return { ...restStateProps, ...restDispatchProps, ...ownProps, - createCancelTransaction: newGasPrice => { - return dispatchCreateCancelTransaction(transactionId, newGasPrice) - }, + createCancelTransaction: () => createCancelTransaction(transactionId, defaultNewGasPrice), } } diff --git a/ui/app/components/modals/cancel-transaction/index.js b/ui/app/components/app/modals/cancel-transaction/index.js index 7abc871ee..7abc871ee 100644 --- a/ui/app/components/modals/cancel-transaction/index.js +++ b/ui/app/components/app/modals/cancel-transaction/index.js diff --git a/ui/app/components/modals/cancel-transaction/index.scss b/ui/app/components/app/modals/cancel-transaction/index.scss index 62e8e36fd..4ffb5a0f8 100644 --- a/ui/app/components/modals/cancel-transaction/index.scss +++ b/ui/app/components/app/modals/cancel-transaction/index.scss @@ -1,4 +1,4 @@ -@import './cancel-transaction-gas-fee/index'; +@import 'cancel-transaction-gas-fee/index'; .cancel-transaction { &__title { @@ -15,4 +15,4 @@ &__cancel-transaction-gas-fee-container { margin-bottom: 16px; } -}
\ No newline at end of file +} diff --git a/ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js b/ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js index 858fb01a8..345951b0f 100644 --- a/ui/app/components/modals/cancel-transaction/tests/cancel-transaction.component.test.js +++ b/ui/app/components/app/modals/cancel-transaction/tests/cancel-transaction.component.test.js @@ -34,6 +34,7 @@ describe('CancelTransaction Component', () => { defaultNewGasPrice="0x3b9aca00" createCancelTransaction={createCancelTransactionSpy} hideModal={hideModalSpy} + showTransactionConfirmedModal={() => {}} />, { context: { t }} ) diff --git a/ui/app/components/modals/clear-approved-origins/clear-approved-origins.component.js b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js index ceaa20a95..ceaa20a95 100644 --- a/ui/app/components/modals/clear-approved-origins/clear-approved-origins.component.js +++ b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.component.js diff --git a/ui/app/components/modals/clear-approved-origins/clear-approved-origins.container.js b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js index 3a801a062..2276bc7e7 100644 --- a/ui/app/components/modals/clear-approved-origins/clear-approved-origins.container.js +++ b/ui/app/components/app/modals/clear-approved-origins/clear-approved-origins.container.js @@ -1,8 +1,8 @@ import { connect } from 'react-redux' import { compose } from 'recompose' -import withModalProps from '../../../higher-order-components/with-modal-props' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' import ClearApprovedOriginsComponent from './clear-approved-origins.component' -import { clearApprovedOrigins } from '../../../actions' +import { clearApprovedOrigins } from '../../../../store/actions' const mapDispatchToProps = dispatch => { return { diff --git a/ui/app/components/modals/clear-approved-origins/index.js b/ui/app/components/app/modals/clear-approved-origins/index.js index b3e321995..b3e321995 100644 --- a/ui/app/components/modals/clear-approved-origins/index.js +++ b/ui/app/components/app/modals/clear-approved-origins/index.js diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.component.js index 195c55421..f35fb85a0 100644 --- a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js +++ b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.component.js @@ -1,9 +1,9 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import Modal from '../../modal' -import { addressSummary } from '../../../util' -import Identicon from '../../identicon' -import genAccountLink from '../../../../lib/account-link' +import { addressSummary } from '../../../../helpers/utils/util' +import Identicon from '../../../ui/identicon' +import genAccountLink from '../../../../../lib/account-link' export default class ConfirmRemoveAccount extends Component { static propTypes = { diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.container.js index 45c6654ab..0a3cda5b6 100644 --- a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js +++ b/ui/app/components/app/modals/confirm-remove-account/confirm-remove-account.container.js @@ -1,8 +1,8 @@ import { connect } from 'react-redux' import { compose } from 'recompose' import ConfirmRemoveAccount from './confirm-remove-account.component' -import withModalProps from '../../../higher-order-components/with-modal-props' -import { removeAccount } from '../../../actions' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' +import { removeAccount } from '../../../../store/actions' const mapStateToProps = state => { return { diff --git a/ui/app/components/modals/confirm-remove-account/index.js b/ui/app/components/app/modals/confirm-remove-account/index.js index ecb5f7790..ecb5f7790 100644 --- a/ui/app/components/modals/confirm-remove-account/index.js +++ b/ui/app/components/app/modals/confirm-remove-account/index.js diff --git a/ui/app/components/modals/confirm-remove-account/index.scss b/ui/app/components/app/modals/confirm-remove-account/index.scss index 3be3a1967..3be3a1967 100644 --- a/ui/app/components/modals/confirm-remove-account/index.scss +++ b/ui/app/components/app/modals/confirm-remove-account/index.scss diff --git a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.component.js index f1a4542ac..f1a4542ac 100644 --- a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.component.js +++ b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.component.js diff --git a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.container.js index c8a7b8478..ffbd40d9d 100644 --- a/ui/app/components/modals/confirm-reset-account/confirm-reset-account.container.js +++ b/ui/app/components/app/modals/confirm-reset-account/confirm-reset-account.container.js @@ -1,8 +1,8 @@ import { connect } from 'react-redux' import { compose } from 'recompose' -import withModalProps from '../../../higher-order-components/with-modal-props' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' import ConfirmResetAccount from './confirm-reset-account.component' -import { resetAccount } from '../../../actions' +import { resetAccount } from '../../../../store/actions' const mapDispatchToProps = dispatch => { return { diff --git a/ui/app/components/modals/confirm-reset-account/index.js b/ui/app/components/app/modals/confirm-reset-account/index.js index ca4d9c5bf..ca4d9c5bf 100644 --- a/ui/app/components/modals/confirm-reset-account/index.js +++ b/ui/app/components/app/modals/confirm-reset-account/index.js diff --git a/ui/app/components/modals/customize-gas/customize-gas.component.js b/ui/app/components/app/modals/customize-gas/customize-gas.component.js index 3f526bd43..5db5c79e7 100644 --- a/ui/app/components/modals/customize-gas/customize-gas.component.js +++ b/ui/app/components/app/modals/customize-gas/customize-gas.component.js @@ -1,8 +1,9 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' +import BigNumber from 'bignumber.js' import GasModalCard from '../../customize-gas-modal/gas-modal-card' import { MIN_GAS_PRICE_GWEI } from '../../send/send.constants' -import Button from '../../button' +import Button from '../../../ui/button' import { getDecimalGasLimit, @@ -14,6 +15,7 @@ import { export default class CustomizeGas extends Component { static contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } static propTypes = { @@ -73,9 +75,9 @@ export default class CustomizeGas extends Component { } render () { - const { t } = this.context + const { t, metricsEvent } = this.context const { hideModal } = this.props - const { gasPrice, gasLimit } = this.state + const { gasPrice, gasLimit, originalGasPrice, originalGasLimit } = this.state const { valid, errorKey } = this.validate() return ( @@ -128,7 +130,24 @@ export default class CustomizeGas extends Component { <Button type="primary" className="customize-gas__save" - onClick={() => this.handleSave()} + onClick={() => { + metricsEvent({ + eventOpts: { + category: 'Activation', + action: 'userCloses', + name: 'closeCustomizeGas', + }, + pageOpts: { + section: 'customizeGasModal', + component: 'customizeGasSaveButton', + }, + customVariables: { + gasPriceChange: (new BigNumber(gasPrice)).minus(new BigNumber(originalGasPrice)).toString(10), + gasLimitChange: (new BigNumber(gasLimit)).minus(new BigNumber(originalGasLimit)).toString(10), + }, + }) + this.handleSave() + }} style={{ marginRight: '10px' }} disabled={!valid} > diff --git a/ui/app/components/modals/customize-gas/customize-gas.container.js b/ui/app/components/app/modals/customize-gas/customize-gas.container.js index 46a799795..221881a8a 100644 --- a/ui/app/components/modals/customize-gas/customize-gas.container.js +++ b/ui/app/components/app/modals/customize-gas/customize-gas.container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux' import CustomizeGas from './customize-gas.component' -import { hideModal } from '../../../actions' +import { hideModal } from '../../../../store/actions' const mapStateToProps = state => { const { appState: { modal: { modalState: { props } } } } = state diff --git a/ui/app/components/modals/customize-gas/customize-gas.util.js b/ui/app/components/app/modals/customize-gas/customize-gas.util.js index 6ba4a7705..e686183bd 100644 --- a/ui/app/components/modals/customize-gas/customize-gas.util.js +++ b/ui/app/components/app/modals/customize-gas/customize-gas.util.js @@ -1,5 +1,5 @@ import ethUtil from 'ethereumjs-util' -import { conversionUtil } from '../../../conversion-util' +import { conversionUtil } from '../../../../helpers/utils/conversion-util' export function getDecimalGasLimit (hexGasLimit) { return conversionUtil(hexGasLimit, { diff --git a/ui/app/components/modals/customize-gas/index.js b/ui/app/components/app/modals/customize-gas/index.js index 3a0ab7edc..3a0ab7edc 100644 --- a/ui/app/components/modals/customize-gas/index.js +++ b/ui/app/components/app/modals/customize-gas/index.js diff --git a/ui/app/components/modals/customize-gas/index.scss b/ui/app/components/app/modals/customize-gas/index.scss index e10452691..e10452691 100644 --- a/ui/app/components/modals/customize-gas/index.scss +++ b/ui/app/components/app/modals/customize-gas/index.scss diff --git a/ui/app/components/modals/deposit-ether-modal.js b/ui/app/components/app/modals/deposit-ether-modal.js index a64b3282e..def88f085 100644 --- a/ui/app/components/modals/deposit-ether-modal.js +++ b/ui/app/components/app/modals/deposit-ether-modal.js @@ -3,16 +3,16 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const actions = require('../../actions') -const { getNetworkDisplayName } = require('../../../../app/scripts/controllers/network/util') +const actions = require('../../../store/actions') +const { getNetworkDisplayName } = require('../../../../../app/scripts/controllers/network/util') const ShapeshiftForm = require('../shapeshift-form') -import Button from '../button' +import Button from '../../ui/button' let DIRECT_DEPOSIT_ROW_TITLE let DIRECT_DEPOSIT_ROW_TEXT -let COINBASE_ROW_TITLE -let COINBASE_ROW_TEXT +let WYRE_ROW_TITLE +let WYRE_ROW_TEXT let SHAPESHIFT_ROW_TITLE let SHAPESHIFT_ROW_TEXT let FAUCET_ROW_TITLE @@ -54,8 +54,8 @@ function DepositEtherModal (props, context) { // need to set after i18n locale has loaded DIRECT_DEPOSIT_ROW_TITLE = context.t('directDepositEther') DIRECT_DEPOSIT_ROW_TEXT = context.t('directDepositEtherExplainer') - COINBASE_ROW_TITLE = context.t('buyCoinbase') - COINBASE_ROW_TEXT = context.t('buyCoinbaseExplainer') + WYRE_ROW_TITLE = context.t('buyWithWyre') + WYRE_ROW_TEXT = context.t('buyWithWyreDescription') SHAPESHIFT_ROW_TITLE = context.t('depositShapeShift') SHAPESHIFT_ROW_TEXT = context.t('depositShapeShiftExplainer') FAUCET_ROW_TITLE = context.t('testFaucet') @@ -183,13 +183,13 @@ DepositEtherModal.prototype.render = function () { this.renderRow({ logo: h('div.deposit-ether-modal__logo', { style: { - backgroundImage: 'url(\'./images/coinbase logo.png\')', + backgroundImage: 'url(\'./images/wyre.svg\')', height: '40px', }, }), - title: COINBASE_ROW_TITLE, - text: COINBASE_ROW_TEXT, - buttonLabel: this.context.t('continueToCoinbase'), + title: WYRE_ROW_TITLE, + text: WYRE_ROW_TEXT, + buttonLabel: this.context.t('continueToWyre'), onButtonClick: () => toCoinbase(address), hide: isTestNetwork || buyingWithShapeshift, }), diff --git a/ui/app/components/modals/edit-account-name-modal.js b/ui/app/components/app/modals/edit-account-name-modal.js index edced8725..41a9862e9 100644 --- a/ui/app/components/modals/edit-account-name-modal.js +++ b/ui/app/components/app/modals/edit-account-name-modal.js @@ -3,8 +3,8 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const actions = require('../../actions') -const { getSelectedAccount } = require('../../selectors') +const actions = require('../../../store/actions') +const { getSelectedAccount } = require('../../../selectors/selectors') function mapStateToProps (state) { return { diff --git a/ui/app/components/modals/export-private-key-modal.js b/ui/app/components/app/modals/export-private-key-modal.js index d3e3c9a56..639887d4c 100644 --- a/ui/app/components/modals/export-private-key-modal.js +++ b/ui/app/components/app/modals/export-private-key-modal.js @@ -5,13 +5,13 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect const { stripHexPrefix } = require('ethereumjs-util') -const actions = require('../../actions') +const actions = require('../../../store/actions') const AccountModalContainer = require('./account-modal-container') -const { getSelectedIdentity } = require('../../selectors') -const ReadOnlyInput = require('../readonly-input') +const { getSelectedIdentity } = require('../../../selectors/selectors') +const ReadOnlyInput = require('../../ui/readonly-input') const copyToClipboard = require('copy-to-clipboard') -const { checksumAddress } = require('../../util') -import Button from '../button' +const { checksumAddress } = require('../../../helpers/utils/util') +import Button from '../../ui/button' function mapStateToPropsFactory () { let selectedIdentity = null diff --git a/ui/app/components/modals/hide-token-confirmation-modal.js b/ui/app/components/app/modals/hide-token-confirmation-modal.js index 43f3009a5..8a9a48fd2 100644 --- a/ui/app/components/modals/hide-token-confirmation-modal.js +++ b/ui/app/components/app/modals/hide-token-confirmation-modal.js @@ -3,8 +3,8 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const actions = require('../../actions') -import Identicon from '../identicon' +const actions = require('../../../store/actions') +import Identicon from '../../ui/identicon' function mapStateToProps (state) { return { diff --git a/ui/app/components/modals/index.js b/ui/app/components/app/modals/index.js index 1db1d33d4..1db1d33d4 100644 --- a/ui/app/components/modals/index.js +++ b/ui/app/components/app/modals/index.js diff --git a/ui/app/components/app/modals/index.scss b/ui/app/components/app/modals/index.scss new file mode 100644 index 000000000..09b0bb73c --- /dev/null +++ b/ui/app/components/app/modals/index.scss @@ -0,0 +1,11 @@ +@import 'cancel-transaction/index'; + +@import 'confirm-remove-account/index'; + +@import 'customize-gas/index'; + +@import 'qr-scanner/index'; + +@import 'transaction-confirmed/index'; + +@import 'metametrics-opt-in-modal/index'; diff --git a/ui/app/components/app/modals/loading-network-error/index.js b/ui/app/components/app/modals/loading-network-error/index.js new file mode 100644 index 000000000..b3737458a --- /dev/null +++ b/ui/app/components/app/modals/loading-network-error/index.js @@ -0,0 +1 @@ +export { default } from './loading-network-error.container' diff --git a/ui/app/components/modals/welcome-beta/welcome-beta.component.js b/ui/app/components/app/modals/loading-network-error/loading-network-error.component.js index ef1799164..44f71e4b2 100644 --- a/ui/app/components/modals/welcome-beta/welcome-beta.component.js +++ b/ui/app/components/app/modals/loading-network-error/loading-network-error.component.js @@ -2,29 +2,28 @@ import React from 'react' import PropTypes from 'prop-types' import Modal, { ModalContent } from '../../modal' -const TransactionConfirmed = (props, context) => { +const LoadingNetworkError = (props, context) => { const { t } = context const { hideModal } = props return ( <Modal onSubmit={() => hideModal()} - submitText={t('ok')} + submitText={t('tryAgain')} > <ModalContent - title={t('uiWelcome')} - description={t('uiWelcomeMessage')} + description={'Oops! Something went wrong.'} /> </Modal> ) } -TransactionConfirmed.contextTypes = { +LoadingNetworkError.contextTypes = { t: PropTypes.func, } -TransactionConfirmed.propTypes = { +LoadingNetworkError.propTypes = { hideModal: PropTypes.func, } -export default TransactionConfirmed +export default LoadingNetworkError diff --git a/ui/app/components/app/modals/loading-network-error/loading-network-error.container.js b/ui/app/components/app/modals/loading-network-error/loading-network-error.container.js new file mode 100644 index 000000000..38ea9b2ab --- /dev/null +++ b/ui/app/components/app/modals/loading-network-error/loading-network-error.container.js @@ -0,0 +1,4 @@ +import LoadingNetworkError from './loading-network-error.component' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' + +export default withModalProps(LoadingNetworkError) diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/index.js b/ui/app/components/app/modals/metametrics-opt-in-modal/index.js new file mode 100644 index 000000000..47f946757 --- /dev/null +++ b/ui/app/components/app/modals/metametrics-opt-in-modal/index.js @@ -0,0 +1 @@ +export { default } from './metametrics-opt-in-modal.container' diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/index.scss b/ui/app/components/app/modals/metametrics-opt-in-modal/index.scss new file mode 100644 index 000000000..88b6d7a4d --- /dev/null +++ b/ui/app/components/app/modals/metametrics-opt-in-modal/index.scss @@ -0,0 +1,30 @@ +.metametrics-opt-in-modal { + .metametrics-opt-in__main { + justify-content: center; + margin-left: 3%; + margin-right: 0%; + max-height: 75vh; + + @media screen and (max-width: 575px) { + max-height: 100vh; + } + } + + + .metametrics-opt-in__title { + font-size: 38px; + } + + .metametrics-opt-in__content { + padding-right: 6px; + } + + .metametrics-opt-in__footer { + @media screen and (max-width: 575px) { + margin-top: 10px; + justify-content: center; + margin-left: 2%; + max-height: 520px; + } + } +}
\ No newline at end of file diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js new file mode 100644 index 000000000..0335991fc --- /dev/null +++ b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.component.js @@ -0,0 +1,141 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import PageContainerFooter from '../../../ui/page-container/page-container-footer' + +export default class MetaMetricsOptInModal extends Component { + static propTypes = { + setParticipateInMetaMetrics: PropTypes.func, + hideModal: PropTypes.func, + } + + static contextTypes = { + metricsEvent: PropTypes.func, + } + + render () { + const { metricsEvent } = this.context + const { setParticipateInMetaMetrics, hideModal } = this.props + + return ( + <div className="metametrics-opt-in metametrics-opt-in-modal"> + <div className="metametrics-opt-in__main"> + <div className="metametrics-opt-in__content"> + <div className="app-header__logo-container"> + <img + className="app-header__metafox-logo app-header__metafox-logo--horizontal" + src="/images/logo/metamask-logo-horizontal.svg" + height={30} + /> + <img + className="app-header__metafox-logo app-header__metafox-logo--icon" + src="/images/logo/metamask-fox.svg" + height={42} + width={42} + /> + </div> + <div className="metametrics-opt-in__body-graphic"> + <img src="images/metrics-chart.svg" /> + </div> + <div className="metametrics-opt-in__title">Help Us Improve MetaMask</div> + <div className="metametrics-opt-in__body"> + <div className="metametrics-opt-in__description"> + MetaMask would like to gather usage data to better understand how our users interact with the extension. This data + will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem. + </div> + <div className="metametrics-opt-in__description"> + MetaMask will.. + </div> + + <div className="metametrics-opt-in__committments"> + <div className="metametrics-opt-in__row"> + <i className="fa fa-check" /> + <div className="metametrics-opt-in__row-description"> + Always allow you to opt-out via Settings + </div> + </div> + <div className="metametrics-opt-in__row"> + <i className="fa fa-check" /> + <div className="metametrics-opt-in__row-description"> + Send anonymized click & pageview events + </div> + </div> + <div className="metametrics-opt-in__row"> + <i className="fa fa-check" /> + <div className="metametrics-opt-in__row-description"> + Maintain a public aggregate dashboard to educate the community + </div> + </div> + <div className="metametrics-opt-in__row metametrics-opt-in__break-row"> + <i className="fa fa-times" /> + <div className="metametrics-opt-in__row-description"> + <span className="metametrics-opt-in__bold">Never</span> collect keys, addresses, transactions, balances, hashes, or any personal information + </div> + </div> + <div className="metametrics-opt-in__row"> + <i className="fa fa-times" /> + <div className="metametrics-opt-in__row-description"> + <span className="metametrics-opt-in__bold">Never</span> collect your full IP address + </div> + </div> + <div className="metametrics-opt-in__row"> + <i className="fa fa-times" /> + <div className="metametrics-opt-in__row-description"> + <span className="metametrics-opt-in__bold">Never</span> sell data for profit. Ever! + </div> + </div> + </div> + </div> + <div className="metametrics-opt-in__bottom-text"> + This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our <a + href="https://metamask.io/privacy.html" + target="_blank" + rel="noopener noreferrer" + > + Privacy Policy here + </a>. + </div> + </div> + <div className="metametrics-opt-in__footer"> + <PageContainerFooter + onCancel={() => { + setParticipateInMetaMetrics(false) + .then(() => { + metricsEvent({ + eventOpts: { + category: 'Onboarding', + action: 'Metrics Option', + name: 'Metrics Opt Out', + }, + isOptIn: true, + }, { + excludeMetaMetricsId: true, + }) + hideModal() + }) + }} + cancelText={'No Thanks'} + hideCancel={false} + onSubmit={() => { + setParticipateInMetaMetrics(true) + .then(() => { + metricsEvent({ + eventOpts: { + category: 'Onboarding', + action: 'Metrics Option', + name: 'Metrics Opt In', + }, + isOptIn: true, + }) + hideModal() + }) + }} + submitText={'I agree'} + submitButtonType={'confirm'} + disabled={false} + /> + </div> + </div> + </div> + ) + } +} diff --git a/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js new file mode 100644 index 000000000..83595281f --- /dev/null +++ b/ui/app/components/app/modals/metametrics-opt-in-modal/metametrics-opt-in-modal.container.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import MetaMetricsOptInModal from './metametrics-opt-in-modal.component' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' +import { setParticipateInMetaMetrics } from '../../../../store/actions' + +const mapStateToProps = (state, ownProps) => { + const { unapprovedTxCount } = ownProps + + return { + unapprovedTxCount, + } +} + +const mapDispatchToProps = dispatch => { + return { + setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)), + } +} + +export default compose( + withModalProps, + connect(mapStateToProps, mapDispatchToProps), +)(MetaMetricsOptInModal) diff --git a/ui/app/components/modals/modal.js b/ui/app/components/app/modals/modal.js index 5aff4f5e1..717f623af 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/app/modals/modal.js @@ -3,10 +3,11 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect const FadeModal = require('boron').FadeModal -const actions = require('../../actions') -const isMobileView = require('../../../lib/is-mobile-view') -const { getEnvironmentType } = require('../../../../app/scripts/lib/util') -const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums') +const actions = require('../../../store/actions') +const { resetCustomData: resetCustomGasData } = require('../../../ducks/gas/gas.duck') +const isMobileView = require('../../../../lib/is-mobile-view') +const { getEnvironmentType } = require('../../../../../app/scripts/lib/util') +const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums') // Modal Components const BuyOptions = require('./buy-options-modal') @@ -17,18 +18,18 @@ const ExportPrivateKeyModal = require('./export-private-key-modal') const NewAccountModal = require('./new-account-modal') const ShapeshiftDepositTxModal = require('./shapeshift-deposit-tx-modal.js') const HideTokenConfirmationModal = require('./hide-token-confirmation-modal') -const CustomizeGasModal = require('../customize-gas-modal') const NotifcationModal = require('./notification-modal') const QRScanner = require('./qr-scanner') import ConfirmRemoveAccount from './confirm-remove-account' import ConfirmResetAccount from './confirm-reset-account' import TransactionConfirmed from './transaction-confirmed' -import ConfirmCustomizeGasModal from './customize-gas' import CancelTransaction from './cancel-transaction' -import WelcomeBeta from './welcome-beta' + +import MetaMetricsOptInModal from './metametrics-opt-in-modal' import RejectTransactions from './reject-transactions' import ClearApprovedOrigins from './clear-approved-origins' +import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container' const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -122,7 +123,8 @@ const MODALS = { display: 'flex', }, laptopModalStyle: { - width: '850px', + width: 'initial', + maxWidth: '850px', top: 'calc(10% + 10px)', left: '0', right: '0', @@ -200,8 +202,8 @@ const MODALS = { }, }, - BETA_UI_NOTIFICATION_MODAL: { - contents: h(WelcomeBeta), + CLEAR_APPROVED_ORIGINS: { + contents: h(ClearApprovedOrigins), mobileModalStyle: { ...modalContainerMobileStyle, }, @@ -213,13 +215,17 @@ const MODALS = { }, }, - CLEAR_APPROVED_ORIGINS: { - contents: h(ClearApprovedOrigins), + METAMETRICS_OPT_IN_MODAL: { + contents: h(MetaMetricsOptInModal), mobileModalStyle: { ...modalContainerMobileStyle, + width: '100%', + height: '100%', + top: '0px', }, laptopModalStyle: { ...modalContainerLaptopStyle, + top: '10%', }, contentStyle: { borderRadius: '8px', @@ -243,6 +249,40 @@ const MODALS = { }, }, + GAS_PRICE_INFO_MODAL: { + contents: [ + h(NotifcationModal, { + header: 'gasPriceNoDenom', + message: 'gasPriceInfoModalContent', + }), + ], + mobileModalStyle: { + width: '95%', + top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh', + }, + laptopModalStyle: { + width: '449px', + top: 'calc(33% + 45px)', + }, + }, + + GAS_LIMIT_INFO_MODAL: { + contents: [ + h(NotifcationModal, { + header: 'gasLimit', + message: 'gasLimitInfoModalContent', + }), + ], + mobileModalStyle: { + width: '95%', + top: getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP ? '52vh' : '36.5vh', + }, + laptopModalStyle: { + width: '449px', + top: 'calc(33% + 45px)', + }, + }, + CONFIRM_RESET_ACCOUNT: { contents: h(ConfirmResetAccount), mobileModalStyle: { @@ -295,7 +335,7 @@ const MODALS = { CUSTOMIZE_GAS: { contents: [ - h(CustomizeGasModal), + h(ConfirmCustomizeGasModal), ], mobileModalStyle: { width: '100vw', @@ -307,35 +347,20 @@ const MODALS = { margin: '0 auto', }, laptopModalStyle: { - width: '720px', - height: '377px', + width: 'auto', + height: '0px', top: '80px', + left: '0px', transform: 'none', - left: '0', - right: '0', margin: '0 auto', + position: 'relative', }, - }, - - CONFIRM_CUSTOMIZE_GAS: { - contents: h(ConfirmCustomizeGasModal), - mobileModalStyle: { - width: '100vw', - height: '100vh', - top: '0', - transform: 'none', - left: '0', - right: '0', - margin: '0 auto', + contentStyle: { + borderRadius: '8px', }, - laptopModalStyle: { - width: '720px', - height: '377px', - top: '80px', - transform: 'none', - left: '0', - right: '0', - margin: '0 auto', + customOnHideOpts: { + action: resetCustomGasData, + args: [], }, }, @@ -412,8 +437,11 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - hideModal: () => { + hideModal: (customOnHideOpts) => { dispatch(actions.hideModal()) + if (customOnHideOpts && customOnHideOpts.action) { + dispatch(customOnHideOpts.action(...customOnHideOpts.args)) + } }, hideWarning: () => { dispatch(actions.hideWarning()) @@ -445,7 +473,7 @@ Modal.prototype.render = function () { if (modal.onHide) { modal.onHide(this.props) } - this.onHide() + this.onHide(modal.customOnHideOpts) }, ref: (ref) => { this.modalRef = ref @@ -467,11 +495,11 @@ Modal.prototype.componentWillReceiveProps = function (nextProps) { } } -Modal.prototype.onHide = function () { +Modal.prototype.onHide = function (customOnHideOpts) { if (this.props.onHideCallback) { this.props.onHideCallback() } - this.props.hideModal() + this.props.hideModal(customOnHideOpts) } Modal.prototype.hide = function () { diff --git a/ui/app/components/modals/new-account-modal.js b/ui/app/components/app/modals/new-account-modal.js index a66a3ed4a..27c81a701 100644 --- a/ui/app/components/modals/new-account-modal.js +++ b/ui/app/components/app/modals/new-account-modal.js @@ -2,7 +2,7 @@ const { Component } = require('react') const PropTypes = require('prop-types') const h = require('react-hyperscript') const connect = require('react-redux').connect -const actions = require('../../actions') +const actions = require('../../../store/actions') class NewAccountModal extends Component { constructor (props) { diff --git a/ui/app/components/modals/notification-modal.js b/ui/app/components/app/modals/notification-modal.js index 46a4c8a21..2d73b2cfa 100644 --- a/ui/app/components/modals/notification-modal.js +++ b/ui/app/components/app/modals/notification-modal.js @@ -2,7 +2,7 @@ const { Component } = require('react') const PropTypes = require('prop-types') const h = require('react-hyperscript') const connect = require('react-redux').connect -const actions = require('../../actions') +const actions = require('../../../store/actions') class NotificationModal extends Component { render () { diff --git a/ui/app/components/modals/qr-scanner/index.js b/ui/app/components/app/modals/qr-scanner/index.js index 470dee1f4..470dee1f4 100644 --- a/ui/app/components/modals/qr-scanner/index.js +++ b/ui/app/components/app/modals/qr-scanner/index.js diff --git a/ui/app/components/modals/qr-scanner/index.scss b/ui/app/components/app/modals/qr-scanner/index.scss index 6fa81d51f..6fa81d51f 100644 --- a/ui/app/components/modals/qr-scanner/index.scss +++ b/ui/app/components/app/modals/qr-scanner/index.scss diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.component.js b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js index cb8d07d83..20915b5f9 100644 --- a/ui/app/components/modals/qr-scanner/qr-scanner.component.js +++ b/ui/app/components/app/modals/qr-scanner/qr-scanner.component.js @@ -2,9 +2,9 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { BrowserQRCodeReader } from '@zxing/library' import adapter from 'webrtc-adapter' // eslint-disable-line import/no-nodejs-modules, no-unused-vars -import Spinner from '../../spinner' -import WebcamUtils from '../../../../lib/webcam-utils' -import PageContainerFooter from '../../page-container/page-container-footer/page-container-footer.component' +import Spinner from '../../../ui/spinner' +import WebcamUtils from '../../../../../lib/webcam-utils' +import PageContainerFooter from '../../../ui/page-container/page-container-footer/page-container-footer.component' export default class QrScanner extends Component { static propTypes = { diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.container.js b/ui/app/components/app/modals/qr-scanner/qr-scanner.container.js index d0a35e03b..2210fbed2 100644 --- a/ui/app/components/modals/qr-scanner/qr-scanner.container.js +++ b/ui/app/components/app/modals/qr-scanner/qr-scanner.container.js @@ -1,10 +1,10 @@ import { connect } from 'react-redux' import QrScanner from './qr-scanner.component' -const { hideModal, qrCodeDetected, showQrScanner } = require('../../../actions') +const { hideModal, qrCodeDetected, showQrScanner } = require('../../../../store/actions') import { SEND_ROUTE, -} from '../../../routes' +} from '../../../../helpers/constants/routes' const mapStateToProps = state => { return { diff --git a/ui/app/components/modals/reject-transactions/index.js b/ui/app/components/app/modals/reject-transactions/index.js index fcdc372b6..fcdc372b6 100644 --- a/ui/app/components/modals/reject-transactions/index.js +++ b/ui/app/components/app/modals/reject-transactions/index.js diff --git a/ui/app/components/modals/reject-transactions/index.scss b/ui/app/components/app/modals/reject-transactions/index.scss index 753466883..753466883 100644 --- a/ui/app/components/modals/reject-transactions/index.scss +++ b/ui/app/components/app/modals/reject-transactions/index.scss diff --git a/ui/app/components/modals/reject-transactions/reject-transactions.component.js b/ui/app/components/app/modals/reject-transactions/reject-transactions.component.js index 60b259bdc..60b259bdc 100644 --- a/ui/app/components/modals/reject-transactions/reject-transactions.component.js +++ b/ui/app/components/app/modals/reject-transactions/reject-transactions.component.js diff --git a/ui/app/components/modals/reject-transactions/reject-transactions.container.js b/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js index 81e98d3ff..d2af05573 100644 --- a/ui/app/components/modals/reject-transactions/reject-transactions.container.js +++ b/ui/app/components/app/modals/reject-transactions/reject-transactions.container.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux' import { compose } from 'recompose' import RejectTransactionsModal from './reject-transactions.component' -import withModalProps from '../../../higher-order-components/with-modal-props' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' const mapStateToProps = (state, ownProps) => { const { unapprovedTxCount } = ownProps diff --git a/ui/app/components/modals/shapeshift-deposit-tx-modal.js b/ui/app/components/app/modals/shapeshift-deposit-tx-modal.js index 242c7b89d..ada9430f7 100644 --- a/ui/app/components/modals/shapeshift-deposit-tx-modal.js +++ b/ui/app/components/app/modals/shapeshift-deposit-tx-modal.js @@ -2,8 +2,8 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const actions = require('../../actions') -const QrView = require('../qr-code') +const actions = require('../../../store/actions') +const QrView = require('../../ui/qr-code') const AccountModalContainer = require('./account-modal-container') function mapStateToProps (state) { diff --git a/ui/app/components/modals/transaction-confirmed/index.js b/ui/app/components/app/modals/transaction-confirmed/index.js index 7776b969e..7776b969e 100644 --- a/ui/app/components/modals/transaction-confirmed/index.js +++ b/ui/app/components/app/modals/transaction-confirmed/index.js diff --git a/ui/app/components/modals/transaction-confirmed/index.scss b/ui/app/components/app/modals/transaction-confirmed/index.scss index c97371fb6..c97371fb6 100644 --- a/ui/app/components/modals/transaction-confirmed/index.scss +++ b/ui/app/components/app/modals/transaction-confirmed/index.scss diff --git a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.component.js index 0a98eb1a1..0a98eb1a1 100644 --- a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.component.js +++ b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.component.js diff --git a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.container.js index d4e39681a..9089ec158 100644 --- a/ui/app/components/modals/transaction-confirmed/transaction-confirmed.container.js +++ b/ui/app/components/app/modals/transaction-confirmed/transaction-confirmed.container.js @@ -1,4 +1,4 @@ import TransactionConfirmed from './transaction-confirmed.component' -import withModalProps from '../../../higher-order-components/with-modal-props' +import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' export default withModalProps(TransactionConfirmed) diff --git a/ui/app/components/network-display/index.js b/ui/app/components/app/network-display/index.js index f6878ae5b..f6878ae5b 100644 --- a/ui/app/components/network-display/index.js +++ b/ui/app/components/app/network-display/index.js diff --git a/ui/app/components/network-display/index.scss b/ui/app/components/app/network-display/index.scss index e9f2f2057..e9f2f2057 100644 --- a/ui/app/components/network-display/index.scss +++ b/ui/app/components/app/network-display/index.scss diff --git a/ui/app/components/network-display/network-display.component.js b/ui/app/components/app/network-display/network-display.component.js index 22d617099..1142e8606 100644 --- a/ui/app/components/network-display/network-display.component.js +++ b/ui/app/components/app/network-display/network-display.component.js @@ -6,7 +6,7 @@ import { ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE, -} from '../../../../app/scripts/controllers/network/enums' +} from '../../../../../app/scripts/controllers/network/enums' const networkToClassHash = { [MAINNET_CODE]: 'mainnet', diff --git a/ui/app/components/network-display/network-display.container.js b/ui/app/components/app/network-display/network-display.container.js index 99a14fff4..99a14fff4 100644 --- a/ui/app/components/network-display/network-display.container.js +++ b/ui/app/components/app/network-display/network-display.container.js diff --git a/ui/app/components/network.js b/ui/app/components/app/network.js index 611aadb7b..e18404f42 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/app/network.js @@ -23,33 +23,19 @@ Network.prototype.render = function () { const props = this.props const context = this.context const networkNumber = props.network - let providerName, providerNick + let providerName, providerNick, providerUrl try { providerName = props.provider.type providerNick = props.provider.nickname || '' + providerUrl = props.provider.rpcTarget } catch (e) { providerName = null } - let iconName, hoverText + const providerId = providerNick || providerName || providerUrl || null + let iconName + let hoverText - if (networkNumber === 'loading') { - return h('span.pointer.network-indicator', { - style: { - display: 'flex', - alignItems: 'center', - flexDirection: 'row', - }, - onClick: (event) => this.props.onClick(event), - }, [ - h('img', { - title: context.t('attemptingConnect'), - style: { - width: '27px', - }, - src: 'images/loading.svg', - }), - ]) - } else if (providerName === 'mainnet') { + if (providerName === 'mainnet') { hoverText = context.t('mainnet') iconName = 'ethereum-network' } else if (providerName === 'ropsten') { @@ -65,8 +51,8 @@ Network.prototype.render = function () { hoverText = context.t('rinkeby') iconName = 'rinkeby-test-network' } else { - hoverText = context.t('unknownNetwork') - iconName = 'unknown-private-network' + hoverText = providerId + iconName = 'private-network' } return ( @@ -92,6 +78,7 @@ Network.prototype.render = function () { h(NetworkDropdownIcon, { backgroundColor: '#038789', // $blue-lagoon nonSelectBackgroundColor: '#15afb2', + loading: networkNumber === 'loading', }), h('.network-name', context.t('mainnet')), h('i.fa.fa-chevron-down.fa-lg.network-caret'), @@ -101,6 +88,7 @@ Network.prototype.render = function () { h(NetworkDropdownIcon, { backgroundColor: '#e91550', // $crimson nonSelectBackgroundColor: '#ec2c50', + loading: networkNumber === 'loading', }), h('.network-name', context.t('ropsten')), h('i.fa.fa-chevron-down.fa-lg.network-caret'), @@ -110,6 +98,7 @@ Network.prototype.render = function () { h(NetworkDropdownIcon, { backgroundColor: '#690496', // $purple nonSelectBackgroundColor: '#b039f3', + loading: networkNumber === 'loading', }), h('.network-name', context.t('kovan')), h('i.fa.fa-chevron-down.fa-lg.network-caret'), @@ -119,13 +108,31 @@ Network.prototype.render = function () { h(NetworkDropdownIcon, { backgroundColor: '#ebb33f', // $tulip-tree nonSelectBackgroundColor: '#ecb23e', + loading: networkNumber === 'loading', }), h('.network-name', context.t('rinkeby')), h('i.fa.fa-chevron-down.fa-lg.network-caret'), ]) default: return h('.network-indicator', [ - h('i.fa.fa-question-circle.fa-lg', { + networkNumber === 'loading' + ? h('span.pointer.network-indicator', { + style: { + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + }, + onClick: (event) => this.props.onClick(event), + }, [ + h('img', { + title: context.t('attemptingConnect'), + style: { + width: '27px', + }, + src: 'images/loading.svg', + }), + ]) + : h('i.fa.fa-question-circle.fa-lg', { style: { margin: '10px', color: 'rgb(125, 128, 130)', diff --git a/ui/app/components/notice.js b/ui/app/components/app/notice.js index bb7e0814c..bb7e0814c 100644 --- a/ui/app/components/notice.js +++ b/ui/app/components/app/notice.js diff --git a/ui/app/components/provider-page-container/index.js b/ui/app/components/app/provider-page-container/index.js index 927c35940..927c35940 100644 --- a/ui/app/components/provider-page-container/index.js +++ b/ui/app/components/app/provider-page-container/index.js diff --git a/ui/app/components/provider-page-container/index.scss b/ui/app/components/app/provider-page-container/index.scss index 8d35ac179..8d35ac179 100644 --- a/ui/app/components/provider-page-container/index.scss +++ b/ui/app/components/app/provider-page-container/index.scss diff --git a/ui/app/components/provider-page-container/provider-page-container-content/index.js b/ui/app/components/app/provider-page-container/provider-page-container-content/index.js index 73e491adc..73e491adc 100644 --- a/ui/app/components/provider-page-container/provider-page-container-content/index.js +++ b/ui/app/components/app/provider-page-container/provider-page-container-content/index.js diff --git a/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.component.js b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js index 268db613f..0eb1d616a 100644 --- a/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.component.js +++ b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.component.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types' import React, {PureComponent} from 'react' -import Identicon from '../../identicon' +import Identicon from '../../../ui/identicon' export default class ProviderPageContainerContent extends PureComponent { static propTypes = { diff --git a/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.container.js b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js index 3ea1ce20e..4dbdddd16 100644 --- a/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.container.js +++ b/ui/app/components/app/provider-page-container/provider-page-container-content/provider-page-container-content.container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux' import ProviderPageContainerContent from './provider-page-container-content.component' -import { getSelectedIdentity } from '../../../selectors' +import { getSelectedIdentity } from '../../../../selectors/selectors' const mapStateToProps = (state) => { return { diff --git a/ui/app/components/provider-page-container/provider-page-container-header/index.js b/ui/app/components/app/provider-page-container/provider-page-container-header/index.js index 430627d3a..430627d3a 100644 --- a/ui/app/components/provider-page-container/provider-page-container-header/index.js +++ b/ui/app/components/app/provider-page-container/provider-page-container-header/index.js diff --git a/ui/app/components/provider-page-container/provider-page-container-header/provider-page-container-header.component.js b/ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js index 41bf6c3dd..41bf6c3dd 100644 --- a/ui/app/components/provider-page-container/provider-page-container-header/provider-page-container-header.component.js +++ b/ui/app/components/app/provider-page-container/provider-page-container-header/provider-page-container-header.component.js diff --git a/ui/app/components/provider-page-container/provider-page-container.component.js b/ui/app/components/app/provider-page-container/provider-page-container.component.js index 902733616..910def2a3 100644 --- a/ui/app/components/provider-page-container/provider-page-container.component.js +++ b/ui/app/components/app/provider-page-container/provider-page-container.component.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types' import React, {PureComponent} from 'react' -import { ProviderPageContainerContent, ProviderPageContainerHeader } from './' -import { PageContainerFooter } from '../page-container' +import { ProviderPageContainerContent, ProviderPageContainerHeader } from '.' +import { PageContainerFooter } from '../../ui/page-container' export default class ProviderPageContainer extends PureComponent { static propTypes = { @@ -10,20 +10,46 @@ export default class ProviderPageContainer extends PureComponent { rejectProviderRequest: PropTypes.func.isRequired, siteImage: PropTypes.string, siteTitle: PropTypes.string.isRequired, + tabID: PropTypes.string.isRequired, }; static contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, }; + componentDidMount () { + this.context.metricsEvent({ + eventOpts: { + category: 'Auth', + action: 'Connect', + name: 'Popup Opened', + }, + }) + } + onCancel = () => { - const { origin, rejectProviderRequest } = this.props - rejectProviderRequest(origin) + const { tabID, rejectProviderRequest } = this.props + this.context.metricsEvent({ + eventOpts: { + category: 'Auth', + action: 'Connect', + name: 'Canceled', + }, + }) + rejectProviderRequest(tabID) } onSubmit = () => { - const { approveProviderRequest, origin } = this.props - approveProviderRequest(origin) + const { approveProviderRequest, tabID } = this.props + this.context.metricsEvent({ + eventOpts: { + category: 'Auth', + action: 'Connect', + name: 'Confirmed', + }, + }) + approveProviderRequest(tabID) } render () { diff --git a/ui/app/components/selected-account/index.js b/ui/app/components/app/selected-account/index.js index eb342181f..eb342181f 100644 --- a/ui/app/components/selected-account/index.js +++ b/ui/app/components/app/selected-account/index.js diff --git a/ui/app/components/selected-account/index.scss b/ui/app/components/app/selected-account/index.scss index 5339a228b..5339a228b 100644 --- a/ui/app/components/selected-account/index.scss +++ b/ui/app/components/app/selected-account/index.scss diff --git a/ui/app/components/selected-account/selected-account.component.js b/ui/app/components/app/selected-account/selected-account.component.js index 4f98df9b6..5a3fa815f 100644 --- a/ui/app/components/selected-account/selected-account.component.js +++ b/ui/app/components/app/selected-account/selected-account.component.js @@ -1,9 +1,9 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import copyToClipboard from 'copy-to-clipboard' -import { addressSlicer, checksumAddress } from '../../util' +import { addressSlicer, checksumAddress } from '../../../helpers/utils/util' -const Tooltip = require('../tooltip-v2.js').default +const Tooltip = require('../../ui/tooltip-v2.js').default class SelectedAccount extends Component { state = { @@ -17,12 +17,13 @@ class SelectedAccount extends Component { static propTypes = { selectedAddress: PropTypes.string, selectedIdentity: PropTypes.object, + network: PropTypes.string, } render () { const { t } = this.context - const { selectedAddress, selectedIdentity } = this.props - const checksummedAddress = checksumAddress(selectedAddress) + const { selectedAddress, selectedIdentity, network } = this.props + const checksummedAddress = checksumAddress(selectedAddress, network) return ( <div className="selected-account"> diff --git a/ui/app/components/selected-account/selected-account.container.js b/ui/app/components/app/selected-account/selected-account.container.js index f9e061d15..b5dbe74f3 100644 --- a/ui/app/components/selected-account/selected-account.container.js +++ b/ui/app/components/app/selected-account/selected-account.container.js @@ -1,12 +1,13 @@ import { connect } from 'react-redux' import SelectedAccount from './selected-account.component' -const selectors = require('../../selectors') +const selectors = require('../../../selectors/selectors') const mapStateToProps = state => { return { selectedAddress: selectors.getSelectedAddress(state), selectedIdentity: selectors.getSelectedIdentity(state), + network: state.metamask.network, } } diff --git a/ui/app/components/selected-account/tests/selected-account-component.test.js b/ui/app/components/app/selected-account/tests/selected-account-component.test.js index 78a94d1c8..78a94d1c8 100644 --- a/ui/app/components/selected-account/tests/selected-account-component.test.js +++ b/ui/app/components/app/selected-account/tests/selected-account-component.test.js diff --git a/ui/app/components/send/README.md b/ui/app/components/app/send/README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/README.md +++ b/ui/app/components/app/send/README.md diff --git a/ui/app/components/send/account-list-item/account-list-item-README.md b/ui/app/components/app/send/account-list-item/account-list-item-README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/account-list-item/account-list-item-README.md +++ b/ui/app/components/app/send/account-list-item/account-list-item-README.md diff --git a/ui/app/components/app/send/account-list-item/account-list-item.component.js b/ui/app/components/app/send/account-list-item/account-list-item.component.js new file mode 100644 index 000000000..18e77b4f9 --- /dev/null +++ b/ui/app/components/app/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 '../../../ui/identicon' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common' +import Tooltip from '../../../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 (<div + className={`account-list-item ${className}`} + onClick={() => handleClick && handleClick({ name, address, balance })} + > + + <div className="account-list-item__top-row"> + <Identicon + address={address} + className="account-list-item__identicon" + diameter={18} + /> + + <div className="account-list-item__account-name">{ name || address }</div> + + {icon && <div className="account-list-item__icon">{ icon }</div>} + + </div> + + {displayAddress && name && <div className="account-list-item__account-address"> + { checksumAddress(address) } + </div>} + + { + displayBalance && ( + <Tooltip + position="left" + title={this.context.t('balanceOutdated')} + disabled={!balanceIsCached} + style={{ + left: '-20px !important', + }} + > + <div className={classnames('account-list-item__account-balances', { + 'account-list-item__cached-balances': balanceIsCached, + })}> + <div className="account-list-item__primary-cached-container"> + <UserPreferencedCurrencyDisplay + type={PRIMARY} + value={balance} + hideTitle={true} + /> + { + balanceIsCached ? <span className="account-list-item__cached-star">*</span> : null + } + </div> + { + showFiat && ( + <UserPreferencedCurrencyDisplay + type={SECONDARY} + value={balance} + hideTitle={true} + /> + ) + } + </div> + </Tooltip> + ) + } + + </div>) + } +} diff --git a/ui/app/components/send/account-list-item/account-list-item.container.js b/ui/app/components/app/send/account-list-item/account-list-item.container.js index f8e73d923..bc9a60f49 100644 --- a/ui/app/components/send/account-list-item/account-list-item.container.js +++ b/ui/app/components/app/send/account-list-item/account-list-item.container.js @@ -4,14 +4,24 @@ import { 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/components/send/account-list-item/index.js b/ui/app/components/app/send/account-list-item/index.js index 907485cf7..907485cf7 100644 --- a/ui/app/components/send/account-list-item/index.js +++ b/ui/app/components/app/send/account-list-item/index.js diff --git a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js b/ui/app/components/app/send/account-list-item/tests/account-list-item-component.test.js index 6ffc0b1c6..5df9f77d6 100644 --- a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js +++ b/ui/app/components/app/send/account-list-item/tests/account-list-item-component.test.js @@ -3,7 +3,7 @@ import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' import proxyquire from 'proxyquire' -import Identicon from '../../../identicon' +import Identicon from '../../../../ui/identicon' import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display' const utilsMethodStubs = { @@ -11,7 +11,7 @@ const utilsMethodStubs = { } const AccountListItem = proxyquire('../account-list-item.component.js', { - '../../../util': utilsMethodStubs, + '../../../../helpers/utils/util': utilsMethodStubs, }).default @@ -121,6 +121,20 @@ describe('AccountListItem Component', function () { { 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, } ) }) @@ -129,5 +143,6 @@ describe('AccountListItem Component', function () { wrapper.setProps({ displayBalance: false }) assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 0) }) + }) }) diff --git a/ui/app/components/app/send/account-list-item/tests/account-list-item-container.test.js b/ui/app/components/app/send/account-list-item/tests/account-list-item-container.test.js new file mode 100644 index 000000000..19a9a02d0 --- /dev/null +++ b/ui/app/components/app/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/components/send/index.js b/ui/app/components/app/send/index.js index b5114babc..b5114babc 100644 --- a/ui/app/components/send/index.js +++ b/ui/app/components/app/send/index.js diff --git a/ui/app/components/send/send-content/index.js b/ui/app/components/app/send/send-content/index.js index 891c17e6a..891c17e6a 100644 --- a/ui/app/components/send/send-content/index.js +++ b/ui/app/components/app/send/send-content/index.js diff --git a/ui/app/components/send/send-content/send-amount-row/README.md b/ui/app/components/app/send/send-content/send-amount-row/README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-content/send-amount-row/README.md +++ b/ui/app/components/app/send/send-content/send-amount-row/README.md diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js index ceb620941..f17137c1e 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js @@ -11,11 +11,11 @@ export default class AmountMaxButton extends Component { setAmountToMax: PropTypes.func, setMaxModeTo: PropTypes.func, tokenBalance: PropTypes.string, - }; + } static contextTypes = { t: PropTypes.func, - }; + } setMaxAmount () { const { @@ -35,7 +35,12 @@ export default class AmountMaxButton extends Component { } onMaxClick = (event) => { - const { setMaxModeTo } = this.props + 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) diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js index 2d2ec42f7..16c5a0db5 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.container.js @@ -10,11 +10,11 @@ import { calcMaxAmount } from './amount-max-button.utils.js' import { updateSendAmount, setMaxModeTo, -} from '../../../../../actions' +} from '../../../../../../store/actions' import AmountMaxButton from './amount-max-button.component' import { updateSendErrors, -} from '../../../../../ducks/send.duck' +} from '../../../../../../ducks/send/send.duck' export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton) diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js index 69fec1994..69fec1994 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors.js diff --git a/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js new file mode 100644 index 000000000..f4c8fad8a --- /dev/null +++ b/ui/app/components/app/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/components/send/send-content/send-amount-row/amount-max-button/index.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/index.js index ee8271494..ee8271494 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/index.js +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/index.js diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js index b04d3897f..b04d3897f 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js index 2cc00d6d6..f446e330c 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-container.test.js @@ -29,8 +29,8 @@ proxyquire('../amount-max-button.container.js', { }, './amount-max-button.selectors.js': { getMaxModeOn: (s) => `mockMaxModeOn:${s}` }, './amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 }, - '../../../../../actions': actionSpies, - '../../../../../ducks/send.duck': duckActionSpies, + '../../../../../../store/actions': actionSpies, + '../../../../../../ducks/send/send.duck': duckActionSpies, }) describe('amount-max-button container', () => { diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js index 655fe1969..655fe1969 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-selectors.test.js diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js index 816df6a12..1ee858f67 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js +++ b/ui/app/components/app/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-utils.test.js @@ -19,7 +19,7 @@ describe('amount-max-button utils', () => { selectedToken: { decimals: 10, }, - tokenBalance: 100, + tokenBalance: '64', }), 'e8d4a51000') }) }) diff --git a/ui/app/components/send/send-content/send-amount-row/index.js b/ui/app/components/app/send/send-content/send-amount-row/index.js index abc6852fe..abc6852fe 100644 --- a/ui/app/components/send/send-content/send-amount-row/index.js +++ b/ui/app/components/app/send/send-content/send-amount-row/index.js diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.component.js index 0268376bf..e725e7eda 100644 --- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js +++ b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import SendRowWrapper from '../send-row-wrapper/' -import AmountMaxButton from './amount-max-button/' +import SendRowWrapper from '../send-row-wrapper' +import AmountMaxButton from './amount-max-button' import UserPreferencedCurrencyInput from '../../../user-preferenced-currency-input' import UserPreferencedTokenInput from '../../../user-preferenced-token-input' @@ -26,11 +26,11 @@ export default class SendAmountRow extends Component { updateSendAmount: PropTypes.func, updateSendAmountError: PropTypes.func, updateGas: PropTypes.func, - }; + } static contextTypes = { t: PropTypes.func, - }; + } validateAmount (amount) { const { @@ -58,7 +58,6 @@ export default class SendAmountRow extends Component { if (selectedToken) { updateGasFeeError({ - amount, amountConversionRate, balance, conversionRate, diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.container.js index 3504d1b73..0646355ab 100644 --- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.container.js +++ b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.container.js @@ -17,10 +17,10 @@ import { getAmountErrorObject, getGasFeeErrorObject } from '../../send.utils' import { setMaxModeTo, updateSendAmount, -} from '../../../../actions' +} from '../../../../../store/actions' import { updateSendErrors, -} from '../../../../ducks/send.duck' +} from '../../../../../ducks/send/send.duck' import SendAmountRow from './send-amount-row.component' export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow) @@ -45,10 +45,10 @@ function mapDispatchToProps (dispatch) { setMaxModeTo: bool => dispatch(setMaxModeTo(bool)), updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)), updateGasFeeError: (amountDataObject) => { - dispatch(updateSendErrors(getGasFeeErrorObject(amountDataObject))) + dispatch(updateSendErrors(getGasFeeErrorObject(amountDataObject))) }, updateSendAmountError: (amountDataObject) => { - dispatch(updateSendErrors(getAmountErrorObject(amountDataObject))) + dispatch(updateSendErrors(getAmountErrorObject(amountDataObject))) }, } } diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.scss b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.scss +++ b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.scss diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.selectors.js index fb08c7ed7..fb08c7ed7 100644 --- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.selectors.js +++ b/ui/app/components/app/send/send-content/send-amount-row/send-amount-row.selectors.js diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-component.test.js index 56e80cb83..14a71129f 100644 --- a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js +++ b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-component.test.js @@ -82,7 +82,6 @@ describe('SendAmountRow Component', function () { assert.deepEqual( propsMethodSpies.updateGasFeeError.getCall(0).args, [{ - amount: 'someAmount', amountConversionRate: 'mockAmountConversionRate', balance: 'mockBalance', conversionRate: 7, diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-container.test.js index 52e351aee..6d20202b0 100644 --- a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-container.test.js +++ b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-container.test.js @@ -37,8 +37,8 @@ proxyquire('../send-amount-row.container.js', { getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }), getGasFeeErrorObject: (mockDataObject) => ({ ...mockDataObject, mockGasFeeErrorChange: true }), }, - '../../../../actions': actionSpies, - '../../../../ducks/send.duck': duckActionSpies, + '../../../../../store/actions': actionSpies, + '../../../../../ducks/send/send.duck': duckActionSpies, }) describe('send-amount-row container', () => { diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js index 4672cb8a7..4672cb8a7 100644 --- a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js +++ b/ui/app/components/app/send/send-content/send-amount-row/tests/send-amount-row-selectors.test.js diff --git a/ui/app/components/send/send-content/send-content.component.js b/ui/app/components/app/send/send-content/send-content.component.js index 1b03ffd2b..2c09ceb19 100644 --- a/ui/app/components/send/send-content/send-content.component.js +++ b/ui/app/components/app/send/send-content/send-content.component.js @@ -1,11 +1,11 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import PageContainerContent from '../../page-container/page-container-content.component' -import SendAmountRow from './send-amount-row/' -import SendFromRow from './send-from-row/' -import SendGasRow from './send-gas-row/' +import PageContainerContent from '../../../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 SendToRow from './send-to-row' export default class SendContent extends Component { @@ -13,7 +13,7 @@ export default class SendContent extends Component { updateGas: PropTypes.func, scanQrCode: PropTypes.func, showHexData: PropTypes.bool, - }; + } updateGas = (updateData) => this.props.updateGas(updateData) diff --git a/ui/app/components/send/send-content/send-dropdown-list/index.js b/ui/app/components/app/send/send-content/send-dropdown-list/index.js index 04af6536c..04af6536c 100644 --- a/ui/app/components/send/send-content/send-dropdown-list/index.js +++ b/ui/app/components/app/send/send-content/send-dropdown-list/index.js diff --git a/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js b/ui/app/components/app/send/send-content/send-dropdown-list/send-dropdown-list.component.js index bedac1259..0d026bc69 100644 --- a/ui/app/components/send/send-content/send-dropdown-list/send-dropdown-list.component.js +++ b/ui/app/components/app/send/send-content/send-dropdown-list/send-dropdown-list.component.js @@ -1,6 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import AccountListItem from '../../account-list-item/' +import AccountListItem from '../../account-list-item' export default class SendDropdownList extends Component { diff --git a/ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js b/ui/app/components/app/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js index b92dd4dfe..b92dd4dfe 100644 --- a/ui/app/components/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js +++ b/ui/app/components/app/send/send-content/send-dropdown-list/tests/send-dropdown-list-component.test.js diff --git a/ui/app/components/send/send-content/send-from-row/index.js b/ui/app/components/app/send/send-content/send-from-row/index.js index 0a79726b2..0a79726b2 100644 --- a/ui/app/components/send/send-content/send-from-row/index.js +++ b/ui/app/components/app/send/send-content/send-from-row/index.js diff --git a/ui/app/components/app/send/send-content/send-from-row/send-from-row.component.js b/ui/app/components/app/send/send-content/send-from-row/send-from-row.component.js new file mode 100644 index 000000000..dfa53e970 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-from-row/send-from-row.component.js @@ -0,0 +1,27 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper' +import AccountListItem from '../../account-list-item' + +export default class SendFromRow extends Component { + static propTypes = { + from: PropTypes.object, + } + + static contextTypes = { + t: PropTypes.func, + } + + render () { + const { t } = this.context + const { from } = this.props + + return ( + <SendRowWrapper label={`${t('from')}:`}> + <div className="send-v2__from-dropdown"> + <AccountListItem account={from} /> + </div> + </SendRowWrapper> + ) + } +} diff --git a/ui/app/components/app/send/send-content/send-from-row/send-from-row.container.js b/ui/app/components/app/send/send-content/send-from-row/send-from-row.container.js new file mode 100644 index 000000000..fe3ac9aa1 --- /dev/null +++ b/ui/app/components/app/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/components/send/send-content/send-from-row/send-from-row.selectors.js b/ui/app/components/app/send/send-content/send-from-row/send-from-row.selectors.js index 03ef4806b..03ef4806b 100644 --- a/ui/app/components/send/send-content/send-from-row/send-from-row.selectors.js +++ b/ui/app/components/app/send/send-content/send-from-row/send-from-row.selectors.js diff --git a/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/components/app/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/components/app/send/send-content/send-from-row/tests/send-from-row-component.test.js @@ -0,0 +1,31 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import SendFromRow from '../send-from-row.component.js' +import AccountListItem from '../../../account-list-item' +import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' + +describe('SendFromRow Component', function () { + describe('render', () => { + const wrapper = shallow( + <SendFromRow + from={ { address: 'mockAddress' } } + />, + { context: { t: str => str + '_t' } } + ) + + it('should render a SendRowWrapper component', () => { + assert.equal(wrapper.find(SendRowWrapper).length, 1) + }) + + it('should pass the correct props to SendRowWrapper', () => { + const { label } = wrapper.find(SendRowWrapper).props() + assert.equal(label, 'from_t:') + }) + + it('should render the FromDropdown with the correct props', () => { + const { account } = wrapper.find(AccountListItem).props() + assert.deepEqual(account, { address: 'mockAddress' }) + }) + }) +}) diff --git a/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/components/app/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/components/app/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/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js b/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-selectors.test.js index ecb57bbc3..ecb57bbc3 100644 --- a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-selectors.test.js +++ b/ui/app/components/app/send/send-content/send-from-row/tests/send-from-row-selectors.test.js diff --git a/ui/app/components/send/send-content/send-gas-row/README.md b/ui/app/components/app/send/send-content/send-gas-row/README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-content/send-gas-row/README.md +++ b/ui/app/components/app/send/send-content/send-gas-row/README.md diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js index 9bbb67506..48088607a 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js +++ b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js @@ -1,7 +1,7 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display' -import { PRIMARY, SECONDARY } from '../../../../../constants/common' +import { PRIMARY, SECONDARY } from '../../../../../../helpers/constants/common' export default class GasFeeDisplay extends Component { @@ -11,7 +11,7 @@ export default class GasFeeDisplay extends Component { convertedCurrency: PropTypes.string, gasLoadingError: PropTypes.bool, gasTotal: PropTypes.string, - onClick: PropTypes.func, + onReset: PropTypes.func, }; static contextTypes = { @@ -19,7 +19,7 @@ export default class GasFeeDisplay extends Component { }; render () { - const { gasTotal, onClick, gasLoadingError } = this.props + const { gasTotal, gasLoadingError, onReset } = this.props return ( <div className="send-v2__gas-fee-display"> @@ -46,11 +46,10 @@ export default class GasFeeDisplay extends Component { </div> } <button - className="sliders-icon-container" - onClick={onClick} - disabled={!gasTotal && !gasLoadingError} + className="gas-fee-reset" + onClick={onReset} > - <i className="fa fa-sliders sliders-icon" /> + { this.context.t('reset') } </button> </div> ) diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/index.js index dba0edb7b..dba0edb7b 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/index.js +++ b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/index.js diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js index 9ff01493a..cb4180508 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js +++ b/ui/app/components/app/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js @@ -8,18 +8,20 @@ import sinon from 'sinon' const propsMethodSpies = { showCustomizeGasModal: sinon.spy(), + onReset: sinon.spy(), } -describe('SendGasRow Component', function () { +describe('GasFeeDisplay Component', function () { let wrapper beforeEach(() => { wrapper = shallow(<GasFeeDisplay conversionRate={20} gasTotal={'mockGasTotal'} - onClick={propsMethodSpies.showCustomizeGasModal} primaryCurrency={'mockPrimaryCurrency'} convertedCurrency={'mockConvertedCurrency'} + showGasButtonGroup={propsMethodSpies.showCustomizeGasModal} + onReset={propsMethodSpies.onReset} />, {context: {t: str => str + '_t'}}) }) @@ -41,13 +43,19 @@ describe('SendGasRow Component', function () { assert.equal(value, 'mockGasTotal') }) - it('should render the Button with the correct props', () => { + it('should render the reset button with the correct props', () => { const { onClick, + className, } = wrapper.find('button').props() - assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) + assert.equal(className, 'gas-fee-reset') + assert.equal(propsMethodSpies.onReset.callCount, 0) onClick() - assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) + 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/components/send/send-content/send-gas-row/index.js b/ui/app/components/app/send/send-content/send-gas-row/index.js index 3c7ff1d5f..3c7ff1d5f 100644 --- a/ui/app/components/send/send-content/send-gas-row/index.js +++ b/ui/app/components/app/send/send-content/send-gas-row/index.js diff --git a/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.component.js new file mode 100644 index 000000000..424a65b20 --- /dev/null +++ b/ui/app/components/app/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 '../../../gas-customization/gas-price-button-group' +import AdvancedGasInputs from '../../../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.number, + gasLimit: PropTypes.number, + insufficientBalance: PropTypes.bool, + } + + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + }; + + renderAdvancedOptionsButton () { + const { metricsEvent } = this.context + const { showCustomizeGasModal } = this.props + return <div className="advanced-gas-options-btn" onClick={() => { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Clicked "Advanced Options"', + }, + }) + showCustomizeGasModal() + }}> + { this.context.t('advancedOptions') } + </div> + } + + renderContent () { + const { + conversionRate, + convertedCurrency, + gasLoadingError, + gasTotal, + showCustomizeGasModal, + gasPriceButtonGroupProps, + gasButtonGroupShown, + advancedInlineGasShown, + resetGasButtons, + setGasPrice, + setGasLimit, + gasPrice, + gasLimit, + insufficientBalance, + } = this.props + const { metricsEvent } = this.context + + const gasPriceButtonGroup = <div> + <GasPriceButtonGroup + className="gas-price-button-group--small" + showCheck={false} + {...gasPriceButtonGroupProps} + handleGasPriceSelection={(...args) => { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Changed Gas Button', + }, + }) + gasPriceButtonGroupProps.handleGasPriceSelection(...args) + }} + /> + { this.renderAdvancedOptionsButton() } + </div> + const gasFeeDisplay = <GasFeeDisplay + conversionRate={conversionRate} + convertedCurrency={convertedCurrency} + gasLoadingError={gasLoadingError} + gasTotal={gasTotal} + onReset={resetGasButtons} + onClick={() => showCustomizeGasModal()} + /> + const advancedGasInputs = <div> + <AdvancedGasInputs + updateCustomGasPrice={newGasPrice => setGasPrice(newGasPrice, gasLimit)} + updateCustomGasLimit={newGasLimit => setGasLimit(newGasLimit, gasPrice)} + customGasPrice={gasPrice} + customGasLimit={gasLimit} + insufficientBalance={insufficientBalance} + customPriceIsSafe={true} + isSpeedUp={false} + /> + { this.renderAdvancedOptionsButton() } + </div> + + if (advancedInlineGasShown) { + return advancedGasInputs + } else if (gasButtonGroupShown) { + return gasPriceButtonGroup + } else { + return gasFeeDisplay + } + } + + render () { + const { gasFeeError } = this.props + + return ( + <SendRowWrapper + label={`${this.context.t('transactionFee')}:`} + showError={gasFeeError} + errorType={'gasFee'} + > + { this.renderContent() } + </SendRowWrapper> + ) + } + +} diff --git a/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.container.js new file mode 100644 index 000000000..f81670c02 --- /dev/null +++ b/ui/app/components/app/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/components/send/send-content/send-gas-row/send-gas-row.scss b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.scss +++ b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.scss diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.selectors.js index 96f6293c2..79c838543 100644 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.selectors.js +++ b/ui/app/components/app/send/send-content/send-gas-row/send-gas-row.selectors.js @@ -1,6 +1,7 @@ const selectors = { gasFeeIsInError, getGasLoadingError, + getGasButtonGroupShown, } module.exports = selectors @@ -12,3 +13,7 @@ function getGasLoadingError (state) { function gasFeeIsInError (state) { return Boolean(state.send.errors.gasFee) } + +function getGasButtonGroupShown (state) { + return state.send.gasButtonGroupShown +} diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-component.test.js index 54a92bd2d..08f26854e 100644 --- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-component.test.js +++ b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-component.test.js @@ -6,9 +6,11 @@ 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 '../../../../gas-customization/gas-price-button-group' const propsMethodSpies = { showCustomizeGasModal: sinon.spy(), + resetGasButtons: sinon.spy(), } describe('SendGasRow Component', function () { @@ -21,12 +23,18 @@ describe('SendGasRow Component', function () { gasFeeError={'mockGasFeeError'} gasLoadingError={false} gasTotal={'mockGasTotal'} + gasButtonGroupShown={false} showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal} - />, { context: { t: str => str + '_t' } }) + resetGasButtons={propsMethodSpies.resetGasButtons} + gasPriceButtonGroupProps={{ + someGasPriceButtonGroupProp: 'foo', + anotherGasPriceButtonGroupProp: 'bar', + }} + />, { context: { t: str => str + '_t', metricsEvent: () => ({}) } }) }) afterEach(() => { - propsMethodSpies.showCustomizeGasModal.resetHistory() + propsMethodSpies.resetGasButtons.resetHistory() }) describe('render', () => { @@ -41,7 +49,7 @@ describe('SendGasRow Component', function () { errorType, } = wrapper.find(SendRowWrapper).props() - assert.equal(label, 'gasFee_t:') + assert.equal(label, 'transactionFee_t:') assert.equal(showError, 'mockGasFeeError') assert.equal(errorType, 'gasFee') }) @@ -56,14 +64,40 @@ describe('SendGasRow Component', function () { convertedCurrency, gasLoadingError, gasTotal, - onClick, + 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) - onClick() + advancedOptionsButton.props().onClick() assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) }) }) diff --git a/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-container.test.js new file mode 100644 index 000000000..d1f753639 --- /dev/null +++ b/ui/app/components/app/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/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js index d46dd9d8b..bd3c9a257 100644 --- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js +++ b/ui/app/components/app/send/send-content/send-gas-row/tests/send-gas-row-selectors.test.js @@ -2,6 +2,7 @@ import assert from 'assert' import { gasFeeIsInError, getGasLoadingError, + getGasButtonGroupShown, } from '../send-gas-row.selectors.js' describe('send-gas-row selectors', () => { @@ -46,4 +47,16 @@ describe('send-gas-row selectors', () => { }) }) + describe('getGasButtonGroupShown()', () => { + it('should return send.gasButtonGroupShown', () => { + const state = { + send: { + gasButtonGroupShown: 'foobar', + }, + } + + assert.equal(getGasButtonGroupShown(state), 'foobar') + }) + }) + }) diff --git a/ui/app/components/send/send-content/send-hex-data-row/index.js b/ui/app/components/app/send/send-content/send-hex-data-row/index.js index 08c341067..08c341067 100644 --- a/ui/app/components/send/send-content/send-hex-data-row/index.js +++ b/ui/app/components/app/send/send-content/send-hex-data-row/index.js diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.component.js index 62a74a77b..62a74a77b 100644 --- a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js +++ b/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.component.js diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js b/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.container.js index df554ca5f..76c929d08 100644 --- a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js +++ b/ui/app/components/app/send/send-content/send-hex-data-row/send-hex-data-row.container.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux' import { updateSendHexData, -} from '../../../../actions' +} from '../../../../../store/actions' import SendHexDataRow from './send-hex-data-row.component' export default connect(mapStateToProps, mapDispatchToProps)(SendHexDataRow) diff --git a/ui/app/components/send/send-content/send-row-wrapper/index.js b/ui/app/components/app/send/send-content/send-row-wrapper/index.js index d17545dcc..d17545dcc 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/index.js +++ b/ui/app/components/app/send/send-content/send-row-wrapper/index.js diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/index.js index c00617f83..c00617f83 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/index.js +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/index.js diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message-README.md diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js index 61bc7bab7..61bc7bab7 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js index 59622047f..59622047f 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.container.js diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.scss diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js index 2304a43d2..2304a43d2 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-component.test.js diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js index eecff165d..eecff165d 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-error-message/tests/send-row-error-message-container.test.js diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/index.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/index.js new file mode 100644 index 000000000..fd4d19ef7 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/index.js @@ -0,0 +1 @@ +export { default } from './send-row-warning-message.container' diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js new file mode 100644 index 000000000..f1caa8f99 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.component.js @@ -0,0 +1,27 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class SendRowWarningMessage extends Component { + + static propTypes = { + warnings: PropTypes.object, + warningType: PropTypes.string, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + render () { + const { warnings, warningType } = this.props + + const warningMessage = warningType in warnings && warnings[warningType] + + return ( + warningMessage + ? <div className="send-v2__warning">{this.context.t(warningMessage)}</div> + : null + ) + } + +} diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js new file mode 100644 index 000000000..7df14fd96 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import { getSendWarnings } from '../../../send.selectors' +import SendRowWarningMessage from './send-row-warning-message.component' + +export default connect(mapStateToProps)(SendRowWarningMessage) + +function mapStateToProps (state, ownProps) { + return { + warnings: getSendWarnings(state), + warningType: ownProps.warningType, + } +} diff --git a/ui/app/components/page-container/tests/page-container.component.test.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/page-container/tests/page-container.component.test.js +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/send-row-warning-message.scss diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js new file mode 100644 index 000000000..bd803d833 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-component.test.js @@ -0,0 +1,28 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import SendRowWarningMessage from '../send-row-warning-message.component.js' + +describe('SendRowWarningMessage Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow(<SendRowWarningMessage + warnings={{ warning1: 'abc', warning2: 'def' }} + warningType={'warning3'} + />, { context: { t: str => str + '_t' } }) + }) + + describe('render', () => { + it('should render null if the passed warnings do not contain a warning of warningType', () => { + assert.equal(wrapper.find('.send-v2__warning').length, 0) + assert.equal(wrapper.html(), null) + }) + + it('should render a warning message if the passed warnings contain a warning of warningType', () => { + wrapper.setProps({ warnings: { warning1: 'abc', warning2: 'def', warning3: 'xyz' } }) + assert.equal(wrapper.find('.send-v2__warning').length, 1) + assert.equal(wrapper.find('.send-v2__warning').text(), 'xyz_t') + }) + }) +}) diff --git a/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js new file mode 100644 index 000000000..225bf056c --- /dev/null +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-warning-message/tests/send-row-warning-message-container.test.js @@ -0,0 +1,28 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../send-row-warning-message.container.js', { + 'react-redux': { + connect: (ms, md) => { + mapStateToProps = ms + return () => ({}) + }, + }, + '../../../send.selectors': { getSendWarnings: (s) => `mockWarnings:${s}` }, +}) + +describe('send-row-warning-message container', () => { + + describe('mapStateToProps()', () => { + + it('should map the correct properties to props', () => { + assert.deepEqual(mapStateToProps('mockState', { warningType: 'someType' }), { + warnings: 'mockWarnings:mockState', + warningType: 'someType' }) + }) + + }) + +}) diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper-README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper-README.md +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper-README.md diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.component.js index b7528a15f..94309bd96 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.component.js +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.component.js @@ -1,6 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import SendRowErrorMessage from './send-row-error-message/' +import SendRowErrorMessage from './send-row-error-message' +import SendRowWarningMessage from './send-row-warning-message' export default class SendRowWrapper extends Component { @@ -9,6 +10,8 @@ export default class SendRowWrapper extends Component { errorType: PropTypes.string, label: PropTypes.string, showError: PropTypes.bool, + showWarning: PropTypes.bool, + warningType: PropTypes.string, }; static contextTypes = { @@ -21,20 +24,22 @@ export default class SendRowWrapper extends Component { errorType = '', label, showError = false, + showWarning = false, + warningType = '', } = this.props - const formField = Array.isArray(children) ? children[1] || children[0] : children const customLabelContent = children.length > 1 ? children[0] : null return ( <div className="send-v2__form-row"> <div className="send-v2__form-label"> - {label} - {showError && <SendRowErrorMessage errorType={errorType}/>} - {customLabelContent} + {label} + {showError && <SendRowErrorMessage errorType={errorType}/>} + {!showError && showWarning && <SendRowWarningMessage warningType={warningType} />} + {customLabelContent} </div> <div className="send-v2__form-field"> - {formField} + {formField} </div> </div> ) diff --git a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/send-row-wrapper.scss +++ b/ui/app/components/app/send/send-content/send-row-wrapper/send-row-wrapper.scss diff --git a/ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js b/ui/app/components/app/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js index 30280e1d0..30280e1d0 100644 --- a/ui/app/components/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js +++ b/ui/app/components/app/send/send-content/send-row-wrapper/tests/send-row-wrapper-component.test.js diff --git a/ui/app/components/send/send-content/send-to-row/index.js b/ui/app/components/app/send/send-content/send-to-row/index.js index 121f15148..121f15148 100644 --- a/ui/app/components/send/send-content/send-to-row/index.js +++ b/ui/app/components/app/send/send-content/send-to-row/index.js diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row-README.md b/ui/app/components/app/send/send-content/send-to-row/send-to-row-README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-content/send-to-row/send-to-row-README.md +++ b/ui/app/components/app/send/send-content/send-to-row/send-to-row-README.md diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js b/ui/app/components/app/send/send-content/send-to-row/send-to-row.component.js index 17c75c817..e8a55cb2a 100644 --- a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js +++ b/ui/app/components/app/send/send-content/send-to-row/send-to-row.component.js @@ -1,8 +1,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import SendRowWrapper from '../send-row-wrapper/' +import SendRowWrapper from '../send-row-wrapper' import EnsInput from '../../../ens-input' -import { getToErrorObject } from './send-to-row.utils.js' +import { getToErrorObject, getToWarningObject } from './send-to-row.utils.js' export default class SendToRow extends Component { @@ -10,26 +10,33 @@ export default class SendToRow extends Component { closeToDropdown: PropTypes.func, hasHexData: PropTypes.bool.isRequired, inError: PropTypes.bool, + inWarning: PropTypes.bool, network: PropTypes.string, openToDropdown: PropTypes.func, + selectedToken: PropTypes.object, to: PropTypes.string, toAccounts: PropTypes.array, toDropdownOpen: PropTypes.bool, + tokens: PropTypes.array, updateGas: PropTypes.func, updateSendTo: PropTypes.func, updateSendToError: PropTypes.func, + updateSendToWarning: PropTypes.func, scanQrCode: PropTypes.func, - }; + } static contextTypes = { t: PropTypes.func, - }; + metricsEvent: PropTypes.func, + } - handleToChange (to, nickname = '', toError) { - const { hasHexData, updateSendTo, updateSendToError, updateGas } = this.props - const toErrorObject = getToErrorObject(to, toError, hasHexData) + handleToChange (to, nickname = '', toError, toWarning, network) { + const { hasHexData, updateSendTo, updateSendToError, updateGas, tokens, selectedToken, updateSendToWarning } = this.props + const toErrorObject = getToErrorObject(to, toError, hasHexData, tokens, selectedToken, network) + const toWarningObject = getToWarningObject(to, toWarning, tokens, selectedToken) updateSendTo(to, nickname) updateSendToError(toErrorObject) + updateSendToWarning(toWarningObject) if (toErrorObject.to === null) { updateGas({ to }) } @@ -39,6 +46,7 @@ export default class SendToRow extends Component { const { closeToDropdown, inError, + inWarning, network, openToDropdown, to, @@ -51,16 +59,27 @@ export default class SendToRow extends Component { errorType={'to'} label={`${this.context.t('to')}: `} showError={inError} - > + showWarning={inWarning} + warningType={'to'} + > <EnsInput - scanQrCode={_ => this.props.scanQrCode()} + scanQrCode={_ => { + this.context.metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Used QR scanner', + }, + }) + this.props.scanQrCode() + }} accounts={toAccounts} closeDropdown={() => closeToDropdown()} dropdownOpen={toDropdownOpen} inError={inError} name={'address'} network={network} - onChange={({ toAddress, nickname, toError }) => this.handleToChange(toAddress, nickname, toError)} + onChange={({ toAddress, nickname, toError, toWarning }) => this.handleToChange(toAddress, nickname, toError, toWarning, this.props.network)} openDropdown={() => openToDropdown()} placeholder={this.context.t('recipientAddress')} to={to} diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.container.js b/ui/app/components/app/send/send-content/send-to-row/send-to-row.container.js index 3ee188bad..30865d295 100644 --- a/ui/app/components/send/send-content/send-to-row/send-to-row.container.js +++ b/ui/app/components/app/send/send-content/send-to-row/send-to-row.container.js @@ -1,22 +1,26 @@ import { connect } from 'react-redux' import { getCurrentNetwork, + getSelectedToken, getSendTo, getSendToAccounts, getSendHexData, } from '../../send.selectors.js' import { getToDropdownOpen, + getTokens, sendToIsInError, + sendToIsInWarning, } from './send-to-row.selectors.js' import { updateSendTo, -} from '../../../../actions' +} from '../../../../../store/actions' import { updateSendErrors, + updateSendWarnings, openToDropdown, closeToDropdown, -} from '../../../../ducks/send.duck' +} from '../../../../../ducks/send/send.duck' import SendToRow from './send-to-row.component' export default connect(mapStateToProps, mapDispatchToProps)(SendToRow) @@ -25,10 +29,13 @@ function mapStateToProps (state) { return { hasHexData: Boolean(getSendHexData(state)), inError: sendToIsInError(state), + inWarning: sendToIsInWarning(state), network: getCurrentNetwork(state), + selectedToken: getSelectedToken(state), to: getSendTo(state), toAccounts: getSendToAccounts(state), toDropdownOpen: getToDropdownOpen(state), + tokens: getTokens(state), } } @@ -40,5 +47,8 @@ function mapDispatchToProps (dispatch) { updateSendToError: (toErrorObject) => { dispatch(updateSendErrors(toErrorObject)) }, + updateSendToWarning: (toWarningObject) => { + dispatch(updateSendWarnings(toWarningObject)) + }, } } diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js b/ui/app/components/app/send/send-content/send-to-row/send-to-row.selectors.js index 8919014be..a6160d335 100644 --- a/ui/app/components/send/send-content/send-to-row/send-to-row.selectors.js +++ b/ui/app/components/app/send/send-content/send-to-row/send-to-row.selectors.js @@ -1,6 +1,8 @@ const selectors = { getToDropdownOpen, + getTokens, sendToIsInError, + sendToIsInWarning, } module.exports = selectors @@ -12,3 +14,11 @@ function getToDropdownOpen (state) { function sendToIsInError (state) { return Boolean(state.send.errors.to) } + +function sendToIsInWarning (state) { + return Boolean(state.send.warnings.to) +} + +function getTokens (state) { + return state.metamask.tokens +} diff --git a/ui/app/components/app/send/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/app/send/send-content/send-to-row/send-to-row.utils.js new file mode 100644 index 000000000..60e75d34c --- /dev/null +++ b/ui/app/components/app/send/send-content/send-to-row/send-to-row.utils.js @@ -0,0 +1,36 @@ +const { + REQUIRED_ERROR, + INVALID_RECIPIENT_ADDRESS_ERROR, + KNOWN_RECIPIENT_ADDRESS_ERROR, + INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR, +} = require('../../send.constants') +const { isValidAddress, isEthNetwork } = require('../../../../../helpers/utils/util') +import { checkExistingAddresses } from '../../../../../pages/add-token/util' + +const ethUtil = require('ethereumjs-util') +const contractMap = require('eth-contract-metadata') + +function getToErrorObject (to, toError = null, hasHexData = false, tokens = [], selectedToken = null, network) { + if (!to) { + if (!hasHexData) { + toError = REQUIRED_ERROR + } + } else if (!isValidAddress(to, network) && !toError) { + toError = isEthNetwork(network) ? INVALID_RECIPIENT_ADDRESS_ERROR : INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR + } else if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) { + toError = KNOWN_RECIPIENT_ADDRESS_ERROR + } + return { to: toError } +} + +function getToWarningObject (to, toWarning = null, tokens = [], selectedToken = null) { + if (selectedToken && (ethUtil.toChecksumAddress(to) in contractMap || checkExistingAddresses(to, tokens))) { + toWarning = KNOWN_RECIPIENT_ADDRESS_ERROR + } + return { to: toWarning } +} + +module.exports = { + getToErrorObject, + getToWarningObject, +} diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-component.test.js index 591229deb..d4d054057 100644 --- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-component.test.js +++ b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-component.test.js @@ -9,6 +9,9 @@ const SendToRow = proxyquire('../send-to-row.component.js', { getToErrorObject: (to, toError) => ({ to: to === false ? null : `mockToErrorObject:${to}${toError}`, }), + getToWarningObject: (to, toWarning) => ({ + to: to === false ? null : `mockToWarningObject:${to}${toWarning}`, + }), }, }).default @@ -21,6 +24,7 @@ const propsMethodSpies = { updateGas: sinon.spy(), updateSendTo: sinon.spy(), updateSendToError: sinon.spy(), + updateSendToWarning: sinon.spy(), } sinon.spy(SendToRow.prototype, 'handleToChange') @@ -33,6 +37,7 @@ describe('SendToRow Component', function () { wrapper = shallow(<SendToRow closeToDropdown={propsMethodSpies.closeToDropdown} inError={false} + inWarning={false} network={'mockNetwork'} openToDropdown={propsMethodSpies.openToDropdown} to={'mockTo'} @@ -41,6 +46,7 @@ describe('SendToRow Component', function () { updateGas={propsMethodSpies.updateGas} updateSendTo={propsMethodSpies.updateSendTo} updateSendToError={propsMethodSpies.updateSendToError} + updateSendToWarning={propsMethodSpies.updateSendToWarning} />, { context: { t: str => str + '_t' } }) instance = wrapper.instance() }) @@ -50,6 +56,7 @@ describe('SendToRow Component', function () { propsMethodSpies.openToDropdown.resetHistory() propsMethodSpies.updateSendTo.resetHistory() propsMethodSpies.updateSendToError.resetHistory() + propsMethodSpies.updateSendToWarning.resetHistory() SendToRow.prototype.handleToChange.resetHistory() }) @@ -75,6 +82,16 @@ describe('SendToRow Component', function () { ) }) + it('should call updateSendToWarning', () => { + assert.equal(propsMethodSpies.updateSendToWarning.callCount, 0) + instance.handleToChange('mockTo2', '', '', 'mockToWarning') + assert.equal(propsMethodSpies.updateSendToWarning.callCount, 1) + assert.deepEqual( + propsMethodSpies.updateSendToWarning.getCall(0).args, + [{ to: 'mockToWarningObject:mockTo2mockToWarning' }] + ) + }) + it('should not call updateGas if there is a to error', () => { assert.equal(propsMethodSpies.updateGas.callCount, 0) instance.handleToChange('mockTo2') @@ -138,11 +155,11 @@ describe('SendToRow Component', function () { openDropdown() assert.equal(propsMethodSpies.openToDropdown.callCount, 1) assert.equal(SendToRow.prototype.handleToChange.callCount, 0) - onChange({ toAddress: 'mockNewTo', nickname: 'mockNewNickname', toError: 'mockToError' }) + onChange({ toAddress: 'mockNewTo', nickname: 'mockNewNickname', toError: 'mockToError', toWarning: 'mockToWarning' }) assert.equal(SendToRow.prototype.handleToChange.callCount, 1) assert.deepEqual( SendToRow.prototype.handleToChange.getCall(0).args, - ['mockNewTo', 'mockNewNickname', 'mockToError'] + ['mockNewTo', 'mockNewNickname', 'mockToError', 'mockToWarning', 'mockNetwork' ] ) }) }) diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-container.test.js index dfce7652f..94b4f1024 100644 --- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-container.test.js +++ b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-container.test.js @@ -12,6 +12,7 @@ const duckActionSpies = { closeToDropdown: sinon.spy(), openToDropdown: sinon.spy(), updateSendErrors: sinon.spy(), + updateSendWarnings: sinon.spy(), } proxyquire('../send-to-row.container.js', { @@ -24,6 +25,7 @@ proxyquire('../send-to-row.container.js', { }, '../../send.selectors.js': { getCurrentNetwork: (s) => `mockNetwork:${s}`, + getSelectedToken: (s) => `mockSelectedToken:${s}`, getSendHexData: (s) => s, getSendTo: (s) => `mockTo:${s}`, getSendToAccounts: (s) => `mockToAccounts:${s}`, @@ -31,9 +33,11 @@ proxyquire('../send-to-row.container.js', { './send-to-row.selectors.js': { getToDropdownOpen: (s) => `mockToDropdownOpen:${s}`, sendToIsInError: (s) => `mockInError:${s}`, + sendToIsInWarning: (s) => `mockInWarning:${s}`, + getTokens: (s) => `mockTokens:${s}`, }, - '../../../../actions': actionSpies, - '../../../../ducks/send.duck': duckActionSpies, + '../../../../../store/actions': actionSpies, + '../../../../../ducks/send/send.duck': duckActionSpies, }) describe('send-to-row container', () => { @@ -44,10 +48,13 @@ describe('send-to-row container', () => { assert.deepEqual(mapStateToProps('mockState'), { hasHexData: true, inError: 'mockInError:mockState', + inWarning: 'mockInWarning:mockState', network: 'mockNetwork:mockState', + selectedToken: 'mockSelectedToken:mockState', to: 'mockTo:mockState', toAccounts: 'mockToAccounts:mockState', toDropdownOpen: 'mockToDropdownOpen:mockState', + tokens: 'mockTokens:mockState', }) }) @@ -110,6 +117,18 @@ describe('send-to-row container', () => { }) }) + describe('updateSendToWarning()', () => { + it('should dispatch an action', () => { + mapDispatchToPropsObject.updateSendToWarning('mockToWarningObject') + assert(dispatchSpy.calledOnce) + assert(duckActionSpies.updateSendWarnings.calledOnce) + assert.equal( + duckActionSpies.updateSendWarnings.getCall(0).args[0], + 'mockToWarningObject' + ) + }) + }) + }) }) diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-selectors.test.js index 122ad3265..0fa342d1e 100644 --- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-selectors.test.js +++ b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-selectors.test.js @@ -1,6 +1,7 @@ import assert from 'assert' import { getToDropdownOpen, + getTokens, sendToIsInError, } from '../send-to-row.selectors.js' @@ -44,4 +45,15 @@ describe('send-to-row selectors', () => { }) }) + describe('getTokens()', () => { + it('should return empty array if no tokens in state', () => { + const state = { + metamask: { + tokens: [], + }, + } + + assert.deepStrictEqual(getTokens(state), []) + }) + }) }) diff --git a/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-utils.test.js new file mode 100644 index 000000000..95882d640 --- /dev/null +++ b/ui/app/components/app/send/send-content/send-to-row/tests/send-to-row-utils.test.js @@ -0,0 +1,107 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' +import sinon from 'sinon' + +import { + REQUIRED_ERROR, + INVALID_RECIPIENT_ADDRESS_ERROR, + KNOWN_RECIPIENT_ADDRESS_ERROR, +} from '../../../send.constants' + +const stubs = { + isValidAddress: sinon.stub().callsFake(to => Boolean(to.match(/^[0xabcdef123456798]+$/))), +} + +const toRowUtils = proxyquire('../send-to-row.utils.js', { + '../../../../../helpers/utils/util': { + isValidAddress: stubs.isValidAddress, + }, +}) +const { + getToErrorObject, + getToWarningObject, +} = toRowUtils + +describe('send-to-row utils', () => { + + describe('getToErrorObject()', () => { + it('should return a required error if to is falsy', () => { + assert.deepEqual(getToErrorObject(null), { + to: REQUIRED_ERROR, + }) + }) + + it('should return null if to is falsy and hexData is truthy', () => { + assert.deepEqual(getToErrorObject(null, undefined, true), { + to: null, + }) + }) + + it('should return an invalid recipient error if to is truthy but invalid', () => { + assert.deepEqual(getToErrorObject('mockInvalidTo'), { + to: INVALID_RECIPIENT_ADDRESS_ERROR, + }) + }) + + it('should return null if to is truthy and valid', () => { + assert.deepEqual(getToErrorObject('0xabc123'), { + to: null, + }) + }) + + it('should return the passed error if to is truthy but invalid if to is truthy and valid', () => { + assert.deepEqual(getToErrorObject('invalid #$ 345878', 'someExplicitError'), { + to: 'someExplicitError', + }) + }) + + it('should return a known address recipient if to is truthy but part of state tokens', () => { + assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: KNOWN_RECIPIENT_ADDRESS_ERROR, + }) + }) + + it('should null if to is truthy part of tokens but selectedToken falsy', () => { + assert.deepEqual(getToErrorObject('0xabc123', undefined, false, [{'address': '0xabc123'}]), { + to: null, + }) + }) + + it('should return a known address recipient if to is truthy but part of contract metadata', () => { + assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: KNOWN_RECIPIENT_ADDRESS_ERROR, + }) + }) + it('should null if to is truthy part of contract metadata but selectedToken falsy', () => { + assert.deepEqual(getToErrorObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, false, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: KNOWN_RECIPIENT_ADDRESS_ERROR, + }) + }) + }) + + describe('getToWarningObject()', () => { + it('should return a known address recipient if to is truthy but part of state tokens', () => { + assert.deepEqual(getToWarningObject('0xabc123', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: KNOWN_RECIPIENT_ADDRESS_ERROR, + }) + }) + + it('should null if to is truthy part of tokens but selectedToken falsy', () => { + assert.deepEqual(getToWarningObject('0xabc123', undefined, [{'address': '0xabc123'}]), { + to: null, + }) + }) + + it('should return a known address recipient if to is truthy but part of contract metadata', () => { + assert.deepEqual(getToWarningObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: KNOWN_RECIPIENT_ADDRESS_ERROR, + }) + }) + it('should null if to is truthy part of contract metadata but selectedToken falsy', () => { + assert.deepEqual(getToWarningObject('0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', undefined, [{'address': '0xabc123'}], {'address': '0xabc123'}), { + to: KNOWN_RECIPIENT_ADDRESS_ERROR, + }) + }) + }) + +}) diff --git a/ui/app/components/send/send-content/tests/send-content-component.test.js b/ui/app/components/app/send/send-content/tests/send-content-component.test.js index c5a11c8bb..7d102c930 100644 --- a/ui/app/components/send/send-content/tests/send-content-component.test.js +++ b/ui/app/components/app/send/send-content/tests/send-content-component.test.js @@ -3,7 +3,7 @@ import assert from 'assert' import { shallow } from 'enzyme' import SendContent from '../send-content.component.js' -import PageContainerContent from '../../../page-container/page-container-content.component' +import PageContainerContent from '../../../../ui/page-container/page-container-content.component' import SendAmountRow from '../send-amount-row/send-amount-row.container' import SendFromRow from '../send-from-row/send-from-row.container' import SendGasRow from '../send-gas-row/send-gas-row.container' diff --git a/ui/app/components/send/send-footer/README.md b/ui/app/components/app/send/send-footer/README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-footer/README.md +++ b/ui/app/components/app/send/send-footer/README.md diff --git a/ui/app/components/send/send-footer/index.js b/ui/app/components/app/send/send-footer/index.js index 58e91d622..58e91d622 100644 --- a/ui/app/components/send/send-footer/index.js +++ b/ui/app/components/app/send/send-footer/index.js diff --git a/ui/app/components/send/send-footer/send-footer.component.js b/ui/app/components/app/send/send-footer/send-footer.component.js index 230bf450f..cc891a9b3 100644 --- a/ui/app/components/send/send-footer/send-footer.component.js +++ b/ui/app/components/app/send/send-footer/send-footer.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import PageContainerFooter from '../../page-container/page-container-footer' -import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../routes' +import PageContainerFooter from '../../../ui/page-container/page-container-footer' +import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../helpers/constants/routes' export default class SendFooter extends Component { @@ -26,10 +26,12 @@ export default class SendFooter extends Component { tokenBalance: PropTypes.string, unapprovedTxs: PropTypes.object, update: PropTypes.func, - }; + sendErrors: PropTypes.object, + } static contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, }; onCancel () { @@ -56,6 +58,7 @@ export default class SendFooter extends Component { toAccounts, history, } = this.props + const { metricsEvent } = this.context // Should not be needed because submit should be disabled if there are errors. // const noErrors = !amountError && toError === null @@ -66,7 +69,6 @@ export default class SendFooter extends Component { // TODO: add nickname functionality addToAddressBookIfNew(to, toAccounts) - const promise = editingTransactionId ? update({ amount, @@ -82,13 +84,44 @@ export default class SendFooter extends Component { : sign({ data, selectedToken, to, amount, from, gas, gasPrice }) Promise.resolve(promise) - .then(() => history.push(CONFIRM_TRANSACTION_ROUTE)) + .then(() => { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Complete', + }, + }) + history.push(CONFIRM_TRANSACTION_ROUTE) + }) } formShouldBeDisabled () { const { data, inError, selectedToken, tokenBalance, gasTotal, to } = this.props const missingTokenBalance = selectedToken && !tokenBalance - return inError || !gasTotal || missingTokenBalance || !(data || to) + const shouldBeDisabled = inError || !gasTotal || missingTokenBalance || !(data || to) + return shouldBeDisabled + } + + componentDidUpdate (prevProps) { + const { inError, sendErrors } = this.props + const { metricsEvent } = this.context + if (!prevProps.inError && inError) { + const errorField = Object.keys(sendErrors).find(key => sendErrors[key]) + const errorMessage = sendErrors[errorField] + + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Edit Screen', + name: 'Error', + }, + customVariables: { + errorField, + errorMessage, + }, + }) + } } render () { diff --git a/ui/app/components/send/send-footer/send-footer.container.js b/ui/app/components/app/send/send-footer/send-footer.container.js index 60de4d030..502159a81 100644 --- a/ui/app/components/send/send-footer/send-footer.container.js +++ b/ui/app/components/app/send/send-footer/send-footer.container.js @@ -6,7 +6,7 @@ import { signTokenTx, signTx, updateTransaction, -} from '../../../actions' +} from '../../../../store/actions' import SendFooter from './send-footer.component' import { getGasLimit, @@ -21,6 +21,7 @@ import { getSendHexData, getTokenBalance, getUnapprovedTxs, + getSendErrors, } from '../send.selectors' import { isSendFormInError, @@ -48,6 +49,7 @@ function mapStateToProps (state) { toAccounts: getSendToAccounts(state), tokenBalance: getTokenBalance(state), unapprovedTxs: getUnapprovedTxs(state), + sendErrors: getSendErrors(state), } } diff --git a/ui/app/components/send/send-footer/send-footer.scss b/ui/app/components/app/send/send-footer/send-footer.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-footer/send-footer.scss +++ b/ui/app/components/app/send/send-footer/send-footer.scss diff --git a/ui/app/components/send/send-footer/send-footer.selectors.js b/ui/app/components/app/send/send-footer/send-footer.selectors.js index e20addfdc..e20addfdc 100644 --- a/ui/app/components/send/send-footer/send-footer.selectors.js +++ b/ui/app/components/app/send/send-footer/send-footer.selectors.js diff --git a/ui/app/components/send/send-footer/send-footer.utils.js b/ui/app/components/app/send/send-footer/send-footer.utils.js index f82ff1e9b..f82ff1e9b 100644 --- a/ui/app/components/send/send-footer/send-footer.utils.js +++ b/ui/app/components/app/send/send-footer/send-footer.utils.js diff --git a/ui/app/components/send/send-footer/tests/send-footer-component.test.js b/ui/app/components/app/send/send-footer/tests/send-footer-component.test.js index 65e4bb654..6683ca8c0 100644 --- a/ui/app/components/send/send-footer/tests/send-footer-component.test.js +++ b/ui/app/components/app/send/send-footer/tests/send-footer-component.test.js @@ -2,10 +2,10 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' -import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../routes' +import { CONFIRM_TRANSACTION_ROUTE, DEFAULT_ROUTE } from '../../../../../helpers/constants/routes' import SendFooter from '../send-footer.component.js' -import PageContainerFooter from '../../../page-container/page-container-footer' +import PageContainerFooter from '../../../../ui/page-container/page-container-footer' const propsMethodSpies = { addToAddressBookIfNew: sinon.spy(), @@ -45,7 +45,8 @@ describe('SendFooter Component', function () { tokenBalance={'mockTokenBalance'} unapprovedTxs={['mockTx']} update={propsMethodSpies.update} - />, { context: { t: str => str } }) + sendErrors={{}} + />, { context: { t: str => str, metricsEvent: () => ({}) } }) }) afterEach(() => { @@ -201,7 +202,7 @@ describe('SendFooter Component', function () { tokenBalance={'mockTokenBalance'} unapprovedTxs={['mockTx']} update={propsMethodSpies.update} - />, { context: { t: str => str } }) + />, { context: { t: str => str, metricsEvent: () => ({}) } }) }) afterEach(() => { diff --git a/ui/app/components/send/send-footer/tests/send-footer-container.test.js b/ui/app/components/app/send/send-footer/tests/send-footer-container.test.js index cf4c893ee..878b0aa19 100644 --- a/ui/app/components/send/send-footer/tests/send-footer-container.test.js +++ b/ui/app/components/app/send/send-footer/tests/send-footer-container.test.js @@ -14,7 +14,9 @@ const actionSpies = { } const utilsStubs = { addressIsNew: sinon.stub().returns(true), - constructTxParams: sinon.stub().returns('mockConstructedTxParams'), + constructTxParams: sinon.stub().returns({ + value: 'mockAmount', + }), constructUpdatedTx: sinon.stub().returns('mockConstructedUpdatedTxParams'), } @@ -26,7 +28,7 @@ proxyquire('../send-footer.container.js', { return () => ({}) }, }, - '../../../actions': actionSpies, + '../../../../store/actions': actionSpies, '../send.selectors': { getGasLimit: (s) => `mockGasLimit:${s}`, getGasPrice: (s) => `mockGasPrice:${s}`, @@ -40,6 +42,7 @@ proxyquire('../send-footer.container.js', { getTokenBalance: (s) => `mockTokenBalance:${s}`, getSendHexData: (s) => `mockHexData:${s}`, getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`, + getSendErrors: (s) => `mockSendErrors:${s}`, }, './send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` }, './send-footer.utils': utilsStubs, @@ -64,6 +67,7 @@ describe('send-footer container', () => { toAccounts: 'mockToAccounts:mockState', tokenBalance: 'mockTokenBalance:mockState', unapprovedTxs: 'mockUnapprovedTxs:mockState', + sendErrors: 'mockSendErrors:mockState', }) }) @@ -115,7 +119,7 @@ describe('send-footer container', () => { ) assert.deepEqual( actionSpies.signTokenTx.getCall(0).args, - [ '0xabc', 'mockTo', 'mockAmount', 'mockConstructedTxParams' ] + [ '0xabc', 'mockTo', 'mockAmount', { value: 'mockAmount' } ] ) }) @@ -143,7 +147,7 @@ describe('send-footer container', () => { ) assert.deepEqual( actionSpies.signTx.getCall(0).args, - [ 'mockConstructedTxParams' ] + [ { value: 'mockAmount' } ] ) }) }) diff --git a/ui/app/components/send/send-footer/tests/send-footer-selectors.test.js b/ui/app/components/app/send/send-footer/tests/send-footer-selectors.test.js index 8de032f57..8de032f57 100644 --- a/ui/app/components/send/send-footer/tests/send-footer-selectors.test.js +++ b/ui/app/components/app/send/send-footer/tests/send-footer-selectors.test.js diff --git a/ui/app/components/send/send-footer/tests/send-footer-utils.test.js b/ui/app/components/app/send/send-footer/tests/send-footer-utils.test.js index 28ff0c891..28ff0c891 100644 --- a/ui/app/components/send/send-footer/tests/send-footer-utils.test.js +++ b/ui/app/components/app/send/send-footer/tests/send-footer-utils.test.js diff --git a/ui/app/components/send/send-header/README.md b/ui/app/components/app/send/send-header/README.md index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send-header/README.md +++ b/ui/app/components/app/send/send-header/README.md diff --git a/ui/app/components/send/send-header/index.js b/ui/app/components/app/send/send-header/index.js index 0b17f0b7d..0b17f0b7d 100644 --- a/ui/app/components/send/send-header/index.js +++ b/ui/app/components/app/send/send-header/index.js diff --git a/ui/app/components/send/send-header/send-header.component.js b/ui/app/components/app/send/send-header/send-header.component.js index efc4bbf27..f216954ef 100644 --- a/ui/app/components/send/send-header/send-header.component.js +++ b/ui/app/components/app/send/send-header/send-header.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import PageContainerHeader from '../../page-container/page-container-header' -import { DEFAULT_ROUTE } from '../../../routes' +import PageContainerHeader from '../../../ui/page-container/page-container-header' +import { DEFAULT_ROUTE } from '../../../../helpers/constants/routes' export default class SendHeader extends Component { diff --git a/ui/app/components/send/send-header/send-header.container.js b/ui/app/components/app/send/send-header/send-header.container.js index 4bcd0d1b6..ce53fba9a 100644 --- a/ui/app/components/send/send-header/send-header.container.js +++ b/ui/app/components/app/send/send-header/send-header.container.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux' -import { clearSend } from '../../../actions' +import { clearSend } from '../../../../store/actions' import SendHeader from './send-header.component' import { getSubtitleParams, getTitleKey } from './send-header.selectors' diff --git a/ui/app/components/send/send-header/send-header.selectors.js b/ui/app/components/app/send/send-header/send-header.selectors.js index d7c9d3766..d7c9d3766 100644 --- a/ui/app/components/send/send-header/send-header.selectors.js +++ b/ui/app/components/app/send/send-header/send-header.selectors.js diff --git a/ui/app/components/send/send-header/tests/send-header-component.test.js b/ui/app/components/app/send/send-header/tests/send-header-component.test.js index 930bfa387..db2ee8967 100644 --- a/ui/app/components/send/send-header/tests/send-header-component.test.js +++ b/ui/app/components/app/send/send-header/tests/send-header-component.test.js @@ -2,10 +2,10 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' -import { DEFAULT_ROUTE } from '../../../../routes' +import { DEFAULT_ROUTE } from '../../../../../helpers/constants/routes' import SendHeader from '../send-header.component.js' -import PageContainerHeader from '../../../page-container/page-container-header' +import PageContainerHeader from '../../../../ui/page-container/page-container-header' const propsMethodSpies = { clearSend: sinon.spy(), diff --git a/ui/app/components/send/send-header/tests/send-header-container.test.js b/ui/app/components/app/send/send-header/tests/send-header-container.test.js index 41a7e8a89..634c3424b 100644 --- a/ui/app/components/send/send-header/tests/send-header-container.test.js +++ b/ui/app/components/app/send/send-header/tests/send-header-container.test.js @@ -17,7 +17,7 @@ proxyquire('../send-header.container.js', { return () => ({}) }, }, - '../../../actions': actionSpies, + '../../../../store/actions': actionSpies, './send-header.selectors': { getTitleKey: (s) => `mockTitleKey:${s}`, getSubtitleParams: (s) => `mockSubtitleParams:${s}`, diff --git a/ui/app/components/send/send-header/tests/send-header-selectors.test.js b/ui/app/components/app/send/send-header/tests/send-header-selectors.test.js index e0c6a3ab3..e0c6a3ab3 100644 --- a/ui/app/components/send/send-header/tests/send-header-selectors.test.js +++ b/ui/app/components/app/send/send-header/tests/send-header-selectors.test.js diff --git a/ui/app/components/send/send.component.js b/ui/app/components/app/send/send.component.js index fb7beca16..a38b681b0 100644 --- a/ui/app/components/send/send.component.js +++ b/ui/app/components/app/send/send.component.js @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import PersistentForm from '../../../lib/persistent-form' +import PersistentForm from '../../../../lib/persistent-form' import { getAmountErrorObject, getGasFeeErrorObject, @@ -8,9 +8,9 @@ import { doesAmountErrorRequireUpdate, } from './send.utils' -import SendHeader from './send-header/' -import SendContent from './send-content/' -import SendFooter from './send-footer/' +import SendHeader from './send-header' +import SendContent from './send-content' +import SendFooter from './send-footer' export default class SendTransactionScreen extends PersistentForm { @@ -35,17 +35,18 @@ export default class SendTransactionScreen extends PersistentForm { selectedToken: PropTypes.object, tokenBalance: PropTypes.string, tokenContract: PropTypes.object, + fetchBasicGasEstimates: PropTypes.func, updateAndSetGasTotal: PropTypes.func, updateSendErrors: PropTypes.func, updateSendTokenBalance: PropTypes.func, scanQrCode: PropTypes.func, qrCodeDetected: PropTypes.func, qrCodeData: PropTypes.object, - }; + } static contextTypes = { t: PropTypes.func, - }; + } componentWillReceiveProps (nextProps) { if (nextProps.qrCodeData) { @@ -73,10 +74,10 @@ export default class SendTransactionScreen extends PersistentForm { selectedAddress, selectedToken = {}, to: currentToAddress, - updateAndSetGasTotal, + updateAndSetGasLimit, } = this.props - updateAndSetGasTotal({ + updateAndSetGasLimit({ blockGasLimit, editingTransactionId, gasLimit, @@ -138,14 +139,12 @@ export default class SendTransactionScreen extends PersistentForm { }) const gasFeeErrorObject = selectedToken ? getGasFeeErrorObject({ - amount, amountConversionRate, balance, conversionRate, gasTotal, primaryCurrency, selectedToken, - tokenBalance, }) : { gasFee: null } updateSendErrors(Object.assign(amountErrorObject, gasFeeErrorObject)) @@ -164,6 +163,13 @@ export default class SendTransactionScreen extends PersistentForm { } } + componentDidMount () { + this.props.fetchBasicGasEstimates() + .then(() => { + this.updateGas() + }) + } + componentWillMount () { const { from: { address }, @@ -171,12 +177,12 @@ export default class SendTransactionScreen extends PersistentForm { tokenContract, updateSendTokenBalance, } = this.props + updateSendTokenBalance({ selectedToken, tokenContract, address, }) - this.updateGas() // Show QR Scanner modal if ?scan=true if (window.location.search === '?scan=true') { diff --git a/ui/app/components/send/send.constants.js b/ui/app/components/app/send/send.constants.js index 8acdf0641..36549038e 100644 --- a/ui/app/components/send/send.constants.js +++ b/ui/app/components/app/send/send.constants.js @@ -1,5 +1,5 @@ const ethUtil = require('ethereumjs-util') -const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') +const { conversionUtil, multiplyCurrencies } = require('../../../helpers/utils/conversion-util') const MIN_GAS_PRICE_DEC = '0' const MIN_GAS_PRICE_HEX = (parseInt(MIN_GAS_PRICE_DEC)).toString(16) @@ -26,7 +26,9 @@ const INSUFFICIENT_FUNDS_ERROR = 'insufficientFunds' const INSUFFICIENT_TOKENS_ERROR = 'insufficientTokens' const NEGATIVE_ETH_ERROR = 'negativeETH' const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient' +const INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR = 'invalidAddressRecipientNotEthNetwork' const REQUIRED_ERROR = 'required' +const KNOWN_RECIPIENT_ADDRESS_ERROR = 'knownAddressRecipient' const ONE_GWEI_IN_WEI_HEX = ethUtil.addHexPrefix(conversionUtil('0x1', { fromDenomination: 'GWEI', @@ -42,6 +44,8 @@ module.exports = { INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_TOKENS_ERROR, INVALID_RECIPIENT_ADDRESS_ERROR, + KNOWN_RECIPIENT_ADDRESS_ERROR, + INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR, MIN_GAS_LIMIT_DEC, MIN_GAS_LIMIT_HEX, MIN_GAS_PRICE_DEC, diff --git a/ui/app/components/send/send.container.js b/ui/app/components/app/send/send.container.js index 87056499f..e65463b93 100644 --- a/ui/app/components/send/send.container.js +++ b/ui/app/components/app/send/send.container.js @@ -31,18 +31,21 @@ import { setGasTotal, showQrScanner, qrCodeDetected, -} from '../../actions' +} from '../../../store/actions' import { resetSendState, updateSendErrors, -} from '../../ducks/send.duck' +} from '../../../ducks/send/send.duck' +import { + fetchBasicGasEstimates, +} from '../../../ducks/gas/gas.duck' import { calcGasTotal, } from './send.utils.js' import { SEND_ROUTE, -} from '../../routes' +} from '../../../helpers/constants/routes' module.exports = compose( withRouter, @@ -76,7 +79,7 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { - updateAndSetGasTotal: ({ + updateAndSetGasLimit: ({ blockGasLimit, editingTransactionId, gasLimit, @@ -89,7 +92,7 @@ function mapDispatchToProps (dispatch) { data, }) => { !editingTransactionId - ? dispatch(updateGasData({ recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value, data })) + ? dispatch(updateGasData({ gasPrice, recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value, data })) : dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice))) }, updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => { @@ -104,5 +107,6 @@ function mapDispatchToProps (dispatch) { scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)), qrCodeDetected: (data) => dispatch(qrCodeDetected(data)), updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), + fetchBasicGasEstimates: () => dispatch(fetchBasicGasEstimates()), } } diff --git a/ui/app/components/send/send.scss b/ui/app/components/app/send/send.scss index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/send.scss +++ b/ui/app/components/app/send/send.scss diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/app/send/send.selectors.js index eb22a08b7..2ec677ad1 100644 --- a/ui/app/components/send/send.selectors.js +++ b/ui/app/components/app/send/send.selectors.js @@ -1,15 +1,21 @@ -const { valuesFor } = require('../../util') +const { valuesFor } = require('../../../helpers/utils/util') const abi = require('human-standard-token-abi') const { multiplyCurrencies, -} = require('../../conversion-util') +} = require('../../../helpers/utils/conversion-util') +const { + getMetaMaskAccounts, +} = require('../../../selectors/selectors') const { estimateGasPriceFromRecentBlocks, + calcGasTotal, } = require('./send.utils') +import { + getFastPriceEstimateInHexWEI, +} from '../../../selectors/custom-gas' const selectors = { accountsWithSendEtherInfoSelector, - // autoAddToBetaUI, getAddressBook, getAmountConversionRate, getBlockGasLimit, @@ -44,6 +50,7 @@ const selectors = { getSendMaxModeState, getSendTo, getSendToAccounts, + getSendWarnings, getTokenBalance, getTokenExchangeRate, getUnapprovedTxs, @@ -54,10 +61,8 @@ const selectors = { module.exports = selectors function accountsWithSendEtherInfoSelector (state) { - const { - accounts, - identities, - } = state.metamask + const accounts = getMetaMaskAccounts(state) + const { identities } = state.metamask const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => { return Object.assign({}, account, identities[key]) @@ -66,23 +71,6 @@ function accountsWithSendEtherInfoSelector (state) { return accountsWithSendEtherInfo } -// function autoAddToBetaUI (state) { -// const autoAddTransactionThreshold = 12 -// const autoAddAccountsThreshold = 2 -// const autoAddTokensThreshold = 1 - -// const numberOfTransactions = state.metamask.selectedAddressTxList.length -// const numberOfAccounts = Object.keys(state.metamask.accounts).length -// const numberOfTokensAdded = state.metamask.tokens.length - -// const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) && -// (numberOfAccounts > autoAddAccountsThreshold) && -// (numberOfTokensAdded > autoAddTokensThreshold) -// const userIsNotInBeta = !state.metamask.featureFlags.betaUI - -// return userIsNotInBeta && userPassesThreshold -// } - function getAddressBook (state) { return state.metamask.addressBook } @@ -130,11 +118,11 @@ function getForceGasMin (state) { } function getGasLimit (state) { - return state.metamask.send.gasLimit + return state.metamask.send.gasLimit || '0' } function getGasPrice (state) { - return state.metamask.send.gasPrice + return state.metamask.send.gasPrice || getFastPriceEstimateInHexWEI(state) } function getGasPriceFromRecentBlocks (state) { @@ -142,7 +130,7 @@ function getGasPriceFromRecentBlocks (state) { } function getGasTotal (state) { - return state.metamask.send.gasTotal + return calcGasTotal(getGasLimit(state), getGasPrice(state)) } function getPrimaryCurrency (state) { @@ -155,14 +143,14 @@ function getRecentBlocks (state) { } function getSelectedAccount (state) { - const accounts = state.metamask.accounts + const accounts = getMetaMaskAccounts(state) const selectedAddress = getSelectedAddress(state) return accounts[selectedAddress] } function getSelectedAddress (state) { - const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0] + const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0] return selectedAddress } @@ -263,6 +251,10 @@ function getSendToAccounts (state) { return Object.entries(allAccounts).map(([key, account]) => account) } +function getSendWarnings (state) { + return state.send.warnings +} + function getTokenBalance (state) { return state.metamask.send.tokenBalance } diff --git a/ui/app/components/send/send.utils.js b/ui/app/components/app/send/send.utils.js index eb1667c63..7609d46ea 100644 --- a/ui/app/components/send/send.utils.js +++ b/ui/app/components/app/send/send.utils.js @@ -5,10 +5,10 @@ const { multiplyCurrencies, conversionGreaterThan, conversionLessThan, -} = require('../../conversion-util') +} = require('../../../helpers/utils/conversion-util') const { calcTokenAmount, -} = require('../../token-util') +} = require('../../../helpers/utils/token-util') const { BASE_TOKEN_GAS_COST, INSUFFICIENT_FUNDS_ERROR, @@ -89,11 +89,10 @@ function isTokenBalanceSufficient ({ const tokenBalanceIsSufficient = conversionGTE( { value: tokenBalance, - fromNumericBase: 'dec', + fromNumericBase: 'hex', }, { value: calcTokenAmount(amountInDec, decimals), - fromNumericBase: 'dec', }, ) @@ -151,7 +150,6 @@ function getAmountErrorObject ({ } function getGasFeeErrorObject ({ - amount, amountConversionRate, balance, conversionRate, @@ -180,7 +178,7 @@ function getGasFeeErrorObject ({ function calcTokenBalance ({ selectedToken, usersToken }) { const { decimals } = selectedToken || {} - return calcTokenAmount(usersToken.balance.toString(), decimals) + '' + return calcTokenAmount(usersToken.balance.toString(), decimals).toString(16) } function doesAmountErrorRequireUpdate ({ @@ -236,10 +234,14 @@ async function estimateGas ({ if (to) { paramsForGasEstimate.to = to } + + if (!value || value === '0') { + paramsForGasEstimate.value = '0xff' + } } // if not, fall back to block gasLimit - paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit, 0.95, { + paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit || '0x5208', 0.95, { multiplicandBase: 16, multiplierBase: 10, roundDown: '0', diff --git a/ui/app/components/send/tests/send-component.test.js b/ui/app/components/app/send/tests/send-component.test.js index f4943e707..738c14839 100644 --- a/ui/app/components/send/tests/send-component.test.js +++ b/ui/app/components/app/send/tests/send-component.test.js @@ -3,16 +3,23 @@ import assert from 'assert' import proxyquire from 'proxyquire' import { shallow } from 'enzyme' import sinon from 'sinon' +import timeout from '../../../../../lib/test-timeout' import SendHeader from '../send-header/send-header.container' import SendContent from '../send-content/send-content.component' import SendFooter from '../send-footer/send-footer.container' +const mockBasicGasEstimates = { + blockTime: 'mockBlockTime', +} + const propsMethodSpies = { - updateAndSetGasTotal: sinon.spy(), + updateAndSetGasLimit: sinon.spy(), updateSendErrors: sinon.spy(), updateSendTokenBalance: sinon.spy(), resetSendState: sinon.spy(), + fetchBasicGasEstimates: sinon.stub().returns(Promise.resolve(mockBasicGasEstimates)), + fetchGasEstimates: sinon.spy(), } const utilsMethodStubs = { getAmountErrorObject: sinon.stub().returns({ amount: 'mockAmountError' }), @@ -37,6 +44,8 @@ describe('Send Component', function () { blockGasLimit={'mockBlockGasLimit'} conversionRate={10} editingTransactionId={'mockEditingTransactionId'} + fetchBasicGasEstimates={propsMethodSpies.fetchBasicGasEstimates} + fetchGasEstimates={propsMethodSpies.fetchGasEstimates} from={ { address: 'mockAddress', balance: 'mockBalance' } } gasLimit={'mockGasLimit'} gasPrice={'mockGasPrice'} @@ -50,7 +59,7 @@ describe('Send Component', function () { showHexData={true} tokenBalance={'mockTokenBalance'} tokenContract={'mockTokenContract'} - updateAndSetGasTotal={propsMethodSpies.updateAndSetGasTotal} + updateAndSetGasLimit={propsMethodSpies.updateAndSetGasLimit} updateSendErrors={propsMethodSpies.updateSendErrors} updateSendTokenBalance={propsMethodSpies.updateSendTokenBalance} resetSendState={propsMethodSpies.resetSendState} @@ -63,7 +72,8 @@ describe('Send Component', function () { utilsMethodStubs.doesAmountErrorRequireUpdate.resetHistory() utilsMethodStubs.getAmountErrorObject.resetHistory() utilsMethodStubs.getGasFeeErrorObject.resetHistory() - propsMethodSpies.updateAndSetGasTotal.resetHistory() + propsMethodSpies.fetchBasicGasEstimates.resetHistory() + propsMethodSpies.updateAndSetGasLimit.resetHistory() propsMethodSpies.updateSendErrors.resetHistory() propsMethodSpies.updateSendTokenBalance.resetHistory() }) @@ -72,12 +82,20 @@ describe('Send Component', function () { assert(SendTransactionScreen.prototype.componentDidMount.calledOnce) }) - describe('componentWillMount', () => { - it('should call this.updateGas', () => { + describe('componentDidMount', () => { + it('should call props.fetchBasicGasAndTimeEstimates', () => { + propsMethodSpies.fetchBasicGasEstimates.resetHistory() + assert.equal(propsMethodSpies.fetchBasicGasEstimates.callCount, 0) + wrapper.instance().componentDidMount() + assert.equal(propsMethodSpies.fetchBasicGasEstimates.callCount, 1) + }) + + it('should call this.updateGas', async () => { SendTransactionScreen.prototype.updateGas.resetHistory() propsMethodSpies.updateSendErrors.resetHistory() assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 0) - wrapper.instance().componentWillMount() + wrapper.instance().componentDidMount() + await timeout(250) assert.equal(SendTransactionScreen.prototype.updateGas.callCount, 1) }) }) @@ -158,14 +176,12 @@ describe('Send Component', function () { assert.deepEqual( utilsMethodStubs.getGasFeeErrorObject.getCall(0).args[0], { - amount: 'mockAmount', amountConversionRate: 'mockAmountConversionRate', balance: 'mockBalance', conversionRate: 10, gasTotal: 'mockGasTotal', primaryCurrency: 'mockPrimaryCurrency', selectedToken: 'mockSelectedToken', - tokenBalance: 'mockTokenBalance', } ) }) @@ -273,12 +289,12 @@ describe('Send Component', function () { }) describe('updateGas', () => { - it('should call updateAndSetGasTotal with the correct params if no to prop is passed', () => { - propsMethodSpies.updateAndSetGasTotal.resetHistory() + it('should call updateAndSetGasLimit with the correct params if no to prop is passed', () => { + propsMethodSpies.updateAndSetGasLimit.resetHistory() wrapper.instance().updateGas() - assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1) + assert.equal(propsMethodSpies.updateAndSetGasLimit.callCount, 1) assert.deepEqual( - propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0], + propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0], { blockGasLimit: 'mockBlockGasLimit', editingTransactionId: 'mockEditingTransactionId', @@ -294,20 +310,20 @@ describe('Send Component', function () { ) }) - it('should call updateAndSetGasTotal with the correct params if a to prop is passed', () => { - propsMethodSpies.updateAndSetGasTotal.resetHistory() + it('should call updateAndSetGasLimit with the correct params if a to prop is passed', () => { + propsMethodSpies.updateAndSetGasLimit.resetHistory() wrapper.setProps({ to: 'someAddress' }) wrapper.instance().updateGas() assert.equal( - propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, + propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0].to, 'someaddress', ) }) - it('should call updateAndSetGasTotal with to set to lowercase if passed', () => { - propsMethodSpies.updateAndSetGasTotal.resetHistory() + it('should call updateAndSetGasLimit with to set to lowercase if passed', () => { + propsMethodSpies.updateAndSetGasLimit.resetHistory() wrapper.instance().updateGas({ to: '0xABC' }) - assert.equal(propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, '0xabc') + assert.equal(propsMethodSpies.updateAndSetGasLimit.getCall(0).args[0].to, '0xabc') }) }) diff --git a/ui/app/components/send/tests/send-container.test.js b/ui/app/components/app/send/tests/send-container.test.js index 6aa4bf826..9538b67b3 100644 --- a/ui/app/components/send/tests/send-container.test.js +++ b/ui/app/components/app/send/tests/send-container.test.js @@ -47,8 +47,8 @@ proxyquire('../send.container.js', { getTokenBalance: (s) => `mockTokenBalance:${s}`, getQrCodeData: (s) => `mockQrCodeData:${s}`, }, - '../../actions': actionSpies, - '../../ducks/send.duck': duckActionSpies, + '../../../store/actions': actionSpies, + '../../../ducks/send/send.duck': duckActionSpies, './send.utils.js': { calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice, }, @@ -94,7 +94,7 @@ describe('send container', () => { mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) }) - describe('updateAndSetGasTotal()', () => { + describe('updateAndSetGasLimit()', () => { const mockProps = { blockGasLimit: 'mockBlockGasLimit', editingTransactionId: '0x2', @@ -109,7 +109,7 @@ describe('send container', () => { } it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => { - mapDispatchToPropsObject.updateAndSetGasTotal(mockProps) + mapDispatchToPropsObject.updateAndSetGasLimit(mockProps) assert(dispatchSpy.calledOnce) assert.equal( actionSpies.setGasTotal.getCall(0).args[0], @@ -118,14 +118,14 @@ describe('send container', () => { }) it('should dispatch an updateGasData action when editingTransactionId is falsy', () => { - const { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } = mockProps - mapDispatchToPropsObject.updateAndSetGasTotal( + const { gasPrice, selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } = mockProps + mapDispatchToPropsObject.updateAndSetGasLimit( Object.assign({}, mockProps, {editingTransactionId: false}) ) assert(dispatchSpy.calledOnce) assert.deepEqual( actionSpies.updateGasData.getCall(0).args[0], - { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } + { gasPrice, selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value, data } ) }) }) diff --git a/ui/app/components/send/tests/send-selectors-test-data.js b/ui/app/components/app/send/tests/send-selectors-test-data.js index 30a2666cf..d43d7c650 100644 --- a/ui/app/components/send/tests/send-selectors-test-data.js +++ b/ui/app/components/app/send/tests/send-selectors-test-data.js @@ -2,7 +2,7 @@ module.exports = { 'metamask': { 'isInitialized': true, 'isUnlocked': true, - 'featureFlags': {'betaUI': true, 'sendHexData': true}, + 'featureFlags': {'sendHexData': true}, 'rpcTarget': 'https://rawtestrpc.metamask.io/', 'identities': { '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825': { @@ -22,6 +22,7 @@ module.exports = { 'name': 'Send Account 4', }, }, + 'cachedBalances': {}, 'currentBlockGasLimit': '0x4c1878', 'currentCurrency': 'USD', 'conversionRate': 1200.88200327, diff --git a/ui/app/components/send/tests/send-selectors.test.js b/ui/app/components/app/send/tests/send-selectors.test.js index e7e901f0d..cdc86fe59 100644 --- a/ui/app/components/send/tests/send-selectors.test.js +++ b/ui/app/components/app/send/tests/send-selectors.test.js @@ -237,7 +237,7 @@ describe('send selectors', () => { it('should return the send.gasTotal', () => { assert.equal( getGasTotal(mockState), - '0xb451dc41b578' + 'a9ff56' ) }) }) diff --git a/ui/app/components/send/tests/send-utils.test.js b/ui/app/components/app/send/tests/send-utils.test.js index b72d87eee..fc4c6deed 100644 --- a/ui/app/components/send/tests/send-utils.test.js +++ b/ui/app/components/app/send/tests/send-utils.test.js @@ -9,7 +9,7 @@ import { const { addCurrencies, subtractCurrencies, -} = require('../../../conversion-util') +} = require('../../../../helpers/utils/conversion-util') const { INSUFFICIENT_FUNDS_ERROR, @@ -32,7 +32,7 @@ const stubs = { } const sendUtils = proxyquire('../send.utils.js', { - '../../conversion-util': { + '../../../helpers/utils/conversion-util': { addCurrencies: stubs.addCurrencies, conversionUtil: stubs.conversionUtil, conversionGTE: stubs.conversionGTE, @@ -40,7 +40,7 @@ const sendUtils = proxyquire('../send.utils.js', { conversionGreaterThan: stubs.conversionGreaterThan, conversionLessThan: stubs.conversionLessThan, }, - '../../token-util': { calcTokenAmount: stubs.calcTokenAmount }, + '../../../helpers/utils/token-util': { calcTokenAmount: stubs.calcTokenAmount }, 'ethereumjs-abi': { rawEncode: stubs.rawEncode, }, @@ -285,11 +285,10 @@ describe('send utils', () => { [ { value: 123, - fromNumericBase: 'dec', + fromNumericBase: 'hex', }, { value: 'calc:1610', - fromNumericBase: 'dec', }, ] ) @@ -317,6 +316,7 @@ describe('send utils', () => { from: 'mockAddress', gas: '0x64x0.95', to: '0xisContract', + value: '0xff', } beforeEach(() => { @@ -374,7 +374,7 @@ describe('send utils', () => { assert.equal(baseMockParams.estimateGasMethod.callCount, 1) assert.deepEqual( baseMockParams.estimateGasMethod.getCall(0).args[0], - { gasPrice: undefined, value: undefined, data, from: baseExpectedCall.from, gas: baseExpectedCall.gas}, + { gasPrice: undefined, value: '0xff', data, from: baseExpectedCall.from, gas: baseExpectedCall.gas}, ) assert.equal(result, '0xabc16') }) diff --git a/ui/app/components/send/to-autocomplete.component.js b/ui/app/components/app/send/to-autocomplete.component.js index 9e270db75..183967c58 100644 --- a/ui/app/components/send/to-autocomplete.component.js +++ b/ui/app/components/app/send/to-autocomplete.component.js @@ -1,7 +1,7 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import AccountListItem from '../send/account-list-item/account-list-item.component' +import AccountListItem from './account-list-item/account-list-item.component' export default class ToAutoComplete extends Component { diff --git a/ui/app/components/send/to-autocomplete/index.js b/ui/app/components/app/send/to-autocomplete/index.js index 244d301d1..244d301d1 100644 --- a/ui/app/components/send/to-autocomplete/index.js +++ b/ui/app/components/app/send/to-autocomplete/index.js diff --git a/ui/app/components/send/to-autocomplete/to-autocomplete.js b/ui/app/components/app/send/to-autocomplete/to-autocomplete.js index 39d15dfa7..d3db8cb59 100644 --- a/ui/app/components/send/to-autocomplete/to-autocomplete.js +++ b/ui/app/components/app/send/to-autocomplete/to-autocomplete.js @@ -4,8 +4,8 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const AccountListItem = require('../account-list-item/account-list-item.component').default const connect = require('react-redux').connect -const Tooltip = require('../../tooltip') -const checksumAddress = require('../../../util').checksumAddress +const Tooltip = require('../../../ui/tooltip') +const checksumAddress = require('../../../../helpers/utils/util').checksumAddress ToAutoComplete.contextTypes = { t: PropTypes.func, diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/app/shapeshift-form.js index a842bcc8b..11459fd5e 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/app/shapeshift-form.js @@ -4,12 +4,12 @@ const PropTypes = require('prop-types') const Component = require('react').Component const connect = require('react-redux').connect const classnames = require('classnames') -const { qrcode } = require('qrcode-npm') -const { shapeShiftSubview, pairUpdate, buyWithShapeShift } = require('../actions') -const { isValidAddress } = require('../util') +const qrcode = require('qrcode-generator') +const { shapeShiftSubview, pairUpdate, buyWithShapeShift } = require('../../store/actions') +const { isValidAddress } = require('../../helpers/utils/util') const SimpleDropdown = require('./dropdowns/simple-dropdown') -import Button from './button' +import Button from '../ui/button' function mapStateToProps (state) { const { diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/app/shift-list-item.js index 0461b615a..f5fa00047 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/app/shift-list-item.js @@ -3,14 +3,13 @@ const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') const connect = require('react-redux').connect -const vreme = new (require('vreme'))() const explorerLink = require('etherscan-link').createExplorerLink -const actions = require('../actions') -const addressSummary = require('../util').addressSummary +const actions = require('../../store/actions') +const { formatDate, addressSummary } = require('../../helpers/utils/util') -const CopyButton = require('./copyButton') -const EthBalance = require('./eth-balance') -const Tooltip = require('./tooltip') +const CopyButton = require('../ui/copyButton') +const EthBalance = require('../ui/eth-balance') +const Tooltip = require('../ui/tooltip') ShiftListItem.contextTypes = { @@ -67,10 +66,6 @@ ShiftListItem.prototype.render = function () { ]) } -function formatDate (date) { - return vreme.format(new Date(date), 'March 16 2014 14:30') -} - ShiftListItem.prototype.renderUtilComponents = function () { var props = this.props const { conversionRate, currentCurrency } = props diff --git a/ui/app/components/sidebars/index.js b/ui/app/components/app/sidebars/index.js index 732925f69..732925f69 100644 --- a/ui/app/components/sidebars/index.js +++ b/ui/app/components/app/sidebars/index.js diff --git a/ui/app/components/sidebars/index.scss b/ui/app/components/app/sidebars/index.scss index 5ab0664df..08181426f 100644 --- a/ui/app/components/sidebars/index.scss +++ b/ui/app/components/app/sidebars/index.scss @@ -1,3 +1,5 @@ +@import 'sidebar-content'; + .sidebar-right-enter { transition: transform 300ms ease-in-out; transform: translateX(-100%); @@ -58,6 +60,11 @@ width: 408px; left: calc(100% - 408px); } + + @media screen and (max-width: $break-small) { + width: 100%; + left: 0%; + } } .sidebar-overlay { @@ -71,4 +78,4 @@ opacity: 1; visibility: visible; background-color: rgba(0, 0, 0, .3); -}
\ No newline at end of file +} diff --git a/ui/app/components/app/sidebars/sidebar-content.scss b/ui/app/components/app/sidebars/sidebar-content.scss new file mode 100644 index 000000000..ca6b0a458 --- /dev/null +++ b/ui/app/components/app/sidebars/sidebar-content.scss @@ -0,0 +1,112 @@ +.sidebar-left { + display: flex; + + .gas-modal-page-container { + display: flex; + + .page-container { + flex: 1; + max-width: 100%; + + &__content { + display: flex; + overflow-y: initial; + } + + @media screen and (max-width: $break-small) { + max-width: 344px; + min-height: auto; + } + + @media screen and (min-width: $break-small) { + max-height: none; + } + } + + .gas-price-chart { + margin-left: 10px; + + &__root { + max-height: 160px !important; + } + } + + .page-container__bottom { + display: flex; + flex-direction: column; + flex-flow: space-between; + height: 100%; + } + + .page-container__content { + overflow-y: inherit; + } + + .basic-tab-content { + height: auto; + margin-bottom: 0px; + border-bottom: 1px solid #d2d8dd; + flex: 1 1 70%; + + @media screen and (max-width: $break-small) { + padding-left: 14px; + padding-bottom: 21px; + } + + .gas-price-button-group--alt { + @media screen and (max-width: $break-small) { + max-width: 318px; + + &__time-estimate { + font-size: 12px; + } + } + } + } + + .advanced-tab { + @media screen and (min-width: $break-small) { + flex: 1 1 70%; + } + + &__fee-chart { + height: 320px; + + @media screen and (max-width: $break-small) { + height: initial; + } + } + + &__fee-chart__speed-buttons { + bottom: 77px; + + @media screen and (max-width: $break-small) { + display: none; + } + } + } + + .gas-modal-content { + display: flex; + flex-direction: column; + width: 100%; + + &__info-row-wrapper { + display: flex; + @media screen and (min-width: $break-small) { + flex: 1 1 30%; + } + } + + &__info-row { + height: 170px; + + @media screen and (max-width: $break-small) { + height: initial; + display: flex; + justify-content: center; + } + } + } + } +}
\ No newline at end of file diff --git a/ui/app/components/app/sidebars/sidebar.component.js b/ui/app/components/app/sidebars/sidebar.component.js new file mode 100644 index 000000000..b9e0f9e81 --- /dev/null +++ b/ui/app/components/app/sidebars/sidebar.component.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import ReactCSSTransitionGroup from 'react-addons-css-transition-group' +import WalletView from '../wallet-view' +import { WALLET_VIEW_SIDEBAR } from './sidebar.constants' +import CustomizeGas from '../gas-customization/gas-modal-page-container/' + +export default class Sidebar extends Component { + + static propTypes = { + sidebarOpen: PropTypes.bool, + hideSidebar: PropTypes.func, + sidebarShouldClose: PropTypes.bool, + transitionName: PropTypes.string, + type: PropTypes.string, + sidebarProps: PropTypes.object, + onOverlayClose: PropTypes.func, + }; + + renderOverlay () { + const { onOverlayClose } = this.props + + return <div + className="sidebar-overlay" + onClick={() => { + onOverlayClose && onOverlayClose() + this.props.hideSidebar() + } + } /> + } + + renderSidebarContent () { + const { type, sidebarProps = {} } = this.props + const { transaction = {} } = sidebarProps + switch (type) { + case WALLET_VIEW_SIDEBAR: + return <WalletView responsiveDisplayClassname={'sidebar-right' } /> + case 'customize-gas': + return <div className={'sidebar-left'}><CustomizeGas transaction={transaction} /></div> + default: + return null + } + + } + + componentDidUpdate (prevProps) { + if (!prevProps.sidebarShouldClose && this.props.sidebarShouldClose) { + this.props.hideSidebar() + } + } + + render () { + const { transitionName, sidebarOpen, sidebarShouldClose } = this.props + + return ( + <div> + <ReactCSSTransitionGroup + transitionName={transitionName} + transitionEnterTimeout={300} + transitionLeaveTimeout={200} + > + { sidebarOpen && !sidebarShouldClose ? this.renderSidebarContent() : null } + </ReactCSSTransitionGroup> + { sidebarOpen && !sidebarShouldClose ? this.renderOverlay() : null } + </div> + ) + } + +} diff --git a/ui/app/components/sidebars/sidebar.constants.js b/ui/app/components/app/sidebars/sidebar.constants.js index 1613a8245..1613a8245 100644 --- a/ui/app/components/sidebars/sidebar.constants.js +++ b/ui/app/components/app/sidebars/sidebar.constants.js diff --git a/ui/app/components/sidebars/tests/sidebars-component.test.js b/ui/app/components/app/sidebars/tests/sidebars-component.test.js index e2d77518a..cee22aca8 100644 --- a/ui/app/components/sidebars/tests/sidebars-component.test.js +++ b/ui/app/components/app/sidebars/tests/sidebars-component.test.js @@ -6,6 +6,7 @@ import ReactCSSTransitionGroup from 'react-addons-css-transition-group' import Sidebar from '../sidebar.component.js' import WalletView from '../../wallet-view' +import CustomizeGas from '../../gas-customization/gas-modal-page-container/' const propsMethodSpies = { hideSidebar: sinon.spy(), @@ -59,6 +60,14 @@ describe('Sidebar Component', function () { assert.equal(renderSidebarContent.props.responsiveDisplayClassname, 'sidebar-right') }) + it('should render sidebar content with the correct props', () => { + wrapper.setProps({ type: 'customize-gas' }) + renderSidebarContent = wrapper.instance().renderSidebarContent() + const renderedSidebarContent = shallow(renderSidebarContent) + assert(renderedSidebarContent.hasClass('sidebar-left')) + assert(renderedSidebarContent.childAt(0).is(CustomizeGas)) + }) + it('should not render with an unrecognized type', () => { wrapper.setProps({ type: 'foobar' }) renderSidebarContent = wrapper.instance().renderSidebarContent() diff --git a/ui/app/components/signature-request.js b/ui/app/components/app/signature-request.js index 85af3b00b..e47791b67 100644 --- a/ui/app/components/signature-request.js +++ b/ui/app/components/app/signature-request.js @@ -2,7 +2,9 @@ const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits -import Identicon from './identicon' +import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums' +import { getEnvironmentType } from '../../../../app/scripts/lib/util' +import Identicon from '../ui/identicon' const connect = require('react-redux').connect const ethUtil = require('ethereumjs-util') const classnames = require('classnames') @@ -10,10 +12,10 @@ const { compose } = require('recompose') const { withRouter } = require('react-router-dom') const { ObjectInspector } = require('react-inspector') -const AccountDropdownMini = require('./dropdowns/account-dropdown-mini') +import AccountDropdownMini from '../ui/account-dropdown-mini' -const actions = require('../actions') -const { conversionUtil } = require('../conversion-util') +const actions = require('../../store/actions') +const { conversionUtil } = require('../../helpers/utils/conversion-util') const { getSelectedAccount, @@ -21,12 +23,12 @@ const { getSelectedAddress, accountsWithSendEtherInfoSelector, conversionRateSelector, -} = require('../selectors.js') +} = require('../../selectors/selectors.js') -import { clearConfirmTransaction } from '../ducks/confirm-transaction.duck' -import Button from './button' +import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck' +import Button from '../ui/button' -const { DEFAULT_ROUTE } = require('../routes') +const { DEFAULT_ROUTE } = require('../../helpers/constants/routes') function mapStateToProps (state) { return { @@ -47,13 +49,50 @@ function mapDispatchToProps (dispatch) { } } +function mergeProps (stateProps, dispatchProps, ownProps) { + const { + signPersonalMessage, + signTypedMessage, + cancelPersonalMessage, + cancelTypedMessage, + signMessage, + cancelMessage, + txData, + } = ownProps + + const { type } = txData + + let cancel + let sign + if (type === 'personal_sign') { + cancel = cancelPersonalMessage + sign = signPersonalMessage + } else if (type === 'eth_signTypedData') { + cancel = cancelTypedMessage + sign = signTypedMessage + } else if (type === 'eth_sign') { + cancel = cancelMessage + sign = signMessage + } + + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + txData, + cancel, + sign, + } +} + SignatureRequest.contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } module.exports = compose( withRouter, - connect(mapStateToProps, mapDispatchToProps) + connect(mapStateToProps, mapDispatchToProps, mergeProps) )(SignatureRequest) @@ -63,7 +102,24 @@ function SignatureRequest (props) { this.state = { selectedAccount: props.selectedAccount, - accountDropdownOpen: false, + } +} + +SignatureRequest.prototype.componentDidMount = function () { + const { clearConfirmTransaction, cancel } = this.props + const { metricsEvent } = this.context + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + window.onbeforeunload = event => { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Sign Request', + name: 'Cancel Sig Request Via Notification Close', + }, + }) + clearConfirmTransaction() + cancel(event) + } } } @@ -82,10 +138,7 @@ SignatureRequest.prototype.renderHeader = function () { } SignatureRequest.prototype.renderAccountDropdown = function () { - const { - selectedAccount, - accountDropdownOpen, - } = this.state + const { selectedAccount } = this.state const { accounts, @@ -98,10 +151,7 @@ SignatureRequest.prototype.renderAccountDropdown = function () { h(AccountDropdownMini, { selectedAccount, accounts, - onSelect: selectedAccount => this.setState({ selectedAccount }), - dropdownOpen: accountDropdownOpen, - openDropdown: () => this.setState({ accountDropdownOpen: true }), - closeDropdown: () => this.setState({ accountDropdownOpen: false }), + disabled: true, }), ]) @@ -164,7 +214,7 @@ SignatureRequest.prototype.msgHexToText = function (hex) { try { const stripped = ethUtil.stripHexPrefix(hex) const buff = Buffer.from(stripped, 'hex') - return buff.toString('utf8') + return buff.length === 32 ? hex : buff.toString('utf8') } catch (e) { return hex } @@ -239,30 +289,7 @@ SignatureRequest.prototype.renderBody = function () { } SignatureRequest.prototype.renderFooter = function () { - const { - signPersonalMessage, - signTypedMessage, - cancelPersonalMessage, - cancelTypedMessage, - signMessage, - cancelMessage, - } = this.props - - const { txData } = this.props - const { type } = txData - - let cancel - let sign - if (type === 'personal_sign') { - cancel = cancelPersonalMessage - sign = signPersonalMessage - } else if (type === 'eth_signTypedData') { - cancel = cancelTypedMessage - sign = signTypedMessage - } else if (type === 'eth_sign') { - cancel = cancelMessage - sign = signMessage - } + const { cancel, sign } = this.props return h('div.request-signature__footer', [ h(Button, { @@ -271,6 +298,13 @@ SignatureRequest.prototype.renderFooter = function () { className: 'request-signature__footer__cancel-button', onClick: event => { cancel(event).then(() => { + this.context.metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Sign Request', + name: 'Cancel', + }, + }) this.props.clearConfirmTransaction() this.props.history.push(DEFAULT_ROUTE) }) @@ -279,8 +313,16 @@ SignatureRequest.prototype.renderFooter = function () { h(Button, { type: 'primary', large: true, + className: 'request-signature__footer__sign-button', onClick: event => { sign(event).then(() => { + this.context.metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Sign Request', + name: 'Confirm', + }, + }) this.props.clearConfirmTransaction() this.props.history.push(DEFAULT_ROUTE) }) diff --git a/ui/app/components/app/tab-bar.js b/ui/app/components/app/tab-bar.js new file mode 100644 index 000000000..43923989a --- /dev/null +++ b/ui/app/components/app/tab-bar.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react' +const PropTypes = require('prop-types') +const classnames = require('classnames') + +class TabBar extends Component { + render () { + const { tabs = [], onSelect, isActive } = this.props + + return ( + <div className="tab-bar"> + {tabs.map(({ key, content, description }) => ( + <div + key={key} + className={classnames('tab-bar__tab pointer', { + 'tab-bar__tab--active': isActive(key, content), + })} + onClick={() => onSelect(key)} + > + <div className="tab-bar__tab__content"> + <div className="tab-bar__tab__content__title">{content}</div> + <div className="tab-bar__tab__content__description">{description}</div> + </div> + <div className="tab-bar__tab__caret" /> + </div> + ))} + </div> + ) + } +} + +TabBar.propTypes = { + isActive: PropTypes.func.isRequired, + tabs: PropTypes.array, + onSelect: PropTypes.func, +} + +module.exports = TabBar diff --git a/ui/app/components/token-cell.js b/ui/app/components/app/token-cell.js index 75ba347fa..cef809e8a 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/app/token-cell.js @@ -1,12 +1,13 @@ const Component = require('react').Component +const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -import Identicon from './identicon' -const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') -const selectors = require('../selectors') -const actions = require('../actions') -const { conversionUtil, multiplyCurrencies } = require('../conversion-util') +import Identicon from '../ui/identicon' +const prefixForNetwork = require('../../../lib/etherscan-prefix-for-network') +const selectors = require('../../selectors/selectors') +const actions = require('../../store/actions') +const { conversionUtil, multiplyCurrencies } = require('../../helpers/utils/conversion-util') const TokenMenuDropdown = require('./dropdowns/token-menu-dropdown.js') @@ -40,6 +41,10 @@ function TokenCell () { } } +TokenCell.contextTypes = { + metricsEvent: PropTypes.func, +} + TokenCell.prototype.render = function () { const { tokenMenuOpen } = this.state const props = this.props @@ -88,6 +93,13 @@ TokenCell.prototype.render = function () { // onClick: this.view.bind(this, address, userAddress, network), onClick: () => { setSelectedToken(address) + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Token Menu', + name: 'Clicked Token', + }, + }) selectedTokenAddress !== address && sidebarOpen && hideSidebar() }, }, [ diff --git a/ui/app/components/token-list.js b/ui/app/components/app/token-list.js index 6a88f30bf..2188e7020 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/app/token-list.js @@ -5,7 +5,7 @@ const inherits = require('util').inherits const TokenTracker = require('eth-token-tracker') const TokenCell = require('./token-cell.js') const connect = require('react-redux').connect -const selectors = require('../selectors') +const selectors = require('../../selectors/selectors') const log = require('loglevel') function mapStateToProps (state) { @@ -134,17 +134,17 @@ TokenList.prototype.createFreshTokenTracker = function () { }) } -TokenList.prototype.componentDidUpdate = function (nextProps) { +TokenList.prototype.componentDidUpdate = function (prevProps) { const { network: oldNet, userAddress: oldAddress, tokens, - } = this.props + } = prevProps const { network: newNet, userAddress: newAddress, tokens: newTokens, - } = nextProps + } = this.props const isLoading = newNet === 'loading' const missingInfo = !oldNet || !newNet || !oldAddress || !newAddress diff --git a/ui/app/components/transaction-action/index.js b/ui/app/components/app/transaction-action/index.js index a6e9097f1..a6e9097f1 100644 --- a/ui/app/components/transaction-action/index.js +++ b/ui/app/components/app/transaction-action/index.js diff --git a/ui/app/components/transaction-action/tests/transaction-action.component.test.js b/ui/app/components/app/transaction-action/tests/transaction-action.component.test.js index b22a9db39..b22a9db39 100644 --- a/ui/app/components/transaction-action/tests/transaction-action.component.test.js +++ b/ui/app/components/app/transaction-action/tests/transaction-action.component.test.js diff --git a/ui/app/components/transaction-action/transaction-action.component.js b/ui/app/components/app/transaction-action/transaction-action.component.js index 1de91cb71..4a5efdaae 100644 --- a/ui/app/components/transaction-action/transaction-action.component.js +++ b/ui/app/components/app/transaction-action/transaction-action.component.js @@ -1,8 +1,8 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { getTransactionActionKey } from '../../helpers/transactions.util' -import { camelCaseToCapitalize } from '../../helpers/common.util' +import { getTransactionActionKey } from '../../../helpers/utils/transactions.util' +import { camelCaseToCapitalize } from '../../../helpers/utils/common.util' export default class TransactionAction extends PureComponent { static contextTypes = { diff --git a/ui/app/components/transaction-activity-log/index.js b/ui/app/components/app/transaction-activity-log/index.js index a33da15a3..a33da15a3 100644 --- a/ui/app/components/transaction-activity-log/index.js +++ b/ui/app/components/app/transaction-activity-log/index.js diff --git a/ui/app/components/transaction-activity-log/index.scss b/ui/app/components/app/transaction-activity-log/index.scss index 27f3006b3..00c17e6aa 100644 --- a/ui/app/components/transaction-activity-log/index.scss +++ b/ui/app/components/app/transaction-activity-log/index.scss @@ -1,7 +1,8 @@ .transaction-activity-log { - &__card { - background: $white; - height: 100%; + &__title { + border-bottom: 1px solid #d8d8d8; + padding-bottom: 4px; + text-transform: capitalize; } &__activities-container { @@ -21,8 +22,8 @@ left: 0; top: 0; height: 100%; - width: 6px; - border-right: 1px solid $scorpion; + width: 7px; + border-right: 1px solid #909090; } &:first-child::after { @@ -40,22 +41,25 @@ } &__activity-icon { - width: 13px; - height: 13px; + width: 15px; + height: 15px; margin-right: 6px; border-radius: 50%; - background: $scorpion; + background: #909090; flex: 0 0 auto; + display: flex; + justify-content: center; + align-items: center; + z-index: 1; } &__activity-text { - color: $scorpion; + color: $dusty-gray; font-size: .75rem; + cursor: pointer; - @media screen and (min-width: $break-large) { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + &:hover { + color: $black; } } @@ -64,6 +68,16 @@ font-weight: 500; } + &__entry-container { + min-width: 0; + } + + &__action-link { + font-size: .75rem; + cursor: pointer; + color: $curious-blue; + } + b { font-weight: 500; } diff --git a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js new file mode 100644 index 000000000..a2946e53d --- /dev/null +++ b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.component.test.js @@ -0,0 +1,101 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import TransactionActivityLog from '../transaction-activity-log.component' + +describe('TransactionActivityLog Component', () => { + it('should render properly', () => { + const activities = [ + { + eventKey: 'transactionCreated', + hash: '0xe46c7f9b39af2fbf1c53e66f72f80343ab54c2c6dba902d51fb98ada08fe1a63', + id: 2005383477493174, + timestamp: 1543957986150, + value: '0x2386f26fc10000', + }, { + eventKey: 'transactionSubmitted', + hash: '0xe46c7f9b39af2fbf1c53e66f72f80343ab54c2c6dba902d51fb98ada08fe1a63', + id: 2005383477493174, + timestamp: 1543957987853, + value: '0x1319718a5000', + }, { + eventKey: 'transactionResubmitted', + hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87', + id: 2005383477493175, + timestamp: 1543957991563, + value: '0x1502634b5800', + }, { + eventKey: 'transactionConfirmed', + hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87', + id: 2005383477493175, + timestamp: 1543958029960, + value: '0x1502634b5800', + }, + ] + + const wrapper = shallow( + <TransactionActivityLog + activities={activities} + className="test-class" + inlineRetryIndex={-1} + inlineCancelIndex={-1} + nativeCurrency="ETH" + onCancel={() => {}} + onRetry={() => {}} + primaryTransactionStatus="confirmed" + />, + { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } + ) + + assert.ok(wrapper.hasClass('transaction-activity-log')) + assert.ok(wrapper.hasClass('test-class')) + }) + + it('should render inline retry and cancel buttons', () => { + const activities = [ + { + eventKey: 'transactionCreated', + hash: '0xa', + id: 1, + timestamp: 1, + value: '0x1', + }, { + eventKey: 'transactionSubmitted', + hash: '0xa', + id: 1, + timestamp: 2, + value: '0x1', + }, { + eventKey: 'transactionResubmitted', + hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87', + id: 2, + timestamp: 3, + value: '0x1', + }, { + eventKey: 'transactionCancelAttempted', + hash: '0x7d09d337fc6f5d6fe2dbf3a6988d69532deb0a82b665f9180b5a20db377eea87', + id: 3, + timestamp: 4, + value: '0x1', + }, + ] + + const wrapper = shallow( + <TransactionActivityLog + activities={activities} + className="test-class" + inlineRetryIndex={2} + inlineCancelIndex={3} + nativeCurrency="ETH" + onCancel={() => {}} + onRetry={() => {}} + primaryTransactionStatus="pending" + />, + { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } + ) + + assert.ok(wrapper.hasClass('transaction-activity-log')) + assert.ok(wrapper.hasClass('test-class')) + assert.equal(wrapper.find('.transaction-activity-log__action-link').length, 2) + }) +}) diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js index a7c35f51e..a7c35f51e 100644 --- a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js +++ b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js diff --git a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js new file mode 100644 index 000000000..d014b8886 --- /dev/null +++ b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js @@ -0,0 +1,335 @@ +import assert from 'assert' +import { combineTransactionHistories, getActivities } from '../transaction-activity-log.util' + +describe('combineTransactionHistories', () => { + it('should return no activites for an empty list of transactions', () => { + assert.deepEqual(combineTransactionHistories([]), []) + }) + + it('should return activities for an array of transactions', () => { + const transactions = [ + { + estimatedGas: '0x5208', + hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3', + history: [ + { + 'id': 6400627574331058, + 'time': 1543958845581, + 'status': 'unapproved', + 'metamaskNetworkId': '3', + 'loadingDefaults': true, + 'txParams': { + 'from': '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', + 'to': '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', + 'value': '0x2386f26fc10000', + 'gas': '0x5208', + 'gasPrice': '0x3b9aca00', + }, + 'type': 'standard', + }, + [{ 'op': 'replace', 'path': '/status', 'value': 'approved', 'note': 'txStateManager: setting status to approved', 'timestamp': 1543958847813 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'submitted', 'note': 'txStateManager: setting status to submitted', 'timestamp': 1543958848147 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'dropped', 'note': 'txStateManager: setting status to dropped', 'timestamp': 1543958897181 }, { 'op': 'add', 'path': '/replacedBy', 'value': '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33' }], + ], + id: 6400627574331058, + loadingDefaults: false, + metamaskNetworkId: '3', + status: 'dropped', + submittedTime: 1543958848135, + time: 1543958845581, + txParams: { + from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', + gas: '0x5208', + gasPrice: '0x3b9aca00', + nonce: '0x32', + to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', + value: '0x2386f26fc10000', + }, + type: 'standard', + }, { + hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33', + history: [ + { + 'id': 6400627574331060, + 'time': 1543958857697, + 'status': 'unapproved', + 'metamaskNetworkId': '3', + 'loadingDefaults': false, + 'txParams': { + 'from': '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', + 'to': '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', + 'value': '0x2386f26fc10000', + 'gas': '0x5208', + 'gasPrice': '0x3b9aca00', + 'nonce': '0x32', + }, + 'lastGasPrice': '0x4190ab00', + 'type': 'retry', + }, + [{ 'op': 'replace', 'path': '/txParams/gasPrice', 'value': '0x481f2280', 'note': 'confTx: user approved transaction', 'timestamp': 1543958859470 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'approved', 'note': 'txStateManager: setting status to approved', 'timestamp': 1543958859485 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'signed', 'note': 'transactions#publishTransaction', 'timestamp': 1543958859889 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'submitted', 'note': 'txStateManager: setting status to submitted', 'timestamp': 1543958860061 }], [{ 'op': 'add', 'path': '/firstRetryBlockNumber', 'value': '0x45a0fd', 'note': 'transactions/pending-tx-tracker#event: tx:block-update', 'timestamp': 1543958896466 }], + [{ 'op': 'replace', 'path': '/status', 'value': 'confirmed', 'timestamp': 1543958897165 }], + ], + id: 6400627574331060, + lastGasPrice: '0x4190ab00', + loadingDefaults: false, + metamaskNetworkId: '3', + status: 'confirmed', + submittedTime: 1543958860054, + time: 1543958857697, + txParams: { + from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', + gas: '0x5208', + gasPrice: '0x481f2280', + nonce: '0x32', + to: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6', + value: '0x2386f26fc10000', + }, + txReceipt: { + status: '0x1', + }, + type: 'retry', + }, + ] + + const expected = [ + { + id: 6400627574331058, + hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3', + eventKey: 'transactionCreated', + timestamp: 1543958845581, + value: '0x2386f26fc10000', + }, { + id: 6400627574331058, + hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3', + eventKey: 'transactionSubmitted', + timestamp: 1543958848147, + value: '0x1319718a5000', + }, { + id: 6400627574331060, + hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33', + eventKey: 'transactionResubmitted', + timestamp: 1543958860061, + value: '0x171c3a061400', + }, { + id: 6400627574331060, + hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33', + eventKey: 'transactionConfirmed', + timestamp: 1543958897165, + value: '0x171c3a061400', + }, + ] + + assert.deepEqual(combineTransactionHistories(transactions), expected) + }) +}) + +describe('getActivities', () => { + it('should return no activities for an empty history', () => { + const transaction = { + history: [], + id: 1, + status: 'confirmed', + txParams: { + from: '0x1', + gas: '0x5208', + gasPrice: '0x3b9aca00', + nonce: '0xa4', + to: '0x2', + value: '0x2386f26fc10000', + }, + } + + assert.deepEqual(getActivities(transaction), []) + }) + + it('should return activities for a transaction\'s history', () => { + const transaction = { + history: [ + { + id: 5559712943815343, + loadingDefaults: true, + metamaskNetworkId: '3', + status: 'unapproved', + time: 1535507561452, + txParams: { + from: '0x1', + gas: '0x5208', + gasPrice: '0x3b9aca00', + nonce: '0xa4', + to: '0x2', + value: '0x2386f26fc10000', + }, + }, + [ + { + op: 'replace', + path: '/loadingDefaults', + timestamp: 1535507561515, + value: false, + }, + { + op: 'add', + path: '/gasPriceSpecified', + value: true, + }, + { + op: 'add', + path: '/gasLimitSpecified', + value: true, + }, + { + op: 'add', + path: '/estimatedGas', + value: '0x5208', + }, + ], + [ + { + note: '#newUnapprovedTransaction - adding the origin', + op: 'add', + path: '/origin', + timestamp: 1535507561516, + value: 'MetaMask', + }, + [], + ], + [ + { + note: 'confTx: user approved transaction', + op: 'replace', + path: '/txParams/gasPrice', + timestamp: 1535664571504, + value: '0x77359400', + }, + ], + [ + { + note: 'txStateManager: setting status to approved', + op: 'replace', + path: '/status', + timestamp: 1535507564302, + value: 'approved', + }, + ], + [ + { + note: 'transactions#approveTransaction', + op: 'add', + path: '/txParams/nonce', + timestamp: 1535507564439, + value: '0xa4', + }, + { + op: 'add', + path: '/nonceDetails', + value: { + local: {}, + network: {}, + params: {}, + }, + }, + ], + [ + { + note: 'transactions#publishTransaction', + op: 'replace', + path: '/status', + timestamp: 1535507564518, + value: 'signed', + }, + { + op: 'add', + path: '/rawTx', + value: '0xf86b81a4843b9aca008252089450a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706872386f26fc10000802aa007b30119fc4fc5954fad727895b7e3ba80a78d197e95703cc603bcf017879151a01c50beda40ffaee541da9c05b9616247074f25f392800e0ad6c7a835d5366edf', + }, + ], + [], + [ + { + note: 'transactions#setTxHash', + op: 'add', + path: '/hash', + timestamp: 1535507564658, + value: '0x7acc4987b5c0dfa8d423798a8c561138259de1f98a62e3d52e7e83c0e0dd9fb7', + }, + ], + [ + { + note: 'txStateManager - add submitted time stamp', + op: 'add', + path: '/submittedTime', + timestamp: 1535507564660, + value: 1535507564660, + }, + ], + [ + { + note: 'txStateManager: setting status to submitted', + op: 'replace', + path: '/status', + timestamp: 1535507564665, + value: 'submitted', + }, + ], + [ + { + note: 'transactions/pending-tx-tracker#event: tx:block-update', + op: 'add', + path: '/firstRetryBlockNumber', + timestamp: 1535507575476, + value: '0x3bf624', + }, + ], + [ + { + note: 'txStateManager: setting status to confirmed', + op: 'replace', + path: '/status', + timestamp: 1535507615993, + value: 'confirmed', + }, + ], + ], + id: 1, + status: 'confirmed', + txParams: { + from: '0x1', + gas: '0x5208', + gasPrice: '0x3b9aca00', + nonce: '0xa4', + to: '0x2', + value: '0x2386f26fc10000', + }, + hash: '0xabc', + } + + const expectedResult = [ + { + 'eventKey': 'transactionCreated', + 'timestamp': 1535507561452, + 'value': '0x2386f26fc10000', + 'id': 1, + 'hash': '0xabc', + }, + { + 'eventKey': 'transactionSubmitted', + 'timestamp': 1535507564665, + 'value': '0x2632e314a000', + 'id': 1, + 'hash': '0xabc', + }, + { + 'eventKey': 'transactionConfirmed', + 'timestamp': 1535507615993, + 'value': '0x2632e314a000', + 'id': 1, + 'hash': '0xabc', + }, + ] + + assert.deepEqual(getActivities(transaction, true), expectedResult) + }) +}) diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/index.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/index.js new file mode 100644 index 000000000..86b12360a --- /dev/null +++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/index.js @@ -0,0 +1 @@ +export { default } from './transaction-activity-log-icon.component' diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js new file mode 100644 index 000000000..871716002 --- /dev/null +++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log-icon/transaction-activity-log-icon.component.js @@ -0,0 +1,55 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +import { + TRANSACTION_CREATED_EVENT, + TRANSACTION_SUBMITTED_EVENT, + TRANSACTION_RESUBMITTED_EVENT, + TRANSACTION_CONFIRMED_EVENT, + TRANSACTION_DROPPED_EVENT, + TRANSACTION_ERRORED_EVENT, + TRANSACTION_CANCEL_ATTEMPTED_EVENT, + TRANSACTION_CANCEL_SUCCESS_EVENT, +} from '../transaction-activity-log.constants' + +const imageHash = { + [TRANSACTION_CREATED_EVENT]: '/images/icons/new.svg', + [TRANSACTION_SUBMITTED_EVENT]: '/images/icons/submitted.svg', + [TRANSACTION_RESUBMITTED_EVENT]: '/images/icons/retry.svg', + [TRANSACTION_CONFIRMED_EVENT]: '/images/icons/confirm.svg', + [TRANSACTION_DROPPED_EVENT]: '/images/icons/cancelled.svg', + [TRANSACTION_ERRORED_EVENT]: '/images/icons/error.svg', + [TRANSACTION_CANCEL_ATTEMPTED_EVENT]: '/images/icons/cancelled.svg', + [TRANSACTION_CANCEL_SUCCESS_EVENT]: '/images/icons/cancelled.svg', +} + +export default class TransactionActivityLogIcon extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + className: PropTypes.string, + eventKey: PropTypes.oneOf(Object.keys(imageHash)), + } + + render () { + const { className, eventKey } = this.props + const imagePath = imageHash[eventKey] + + return ( + <div className={classnames('transaction-activity-log-icon', className)}> + { + imagePath && ( + <img + src={imagePath} + height={9} + width={9} + /> + ) + } + </div> + ) + } +} diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js new file mode 100644 index 000000000..de4d29750 --- /dev/null +++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js @@ -0,0 +1,131 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { getEthConversionFromWeiHex, getValueFromWeiHex } from '../../../helpers/utils/conversions.util' +import { formatDate } from '../../../helpers/utils/util' +import TransactionActivityLogIcon from './transaction-activity-log-icon' +import { CONFIRMED_STATUS } from './transaction-activity-log.constants' +import prefixForNetwork from '../../../../lib/etherscan-prefix-for-network' + +export default class TransactionActivityLog extends PureComponent { + static contextTypes = { + t: PropTypes.func, + metricEvent: PropTypes.func, + } + + static propTypes = { + activities: PropTypes.array, + className: PropTypes.string, + conversionRate: PropTypes.number, + inlineRetryIndex: PropTypes.number, + inlineCancelIndex: PropTypes.number, + nativeCurrency: PropTypes.string, + onCancel: PropTypes.func, + onRetry: PropTypes.func, + primaryTransaction: PropTypes.object, + } + + handleActivityClick = hash => { + const { primaryTransaction } = this.props + const { metamaskNetworkId } = primaryTransaction + + const prefix = prefixForNetwork(metamaskNetworkId) + const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}` + + global.platform.openWindow({ url: etherscanUrl }) + } + + renderInlineRetry (index, activity) { + const { t } = this.context + const { inlineRetryIndex, primaryTransaction = {}, onRetry } = this.props + const { status } = primaryTransaction + const { id } = activity + + return status !== CONFIRMED_STATUS && index === inlineRetryIndex + ? ( + <div + className="transaction-activity-log__action-link" + onClick={() => onRetry(id)} + > + { t('speedUpTransaction') } + </div> + ) : null + } + + renderInlineCancel (index, activity) { + const { t } = this.context + const { inlineCancelIndex, primaryTransaction = {}, onCancel } = this.props + const { status } = primaryTransaction + const { id } = activity + + return status !== CONFIRMED_STATUS && index === inlineCancelIndex + ? ( + <div + className="transaction-activity-log__action-link" + onClick={() => onCancel(id)} + > + { t('speedUpCancellation') } + </div> + ) : null + } + + renderActivity (activity, index) { + const { conversionRate, nativeCurrency } = this.props + const { eventKey, value, timestamp, hash } = activity + const ethValue = index === 0 + ? `${getValueFromWeiHex({ + value, + fromCurrency: nativeCurrency, + toCurrency: nativeCurrency, + conversionRate, + numberOfDecimals: 6, + })} ${nativeCurrency}` + : getEthConversionFromWeiHex({ + value, + fromCurrency: nativeCurrency, + conversionRate, + numberOfDecimals: 3, + }) + const formattedTimestamp = formatDate(timestamp, 'T \'on\' M/d/y') + const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp]) + + return ( + <div + key={index} + className="transaction-activity-log__activity" + > + <TransactionActivityLogIcon + className="transaction-activity-log__activity-icon" + eventKey={eventKey} + /> + <div className="transaction-activity-log__entry-container"> + <div + className="transaction-activity-log__activity-text" + title={activityText} + onClick={() => this.handleActivityClick(hash)} + > + { activityText } + </div> + { this.renderInlineRetry(index, activity) } + { this.renderInlineCancel(index, activity) } + </div> + </div> + ) + } + + render () { + const { t } = this.context + const { className, activities } = this.props + + return ( + <div className={classnames('transaction-activity-log', className)}> + <div className="transaction-activity-log__title"> + { t('activityLog') } + </div> + <div className="transaction-activity-log__activities-container"> + { activities.map((activity, index) => this.renderActivity(activity, index)) } + </div> + </div> + ) + } +} diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log.constants.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.constants.js new file mode 100644 index 000000000..72e63d85c --- /dev/null +++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.constants.js @@ -0,0 +1,13 @@ +export const TRANSACTION_CREATED_EVENT = 'transactionCreated' +export const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted' +export const TRANSACTION_RESUBMITTED_EVENT = 'transactionResubmitted' +export const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed' +export const TRANSACTION_DROPPED_EVENT = 'transactionDropped' +export const TRANSACTION_UPDATED_EVENT = 'transactionUpdated' +export const TRANSACTION_ERRORED_EVENT = 'transactionErrored' +export const TRANSACTION_CANCEL_ATTEMPTED_EVENT = 'transactionCancelAttempted' +export const TRANSACTION_CANCEL_SUCCESS_EVENT = 'transactionCancelSuccess' + +export const SUBMITTED_STATUS = 'submitted' +export const CONFIRMED_STATUS = 'confirmed' +export const DROPPED_STATUS = 'dropped' diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log.container.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.container.js new file mode 100644 index 000000000..11b20f245 --- /dev/null +++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.container.js @@ -0,0 +1,44 @@ +import { connect } from 'react-redux' +import R from 'ramda' +import TransactionActivityLog from './transaction-activity-log.component' +import { conversionRateSelector, getNativeCurrency } from '../../../selectors/selectors' +import { combineTransactionHistories } from './transaction-activity-log.util' +import { + TRANSACTION_RESUBMITTED_EVENT, + TRANSACTION_CANCEL_ATTEMPTED_EVENT, +} from './transaction-activity-log.constants' + +const matchesEventKey = matchEventKey => ({ eventKey }) => eventKey === matchEventKey + +const mapStateToProps = state => { + return { + conversionRate: conversionRateSelector(state), + nativeCurrency: getNativeCurrency(state), + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { + transactionGroup: { + transactions = [], + primaryTransaction, + } = {}, + ...restOwnProps + } = ownProps + + const activities = combineTransactionHistories(transactions) + const inlineRetryIndex = R.findLastIndex(matchesEventKey(TRANSACTION_RESUBMITTED_EVENT))(activities) + const inlineCancelIndex = R.findLastIndex(matchesEventKey(TRANSACTION_CANCEL_ATTEMPTED_EVENT))(activities) + + return { + ...stateProps, + ...dispatchProps, + ...restOwnProps, + activities, + inlineRetryIndex, + inlineCancelIndex, + primaryTransaction, + } +} + +export default connect(mapStateToProps, null, mergeProps)(TransactionActivityLog) diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log.util.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.util.js new file mode 100644 index 000000000..b74513879 --- /dev/null +++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.util.js @@ -0,0 +1,233 @@ +import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util' + +// path constants +const STATUS_PATH = '/status' +const GAS_PRICE_PATH = '/txParams/gasPrice' +const GAS_LIMIT_PATH = '/txParams/gas' + +// op constants +const REPLACE_OP = 'replace' + +import { + // event constants + TRANSACTION_CREATED_EVENT, + TRANSACTION_SUBMITTED_EVENT, + TRANSACTION_RESUBMITTED_EVENT, + TRANSACTION_CONFIRMED_EVENT, + TRANSACTION_DROPPED_EVENT, + TRANSACTION_UPDATED_EVENT, + TRANSACTION_ERRORED_EVENT, + TRANSACTION_CANCEL_ATTEMPTED_EVENT, + TRANSACTION_CANCEL_SUCCESS_EVENT, + // status constants + SUBMITTED_STATUS, + CONFIRMED_STATUS, + DROPPED_STATUS, +} from './transaction-activity-log.constants' + +import { + TRANSACTION_TYPE_CANCEL, + TRANSACTION_TYPE_RETRY, +} from '../../../../../app/scripts/controllers/transactions/enums' + +const eventPathsHash = { + [STATUS_PATH]: true, + [GAS_PRICE_PATH]: true, + [GAS_LIMIT_PATH]: true, +} + +const statusHash = { + [SUBMITTED_STATUS]: TRANSACTION_SUBMITTED_EVENT, + [CONFIRMED_STATUS]: TRANSACTION_CONFIRMED_EVENT, + [DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT, +} + +/** + * @name getActivities + * @param {Object} transaction - txMeta object + * @param {boolean} isFirstTransaction - True if the transaction is the first created transaction + * in the list of transactions with the same nonce. If so, we use this transaction to create the + * transactionCreated activity. + * @returns {Array} + */ +export function getActivities (transaction, isFirstTransaction = false) { + const { + id, + hash, + history = [], + txParams: { gas: paramsGasLimit, gasPrice: paramsGasPrice}, + xReceipt: { status } = {}, + type, + } = transaction + + let cachedGasLimit = '0x0' + let cachedGasPrice = '0x0' + + const historyActivities = history.reduce((acc, base, index) => { + // First history item should be transaction creation + if (index === 0 && !Array.isArray(base) && base.txParams) { + const { time: timestamp, txParams: { value, gas = '0x0', gasPrice = '0x0' } = {} } = base + // The cached gas limit and gas price are used to display the gas fee in the activity log. We + // need to cache these values because the status update history events don't provide us with + // the latest gas limit and gas price. + cachedGasLimit = gas + cachedGasPrice = gasPrice + + if (isFirstTransaction) { + return acc.concat({ + id, + hash, + eventKey: TRANSACTION_CREATED_EVENT, + timestamp, + value, + }) + } + // An entry in the history may be an array of more sub-entries. + } else if (Array.isArray(base)) { + const events = [] + + base.forEach(entry => { + const { op, path, value, timestamp: entryTimestamp } = entry + // Not all sub-entries in a history entry have a timestamp. If the sub-entry does not have a + // timestamp, the first sub-entry in a history entry should. + const timestamp = entryTimestamp || base[0] && base[0].timestamp + + if (path in eventPathsHash && op === REPLACE_OP) { + switch (path) { + case STATUS_PATH: { + const gasFee = cachedGasLimit === '0x0' && cachedGasPrice === '0x0' + ? getHexGasTotal({ gasLimit: paramsGasLimit, gasPrice: paramsGasPrice }) + : getHexGasTotal({ gasLimit: cachedGasLimit, gasPrice: cachedGasPrice }) + + if (value in statusHash) { + let eventKey = statusHash[value] + + // If the status is 'submitted', we need to determine whether the event is a + // transaction retry or a cancellation attempt. + if (value === SUBMITTED_STATUS) { + if (type === TRANSACTION_TYPE_RETRY) { + eventKey = TRANSACTION_RESUBMITTED_EVENT + } else if (type === TRANSACTION_TYPE_CANCEL) { + eventKey = TRANSACTION_CANCEL_ATTEMPTED_EVENT + } + } else if (value === CONFIRMED_STATUS) { + if (type === TRANSACTION_TYPE_CANCEL) { + eventKey = TRANSACTION_CANCEL_SUCCESS_EVENT + } + } + + events.push({ + id, + hash, + eventKey, + timestamp, + value: gasFee, + }) + } + + break + } + + // If the gas price or gas limit has been changed, we update the gasFee of the + // previously submitted event. These events happen when the gas limit and gas price is + // changed at the confirm screen. + case GAS_PRICE_PATH: + case GAS_LIMIT_PATH: { + const lastEvent = events[events.length - 1] || {} + const { lastEventKey } = lastEvent + + if (path === GAS_LIMIT_PATH) { + cachedGasLimit = value + } else if (path === GAS_PRICE_PATH) { + cachedGasPrice = value + } + + if (lastEventKey === TRANSACTION_SUBMITTED_EVENT || + lastEventKey === TRANSACTION_RESUBMITTED_EVENT) { + lastEvent.value = getHexGasTotal({ + gasLimit: cachedGasLimit, + gasPrice: cachedGasPrice, + }) + } + + break + } + + default: { + events.push({ + id, + hash, + eventKey: TRANSACTION_UPDATED_EVENT, + timestamp, + }) + } + } + } + }) + + return acc.concat(events) + } + + return acc + }, []) + + // If txReceipt.status is '0x0', that means that an on-chain error occured for the transaction, + // so we add an error entry to the Activity Log. + return status === '0x0' + ? historyActivities.concat({ id, hash, eventKey: TRANSACTION_ERRORED_EVENT }) + : historyActivities +} + +/** + * @description Removes "Transaction dropped" activities from a list of sorted activities if one of + * the transactions has been confirmed. Typically, if multiple transactions have the same nonce, + * once one transaction is confirmed, the rest are dropped. In this case, we don't want to show + * multiple "Transaction dropped" activities, and instead want to show a single "Transaction + * confirmed". + * @param {Array} activities - List of sorted activities generated from the getActivities function. + * @returns {Array} + */ +function filterSortedActivities (activities) { + const filteredActivities = [] + const hasConfirmedActivity = Boolean(activities.find(({ eventKey }) => ( + eventKey === TRANSACTION_CONFIRMED_EVENT || eventKey === TRANSACTION_CANCEL_SUCCESS_EVENT + ))) + let addedDroppedActivity = false + + activities.forEach(activity => { + if (activity.eventKey === TRANSACTION_DROPPED_EVENT) { + if (!hasConfirmedActivity && !addedDroppedActivity) { + filteredActivities.push(activity) + addedDroppedActivity = true + } + } else { + filteredActivities.push(activity) + } + }) + + return filteredActivities +} + +/** + * Combines the histories of an array of transactions into a single array. + * @param {Array} transactions - Array of txMeta transaction objects. + * @returns {Array} + */ +export function combineTransactionHistories (transactions = []) { + if (!transactions.length) { + return [] + } + + const activities = [] + + transactions.forEach((transaction, index) => { + // The first transaction should be the transaction with the earliest submittedTime. We show the + // 'created' and 'submitted' activities here. All subsequent transactions will use 'resubmitted' + // instead. + const transactionActivities = getActivities(transaction, index === 0) + activities.push(...transactionActivities) + }) + + const sortedActivities = activities.sort((a, b) => a.timestamp - b.timestamp) + return filterSortedActivities(sortedActivities) +} diff --git a/ui/app/components/transaction-breakdown/index.js b/ui/app/components/app/transaction-breakdown/index.js index 4a5b52663..4a5b52663 100644 --- a/ui/app/components/transaction-breakdown/index.js +++ b/ui/app/components/app/transaction-breakdown/index.js diff --git a/ui/app/components/transaction-breakdown/index.scss b/ui/app/components/app/transaction-breakdown/index.scss index 1bb108943..c8144eac2 100644 --- a/ui/app/components/transaction-breakdown/index.scss +++ b/ui/app/components/app/transaction-breakdown/index.scss @@ -1,9 +1,10 @@ -@import './transaction-breakdown-row/index'; +@import 'transaction-breakdown-row/index'; .transaction-breakdown { - &__card { - background: $white; - height: 100%; + &__title { + border-bottom: 1px solid #d8d8d8; + padding-bottom: 4px; + text-transform: capitalize; } &__row-title { diff --git a/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js b/ui/app/components/app/transaction-breakdown/tests/transaction-breakdown.component.test.js index d18cd420c..4512b84f0 100644 --- a/ui/app/components/transaction-breakdown/tests/transaction-breakdown.component.test.js +++ b/ui/app/components/app/transaction-breakdown/tests/transaction-breakdown.component.test.js @@ -2,8 +2,6 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import TransactionBreakdown from '../transaction-breakdown.component' -import TransactionBreakdownRow from '../transaction-breakdown-row' -import Card from '../../card' describe('TransactionBreakdown Component', () => { it('should render properly', () => { @@ -31,7 +29,5 @@ describe('TransactionBreakdown Component', () => { assert.ok(wrapper.hasClass('transaction-breakdown')) assert.ok(wrapper.hasClass('test-class')) - assert.equal(wrapper.find(Card).length, 1) - assert.equal(wrapper.find(Card).find(TransactionBreakdownRow).length, 4) }) }) diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.js index 557bf75fb..557bf75fb 100644 --- a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.js +++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.js diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.scss index 8c73be1a6..8c73be1a6 100644 --- a/ui/app/components/transaction-breakdown/transaction-breakdown-row/index.scss +++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/index.scss diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js index c19399dbb..82e40fce2 100644 --- a/ui/app/components/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js +++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/tests/transaction-breakdown-row.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import TransactionBreakdownRow from '../transaction-breakdown-row.component' -import Button from '../../../button' +import Button from '../../../../ui/button' describe('TransactionBreakdownRow Component', () => { it('should render text properly', () => { diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js index c11ff8efa..c11ff8efa 100644 --- a/ui/app/components/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js +++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown-row/transaction-breakdown-row.component.js diff --git a/ui/app/components/app/transaction-breakdown/transaction-breakdown.component.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown.component.js new file mode 100644 index 000000000..5642e0fa5 --- /dev/null +++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown.component.js @@ -0,0 +1,106 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import TransactionBreakdownRow from './transaction-breakdown-row' +import CurrencyDisplay from '../../ui/currency-display' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' +import HexToDecimal from '../../ui/hex-to-decimal' +import { GWEI, PRIMARY, SECONDARY } from '../../../helpers/constants/common' + +export default class TransactionBreakdown extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + transaction: PropTypes.object, + className: PropTypes.string, + nativeCurrency: PropTypes.string.isRequired, + showFiat: PropTypes.bool, + gas: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + gasPrice: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + gasUsed: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + totalInHex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + } + + static defaultProps = { + transaction: {}, + showFiat: true, + } + + render () { + const { t } = this.context + const { gas, gasPrice, value, className, nativeCurrency, showFiat, totalInHex, gasUsed } = this.props + + return ( + <div className={classnames('transaction-breakdown', className)}> + <div className="transaction-breakdown__title"> + { t('transaction') } + </div> + <TransactionBreakdownRow title={t('amount')}> + <UserPreferencedCurrencyDisplay + className="transaction-breakdown__value" + type={PRIMARY} + value={value} + /> + </TransactionBreakdownRow> + <TransactionBreakdownRow + title={`${t('gasLimit')} (${t('units')})`} + className="transaction-breakdown__row-title" + > + {typeof gas !== 'undefined' + ? <HexToDecimal + className="transaction-breakdown__value" + value={gas} + /> + : '?' + } + </TransactionBreakdownRow> + { + typeof gasUsed === 'string' && ( + <TransactionBreakdownRow + title={`${t('gasUsed')} (${t('units')})`} + className="transaction-breakdown__row-title" + > + <HexToDecimal + className="transaction-breakdown__value" + value={gasUsed} + /> + </TransactionBreakdownRow> + ) + } + <TransactionBreakdownRow title={t('gasPrice')}> + {typeof gasPrice !== 'undefined' + ? <CurrencyDisplay + className="transaction-breakdown__value" + currency={nativeCurrency} + denomination={GWEI} + value={gasPrice} + hideLabel + /> + : '?' + } + </TransactionBreakdownRow> + <TransactionBreakdownRow title={t('total')}> + <div> + <UserPreferencedCurrencyDisplay + className="transaction-breakdown__value transaction-breakdown__value--eth-total" + type={PRIMARY} + value={totalInHex} + /> + { + showFiat && ( + <UserPreferencedCurrencyDisplay + className="transaction-breakdown__value" + type={SECONDARY} + value={totalInHex} + /> + ) + } + </div> + </TransactionBreakdownRow> + </div> + ) + } +} diff --git a/ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js b/ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js new file mode 100644 index 000000000..82f377358 --- /dev/null +++ b/ui/app/components/app/transaction-breakdown/transaction-breakdown.container.js @@ -0,0 +1,29 @@ +import { connect } from 'react-redux' +import TransactionBreakdown from './transaction-breakdown.component' +import {getIsMainnet, getNativeCurrency, preferencesSelector} from '../../../selectors/selectors' +import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util' +import { sumHexes } from '../../../helpers/utils/transactions.util' + +const mapStateToProps = (state, ownProps) => { + const { transaction } = ownProps + const { txParams: { gas, gasPrice, value } = {}, txReceipt: { gasUsed } = {} } = transaction + const { showFiatInTestnets } = preferencesSelector(state) + const isMainnet = getIsMainnet(state) + + const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas + + const hexGasTotal = gasLimit && gasPrice && getHexGasTotal({ gasLimit, gasPrice }) || '0x0' + const totalInHex = sumHexes(hexGasTotal, value) + + return { + nativeCurrency: getNativeCurrency(state), + showFiat: (isMainnet || !!showFiatInTestnets), + totalInHex, + gas, + gasPrice, + value, + gasUsed, + } +} + +export default connect(mapStateToProps)(TransactionBreakdown) diff --git a/ui/app/components/transaction-list-item-details/index.js b/ui/app/components/app/transaction-list-item-details/index.js index 0e878d032..0e878d032 100644 --- a/ui/app/components/transaction-list-item-details/index.js +++ b/ui/app/components/app/transaction-list-item-details/index.js diff --git a/ui/app/components/transaction-list-item-details/index.scss b/ui/app/components/app/transaction-list-item-details/index.scss index 54cf834cc..7cb253e4e 100644 --- a/ui/app/components/transaction-list-item-details/index.scss +++ b/ui/app/components/app/transaction-list-item-details/index.scss @@ -1,11 +1,16 @@ .transaction-list-item-details { &__header { - margin-bottom: 8px; + margin: 8px 16px; display: flex; justify-content: space-between; align-items: center; } + &__body { + background: #fafbfc; + padding: 8px 16px; + } + &__header-buttons { display: flex; flex-direction: row; @@ -17,6 +22,11 @@ &:not(:last-child) { margin-right: 8px; } + + &__copy-icon { + width: 10px; + height: 10px; + } } &__sender-to-recipient-container { @@ -45,5 +55,9 @@ &__transaction-activity-log { flex: 2; min-width: 0; + + @media screen and (min-width: $break-large) { + padding-left: 12px; + } } } diff --git a/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js b/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js index f2bbe8789..c4e118b01 100644 --- a/ui/app/components/transaction-list-item-details/tests/transaction-list-item-details.component.test.js +++ b/ui/app/components/app/transaction-list-item-details/tests/transaction-list-item-details.component.test.js @@ -2,8 +2,8 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import TransactionListItemDetails from '../transaction-list-item-details.component' -import Button from '../../button' -import SenderToRecipient from '../../sender-to-recipient' +import Button from '../../../ui/button' +import SenderToRecipient from '../../../ui/sender-to-recipient' import TransactionBreakdown from '../../transaction-breakdown' import TransactionActivityLog from '../../transaction-activity-log' @@ -23,15 +23,21 @@ describe('TransactionListItemDetails Component', () => { }, } + const transactionGroup = { + transactions: [transaction], + primaryTransaction: transaction, + initialTransaction: transaction, + } + const wrapper = shallow( <TransactionListItemDetails - transaction={transaction} + transactionGroup={transactionGroup} />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } ) assert.ok(wrapper.hasClass('transaction-list-item-details')) - assert.equal(wrapper.find(Button).length, 1) + assert.equal(wrapper.find(Button).length, 2) assert.equal(wrapper.find(SenderToRecipient).length, 1) assert.equal(wrapper.find(TransactionBreakdown).length, 1) assert.equal(wrapper.find(TransactionActivityLog).length, 1) @@ -52,15 +58,24 @@ describe('TransactionListItemDetails Component', () => { }, } + const transactionGroup = { + transactions: [transaction], + primaryTransaction: transaction, + initialTransaction: transaction, + nonce: '0xa4', + hasRetried: false, + hasCancelled: false, + } + const wrapper = shallow( <TransactionListItemDetails - transaction={transaction} + transactionGroup={transactionGroup} showRetry={true} />, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } ) assert.ok(wrapper.hasClass('transaction-list-item-details')) - assert.equal(wrapper.find(Button).length, 2) + assert.equal(wrapper.find(Button).length, 3) }) }) diff --git a/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js new file mode 100644 index 000000000..4a3b04998 --- /dev/null +++ b/ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -0,0 +1,215 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import copyToClipboard from 'copy-to-clipboard' +import SenderToRecipient from '../../ui/sender-to-recipient' +import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants' +import TransactionActivityLog from '../transaction-activity-log' +import TransactionBreakdown from '../transaction-breakdown' +import Button from '../../ui/button' +import Tooltip from '../../ui/tooltip' +import prefixForNetwork from '../../../../lib/etherscan-prefix-for-network' + +export default class TransactionListItemDetails extends PureComponent { + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + static propTypes = { + onCancel: PropTypes.func, + onRetry: PropTypes.func, + showCancel: PropTypes.bool, + showRetry: PropTypes.bool, + cancelDisabled: PropTypes.bool, + transactionGroup: PropTypes.object, + } + + state = { + justCopied: false, + cancelDisabled: false, + } + + handleEtherscanClick = () => { + const { transactionGroup: { primaryTransaction } } = this.props + const { hash, metamaskNetworkId } = primaryTransaction + + const prefix = prefixForNetwork(metamaskNetworkId) + const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}` + + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Activity Log', + name: 'Clicked "View on Etherscan"', + }, + }) + + global.platform.openWindow({ url: etherscanUrl }) + } + + handleCancel = event => { + const { transactionGroup: { initialTransaction: { id } = {} } = {}, onCancel } = this.props + + event.stopPropagation() + onCancel(id) + } + + handleRetry = event => { + const { transactionGroup: { initialTransaction: { id } = {} } = {}, onRetry } = this.props + + event.stopPropagation() + onRetry(id) + } + + handleCopyTxId = () => { + const { transactionGroup} = this.props + const { primaryTransaction: transaction } = transactionGroup + const { hash } = transaction + + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Activity Log', + name: 'Copied Transaction ID', + }, + }) + + this.setState({ justCopied: true }, () => { + copyToClipboard(hash) + setTimeout(() => this.setState({ justCopied: false }), 1000) + }) + } + + renderCancel () { + const { t } = this.context + const { + showCancel, + cancelDisabled, + } = this.props + + if (!showCancel) { + return null + } + + return cancelDisabled + ? ( + <Tooltip title={t('notEnoughGas')}> + <div> + <Button + type="raised" + onClick={this.handleCancel} + className="transaction-list-item-details__header-button" + disabled + > + { t('cancel') } + </Button> + </div> + </Tooltip> + ) + : ( + <Button + type="raised" + onClick={this.handleCancel} + className="transaction-list-item-details__header-button" + > + { t('cancel') } + </Button> + ) + } + + render () { + const { t } = this.context + const { justCopied } = this.state + const { + transactionGroup, + showRetry, + onCancel, + onRetry, + } = this.props + const { primaryTransaction: transaction } = transactionGroup + const { txParams: { to, from } = {} } = transaction + + return ( + <div className="transaction-list-item-details"> + <div className="transaction-list-item-details__header"> + <div>{ t('details') }</div> + <div className="transaction-list-item-details__header-buttons"> + { + showRetry && ( + <Button + type="raised" + onClick={this.handleRetry} + className="transaction-list-item-details__header-button" + > + { t('speedUp') } + </Button> + ) + } + { this.renderCancel() } + <Tooltip title={justCopied ? t('copiedTransactionId') : t('copyTransactionId')}> + <Button + type="raised" + onClick={this.handleCopyTxId} + className="transaction-list-item-details__header-button" + > + <img + className="transaction-list-item-details__header-button__copy-icon" + src="/images/copy-to-clipboard.svg" + /> + </Button> + </Tooltip> + <Tooltip title={t('viewOnEtherscan')}> + <Button + type="raised" + onClick={this.handleEtherscanClick} + className="transaction-list-item-details__header-button" + > + <img src="/images/arrow-popout.svg" /> + </Button> + </Tooltip> + </div> + </div> + <div className="transaction-list-item-details__body"> + <div className="transaction-list-item-details__sender-to-recipient-container"> + <SenderToRecipient + variant={FLAT_VARIANT} + addressOnly + recipientAddress={to} + senderAddress={from} + onRecipientClick={() => { + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Activity Log', + name: 'Copied "To" Address', + }, + }) + }} + onSenderClick={() => { + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Activity Log', + name: 'Copied "From" Address', + }, + }) + }} + /> + </div> + <div className="transaction-list-item-details__cards-container"> + <TransactionBreakdown + transaction={transaction} + className="transaction-list-item-details__transaction-breakdown" + /> + <TransactionActivityLog + transactionGroup={transactionGroup} + className="transaction-list-item-details__transaction-activity-log" + onCancel={onCancel} + onRetry={onRetry} + /> + </div> + </div> + </div> + ) + } +} diff --git a/ui/app/components/transaction-list-item/index.js b/ui/app/components/app/transaction-list-item/index.js index 697cc55e9..697cc55e9 100644 --- a/ui/app/components/transaction-list-item/index.js +++ b/ui/app/components/app/transaction-list-item/index.js diff --git a/ui/app/components/transaction-list-item/index.scss b/ui/app/components/app/transaction-list-item/index.scss index ac0e7beeb..9e73a546c 100644 --- a/ui/app/components/transaction-list-item/index.scss +++ b/ui/app/components/app/transaction-list-item/index.scss @@ -80,6 +80,8 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + min-width: 0; + max-width: 100%; &--primary { text-align: end; @@ -115,12 +117,6 @@ } } - &__details-container { - padding: 8px 16px 16px; - background: #f3f4f7; - width: 100%; - } - &__expander { max-height: 0px; width: 100%; diff --git a/ui/app/components/transaction-list-item/transaction-list-item.component.js b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js index 696634fe0..c7d9dd7c7 100644 --- a/ui/app/components/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.component.js @@ -1,16 +1,16 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import Identicon from '../identicon' +import Identicon from '../../ui/identicon' import TransactionStatus from '../transaction-status' import TransactionAction from '../transaction-action' import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' -import TokenCurrencyDisplay from '../token-currency-display' +import TokenCurrencyDisplay from '../../ui/token-currency-display' import TransactionListItemDetails from '../transaction-list-item-details' -import { CONFIRM_TRANSACTION_ROUTE } from '../../routes' -import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions' -import { PRIMARY, SECONDARY } from '../../constants/common' -import { getStatusKey } from '../../helpers/transactions.util' +import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes' +import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../../helpers/constants/transactions' +import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' +import { getStatusKey } from '../../../helpers/utils/transactions.util' export default class TransactionListItem extends PureComponent { static propTypes = { @@ -18,15 +18,29 @@ export default class TransactionListItem extends PureComponent { history: PropTypes.object, methodData: PropTypes.object, nonceAndDate: PropTypes.string, + primaryTransaction: PropTypes.object, retryTransaction: PropTypes.func, setSelectedToken: PropTypes.func, showCancelModal: PropTypes.func, showCancel: PropTypes.bool, + hasEnoughCancelGas: PropTypes.bool, showRetry: PropTypes.bool, + showFiat: PropTypes.bool, token: PropTypes.object, tokenData: PropTypes.object, transaction: PropTypes.object, + transactionGroup: PropTypes.object, value: PropTypes.string, + fetchBasicGasAndTimeEstimates: PropTypes.func, + fetchGasEstimates: PropTypes.func, + } + + static defaultProps = { + showFiat: true, + } + + static contextTypes = { + metricsEvent: PropTypes.func, } state = { @@ -46,36 +60,61 @@ export default class TransactionListItem extends PureComponent { return } + if (!showTransactionDetails) { + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Expand Transaction', + }, + }) + } + this.setState({ showTransactionDetails: !showTransactionDetails }) } - handleCancel = () => { - const { transaction: { id, txParams: { gasPrice } } = {}, showCancelModal } = this.props - showCancelModal(id, gasPrice) + handleCancel = id => { + const { + primaryTransaction: { txParams: { gasPrice } } = {}, + transaction: { id: initialTransactionId }, + showCancelModal, + } = this.props + + const cancelId = id || initialTransactionId + showCancelModal(cancelId, gasPrice) } - handleRetry = () => { + /** + * @name handleRetry + * @description Resubmits a transaction. Retrying a transaction within a list of transactions with + * the same nonce requires keeping the original value while increasing the gas price of the latest + * transaction. + * @param {number} id - Transaction id + */ + handleRetry = id => { const { - transaction: { txParams: { to } = {} }, + primaryTransaction: { txParams: { gasPrice } } = {}, + transaction: { txParams: { to } = {}, id: initialTransactionId }, methodData: { name } = {}, setSelectedToken, + retryTransaction, + fetchBasicGasAndTimeEstimates, + fetchGasEstimates, } = this.props if (name === TOKEN_METHOD_TRANSFER) { setSelectedToken(to) } - return this.resubmit() - } + const retryId = id || initialTransactionId - resubmit () { - const { transaction: { id }, retryTransaction, history } = this.props - return retryTransaction(id) - .then(id => history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)) + return fetchBasicGasAndTimeEstimates() + .then(basicEstimates => fetchGasEstimates(basicEstimates.blockTime)) + .then(retryTransaction(retryId, gasPrice)) } renderPrimaryCurrency () { - const { token, transaction: { txParams: { data } = {} } = {}, value } = this.props + const { token, primaryTransaction: { txParams: { data } = {} } = {}, value } = this.props return token ? ( @@ -96,9 +135,9 @@ export default class TransactionListItem extends PureComponent { } renderSecondaryCurrency () { - const { token, value } = this.props + const { token, value, showFiat } = this.props - return token + return token || !showFiat ? null : ( <UserPreferencedCurrencyDisplay @@ -113,12 +152,15 @@ export default class TransactionListItem extends PureComponent { render () { const { assetImages, + transaction, methodData, nonceAndDate, + primaryTransaction, showCancel, + hasEnoughCancelGas, showRetry, tokenData, - transaction, + transactionGroup, } = this.props const { txParams = {} } = transaction const { showTransactionDetails } = this.state @@ -151,11 +193,11 @@ export default class TransactionListItem extends PureComponent { </div> <TransactionStatus className="transaction-list-item__status" - statusKey={getStatusKey(transaction)} + statusKey={getStatusKey(primaryTransaction)} title={( - (transaction.err && transaction.err.rpc) - ? transaction.err.rpc.message - : transaction.err && transaction.err.message + (primaryTransaction.err && primaryTransaction.err.rpc) + ? primaryTransaction.err.rpc.message + : primaryTransaction.err && primaryTransaction.err.message )} /> { this.renderPrimaryCurrency() } @@ -168,11 +210,12 @@ export default class TransactionListItem extends PureComponent { showTransactionDetails && ( <div className="transaction-list-item__details-container"> <TransactionListItemDetails - transaction={transaction} + transactionGroup={transactionGroup} onRetry={this.handleRetry} showRetry={showRetry && methodData.done} onCancel={this.handleCancel} showCancel={showCancel} + cancelDisabled={!hasEnoughCancelGas} /> </div> ) diff --git a/ui/app/components/app/transaction-list-item/transaction-list-item.container.js b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js new file mode 100644 index 000000000..de8a3bbba --- /dev/null +++ b/ui/app/components/app/transaction-list-item/transaction-list-item.container.js @@ -0,0 +1,98 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import withMethodData from '../../../helpers/higher-order-components/with-method-data' +import TransactionListItem from './transaction-list-item.component' +import { setSelectedToken, showModal, showSidebar, addKnownMethodData } from '../../../store/actions' +import { hexToDecimal } from '../../../helpers/utils/conversions.util' +import { getTokenData } from '../../../helpers/utils/transactions.util' +import { getHexGasTotal, increaseLastGasPrice } from '../../../helpers/utils/confirm-tx.util' +import { formatDate } from '../../../helpers/utils/util' +import { + fetchBasicGasAndTimeEstimates, + fetchGasEstimates, + setCustomGasPriceForRetry, + setCustomGasLimit, +} from '../../../ducks/gas/gas.duck' +import { getIsMainnet, preferencesSelector, getSelectedAddress, conversionRateSelector } from '../../../selectors/selectors' +import { isBalanceSufficient } from '../send/send.utils' + +const mapStateToProps = (state, ownProps) => { + const { metamask: { knownMethodData, accounts } } = state + const { showFiatInTestnets } = preferencesSelector(state) + const isMainnet = getIsMainnet(state) + const { transactionGroup: { primaryTransaction } = {} } = ownProps + const { txParams: { gas: gasLimit, gasPrice } = {} } = primaryTransaction + const selectedAccountBalance = accounts[getSelectedAddress(state)].balance + + const hasEnoughCancelGas = primaryTransaction.txParams && isBalanceSufficient({ + amount: '0x0', + gasTotal: getHexGasTotal({ + gasPrice: increaseLastGasPrice(gasPrice), + gasLimit, + }), + balance: selectedAccountBalance, + conversionRate: conversionRateSelector(state), + }) + + return { + knownMethodData, + showFiat: (isMainnet || !!showFiatInTestnets), + selectedAccountBalance, + hasEnoughCancelGas, + } +} + +const mapDispatchToProps = dispatch => { + return { + fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), + fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), + setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)), + addKnownMethodData: (fourBytePrefix, methodData) => dispatch(addKnownMethodData(fourBytePrefix, methodData)), + retryTransaction: (transaction, gasPrice) => { + dispatch(setCustomGasPriceForRetry(gasPrice || transaction.txParams.gasPrice)) + dispatch(setCustomGasLimit(transaction.txParams.gas)) + dispatch(showSidebar({ + transitionName: 'sidebar-left', + type: 'customize-gas', + props: { transaction }, + })) + }, + showCancelModal: (transactionId, originalGasPrice) => { + return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice })) + }, + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { transactionGroup: { primaryTransaction, initialTransaction } = {} } = ownProps + const { retryTransaction, ...restDispatchProps } = dispatchProps + const { txParams: { nonce, data } = {}, time } = initialTransaction + const { txParams: { value } = {} } = primaryTransaction + + const tokenData = data && getTokenData(data) + const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time) + + return { + ...stateProps, + ...restDispatchProps, + ...ownProps, + value, + nonceAndDate, + tokenData, + transaction: initialTransaction, + primaryTransaction, + retryTransaction: (transactionId, gasPrice) => { + const { transactionGroup: { transactions = [] } } = ownProps + const transaction = transactions.find(tx => tx.id === transactionId) || {} + const increasedGasPrice = increaseLastGasPrice(gasPrice) + retryTransaction(transaction, increasedGasPrice) + }, + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withMethodData, +)(TransactionListItem) diff --git a/ui/app/components/transaction-list/index.js b/ui/app/components/app/transaction-list/index.js index 688994367..688994367 100644 --- a/ui/app/components/transaction-list/index.js +++ b/ui/app/components/app/transaction-list/index.js diff --git a/ui/app/components/transaction-list/index.scss b/ui/app/components/app/transaction-list/index.scss index ba7ffd87b..a486f4112 100644 --- a/ui/app/components/transaction-list/index.scss +++ b/ui/app/components/app/transaction-list/index.scss @@ -33,7 +33,7 @@ &__empty { flex: 1; display: grid; - grid-template-rows: 35% 1fr; + grid-template-rows: auto; padding-top: 8px; } diff --git a/ui/app/components/transaction-list/transaction-list.component.js b/ui/app/components/app/transaction-list/transaction-list.component.js index eef60186d..fc5488884 100644 --- a/ui/app/components/transaction-list/transaction-list.component.js +++ b/ui/app/components/app/transaction-list/transaction-list.component.js @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import TransactionListItem from '../transaction-list-item' import ShapeShiftTransactionListItem from '../shift-list-item' -import { TRANSACTION_TYPE_SHAPESHIFT } from '../../constants/transactions' +import { TRANSACTION_TYPE_SHAPESHIFT } from '../../../helpers/constants/transactions' export default class TransactionList extends PureComponent { static contextTypes = { @@ -12,13 +12,11 @@ export default class TransactionList extends PureComponent { static defaultProps = { pendingTransactions: [], completedTransactions: [], - transactionToRetry: {}, } static propTypes = { pendingTransactions: PropTypes.array, completedTransactions: PropTypes.array, - transactionToRetry: PropTypes.object, selectedToken: PropTypes.object, updateNetworkNonce: PropTypes.func, assetImages: PropTypes.object, @@ -37,26 +35,34 @@ export default class TransactionList extends PureComponent { } } - shouldShowRetry = transaction => { - const { transactionToRetry } = this.props - const { id, submittedTime } = transaction - return id === transactionToRetry.id && Date.now() - submittedTime > 30000 + shouldShowRetry = (transactionGroup, isEarliestNonce) => { + const { transactions = [], hasRetried } = transactionGroup + const [earliestTransaction = {}] = transactions + const { submittedTime } = earliestTransaction + return Date.now() - submittedTime > 30000 && isEarliestNonce && !hasRetried + } + + shouldShowCancel (transactionGroup) { + const { hasCancelled } = transactionGroup + return !hasCancelled } renderTransactions () { const { t } = this.context const { pendingTransactions = [], completedTransactions = [] } = this.props + const pendingLength = pendingTransactions.length + return ( <div className="transaction-list__transactions"> { - pendingTransactions.length > 0 && ( + pendingLength > 0 && ( <div className="transaction-list__pending-transactions"> <div className="transaction-list__header"> { `${t('queue')} (${pendingTransactions.length})` } </div> { - pendingTransactions.map((transaction, index) => ( - this.renderTransaction(transaction, index, true) + pendingTransactions.map((transactionGroup, index) => ( + this.renderTransaction(transactionGroup, index, true) )) } </div> @@ -68,8 +74,8 @@ export default class TransactionList extends PureComponent { </div> { completedTransactions.length > 0 - ? completedTransactions.map((transaction, index) => ( - this.renderTransaction(transaction, index) + ? completedTransactions.map((transactionGroup, index) => ( + this.renderTransaction(transactionGroup, index) )) : this.renderEmpty() } @@ -78,21 +84,22 @@ export default class TransactionList extends PureComponent { ) } - renderTransaction (transaction, index, showCancel) { + renderTransaction (transactionGroup, index, isPendingTx = false) { const { selectedToken, assetImages } = this.props + const { transactions = [] } = transactionGroup - return transaction.key === TRANSACTION_TYPE_SHAPESHIFT + return transactions[0].key === TRANSACTION_TYPE_SHAPESHIFT ? ( <ShapeShiftTransactionListItem - { ...transaction } + { ...transactions[0] } key={`shapeshift${index}`} /> ) : ( <TransactionListItem - transaction={transaction} - key={transaction.id} - showRetry={this.shouldShowRetry(transaction)} - showCancel={showCancel} + transactionGroup={transactionGroup} + key={`${transactionGroup.nonce}:${index}`} + showRetry={isPendingTx && this.shouldShowRetry(transactionGroup, index === 0)} + showCancel={isPendingTx && this.shouldShowCancel(transactionGroup)} token={selectedToken} assetImages={assetImages} /> diff --git a/ui/app/components/transaction-list/transaction-list.container.js b/ui/app/components/app/transaction-list/transaction-list.container.js index 2e946c67d..67a24588b 100644 --- a/ui/app/components/transaction-list/transaction-list.container.js +++ b/ui/app/components/app/transaction-list/transaction-list.container.js @@ -3,24 +3,17 @@ import { withRouter } from 'react-router-dom' import { compose } from 'recompose' import TransactionList from './transaction-list.component' import { - pendingTransactionsSelector, - submittedPendingTransactionsSelector, - completedTransactionsSelector, -} from '../../selectors/transactions' -import { getSelectedAddress, getAssetImages } from '../../selectors' -import { selectedTokenSelector } from '../../selectors/tokens' -import { getLatestSubmittedTxWithNonce } from '../../helpers/transactions.util' -import { updateNetworkNonce } from '../../actions' + nonceSortedCompletedTransactionsSelector, + nonceSortedPendingTransactionsSelector, +} from '../../../selectors/transactions' +import { getSelectedAddress, getAssetImages } from '../../../selectors/selectors' +import { selectedTokenSelector } from '../../../selectors/tokens' +import { updateNetworkNonce } from '../../../store/actions' const mapStateToProps = state => { - const pendingTransactions = pendingTransactionsSelector(state) - const submittedPendingTransactions = submittedPendingTransactionsSelector(state) - const networkNonce = state.appState.networkNonce - return { - completedTransactions: completedTransactionsSelector(state), - pendingTransactions, - transactionToRetry: getLatestSubmittedTxWithNonce(submittedPendingTransactions, networkNonce), + completedTransactions: nonceSortedCompletedTransactionsSelector(state), + pendingTransactions: nonceSortedPendingTransactionsSelector(state), selectedToken: selectedTokenSelector(state), selectedAddress: getSelectedAddress(state), assetImages: getAssetImages(state), diff --git a/ui/app/components/transaction-status/index.js b/ui/app/components/app/transaction-status/index.js index dece41e9c..dece41e9c 100644 --- a/ui/app/components/transaction-status/index.js +++ b/ui/app/components/app/transaction-status/index.js diff --git a/ui/app/components/transaction-status/index.scss b/ui/app/components/app/transaction-status/index.scss index 26a1f5d38..e7daafeef 100644 --- a/ui/app/components/transaction-status/index.scss +++ b/ui/app/components/app/transaction-status/index.scss @@ -1,6 +1,6 @@ .transaction-status { height: 26px; - width: 81px; + width: 84px; border-radius: 4px; background-color: #f0f0f0; color: #5e6064; @@ -12,22 +12,34 @@ @media screen and (max-width: $break-small) { height: 16px; - width: 70px; + width: 72px; font-size: .5rem; } &--confirmed { background-color: #eafad7; color: #609a1c; + + .transaction-status__transaction-count { + border: 1px solid #609a1c; + } } &--approved, &--submitted { background-color: #FFF2DB; color: #CA810A; + + .transaction-status__transaction-count { + border: 1px solid #CA810A; + } } &--failed { background: lighten($monzo, 56%); color: $monzo; + + .transaction-status__transaction-count { + border: 1px solid $monzo; + } } } diff --git a/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js b/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js new file mode 100644 index 000000000..ec1d580bd --- /dev/null +++ b/ui/app/components/app/transaction-status/tests/transaction-status.component.test.js @@ -0,0 +1,33 @@ +import React from 'react' +import assert from 'assert' +import { mount } from 'enzyme' +import TransactionStatus from '../transaction-status.component' +import Tooltip from '../../../ui/tooltip-v2' + +describe('TransactionStatus Component', () => { + it('should render APPROVED properly', () => { + const wrapper = mount( + <TransactionStatus + statusKey="approved" + title="test-title" + />, + { context: { t: str => str.toUpperCase() } } + ) + + assert.ok(wrapper) + assert.equal(wrapper.text(), 'APPROVED') + assert.equal(wrapper.find(Tooltip).props().title, 'test-title') + }) + + it('should render SUBMITTED properly', () => { + const wrapper = mount( + <TransactionStatus + statusKey="submitted" + />, + { context: { t: str => str.toUpperCase() } } + ) + + assert.ok(wrapper) + assert.equal(wrapper.text(), 'PENDING') + }) +}) diff --git a/ui/app/components/transaction-status/transaction-status.component.js b/ui/app/components/app/transaction-status/transaction-status.component.js index c22baf18a..d3a239539 100644 --- a/ui/app/components/transaction-status/transaction-status.component.js +++ b/ui/app/components/app/transaction-status/transaction-status.component.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import Tooltip from '../tooltip-v2' +import Tooltip from '../../ui/tooltip-v2' import { UNAPPROVED_STATUS, REJECTED_STATUS, @@ -11,7 +11,8 @@ import { CONFIRMED_STATUS, FAILED_STATUS, DROPPED_STATUS, -} from '../../constants/transactions' + CANCELLED_STATUS, +} from '../../../helpers/constants/transactions' const statusToClassNameHash = { [UNAPPROVED_STATUS]: 'transaction-status--unapproved', @@ -22,10 +23,10 @@ const statusToClassNameHash = { [CONFIRMED_STATUS]: 'transaction-status--confirmed', [FAILED_STATUS]: 'transaction-status--failed', [DROPPED_STATUS]: 'transaction-status--dropped', + [CANCELLED_STATUS]: 'transaction-status--failed', } const statusToTextHash = { - [APPROVED_STATUS]: 'pending', [SUBMITTED_STATUS]: 'pending', } @@ -50,7 +51,10 @@ export default class TransactionStatus extends PureComponent { return ( <div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}> - <Tooltip position="top" title={title}> + <Tooltip + position="top" + title={title} + > { statusText } </Tooltip> </div> diff --git a/ui/app/components/transaction-view-balance/index.js b/ui/app/components/app/transaction-view-balance/index.js index 8824737f7..8824737f7 100644 --- a/ui/app/components/transaction-view-balance/index.js +++ b/ui/app/components/app/transaction-view-balance/index.js diff --git a/ui/app/components/transaction-view-balance/index.scss b/ui/app/components/app/transaction-view-balance/index.scss index 659f896ff..bdcd536b0 100644 --- a/ui/app/components/transaction-view-balance/index.scss +++ b/ui/app/components/app/transaction-view-balance/index.scss @@ -6,27 +6,29 @@ height: 54px; min-width: 0; + @media screen and (max-width: $break-small) { + flex-direction: column; + height: initial; + width: 100%; + } + &__balance { margin: 0 12px; display: flex; flex-direction: column; min-width: 0; + position: relative; @media screen and (max-width: $break-small) { align-items: center; margin: 16px 0; + padding: 0 16px; + max-width: 100%; } } - &__token-balance { - margin-left: 12px; - font-size: 1.5rem; - - @media screen and (max-width: $break-small) { - margin: 12px 0; - margin-left: 0; - font-size: 1.75rem; - } + &__primary-container { + display: flex; } &__primary-balance { @@ -34,9 +36,24 @@ @media screen and (max-width: $break-small) { font-size: 1.75rem; + width: 100%; + justify-content: center; } } + &__cached-star { + margin-left: 4px; + } + + &__cached-balance, &__cached-star { + color: $web-orange; + } + + &__cached-secondary-balance { + color: rgba(220, 153, 18, 0.6901960784313725); + font-size: 1.15rem; + } + &__secondary-balance { font-size: 1.15rem; color: #a0a0a0; @@ -51,6 +68,7 @@ @media screen and (max-width: $break-small) { flex-direction: column; + width: 100%; } } @@ -71,9 +89,4 @@ margin-right: 12px; } } - - @media screen and (max-width: $break-small) { - flex-direction: column; - height: initial - } } diff --git a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js b/ui/app/components/app/transaction-view-balance/tests/token-view-balance.component.test.js index 513a8aac9..0e2882e9c 100644 --- a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js +++ b/ui/app/components/app/transaction-view-balance/tests/token-view-balance.component.test.js @@ -2,9 +2,9 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' -import TokenBalance from '../../token-balance' +import TokenBalance from '../../../ui/token-balance' import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' -import { SEND_ROUTE } from '../../../routes' +import { SEND_ROUTE } from '../../../../helpers/constants/routes' import TransactionViewBalance from '../transaction-view-balance.component' const propsMethodSpies = { @@ -16,6 +16,7 @@ const historySpies = { } const t = (str1, str2) => str2 ? str1 + str2 : str1 +const metricsEvent = () => ({}) describe('TransactionViewBalance Component', () => { afterEach(() => { @@ -31,7 +32,7 @@ describe('TransactionViewBalance Component', () => { ethBalance={123} fiatBalance={456} currentCurrency="usd" - />, { context: { t } }) + />, { context: { t, metricsEvent } }) assert.equal(wrapper.find('.transaction-view-balance').length, 1) assert.equal(wrapper.find('.transaction-view-balance__button').length, 2) diff --git a/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js b/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js new file mode 100644 index 000000000..8559e2233 --- /dev/null +++ b/ui/app/components/app/transaction-view-balance/transaction-view-balance.component.js @@ -0,0 +1,145 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import Button from '../../ui/button' +import Identicon from '../../ui/identicon' +import TokenBalance from '../../ui/token-balance' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' +import { SEND_ROUTE } from '../../../helpers/constants/routes' +import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' +import Tooltip from '../../ui/tooltip-v2' + +export default class TransactionViewBalance extends PureComponent { + static contextTypes = { + t: PropTypes.func, + metricsEvent: PropTypes.func, + } + + static propTypes = { + showDepositModal: PropTypes.func, + selectedToken: PropTypes.object, + history: PropTypes.object, + network: PropTypes.string, + balance: PropTypes.string, + assetImage: PropTypes.string, + balanceIsCached: PropTypes.bool, + showFiat: PropTypes.bool, + } + + static defaultProps = { + showFiat: true, + } + + renderBalance () { + const { selectedToken, balance, balanceIsCached, showFiat } = this.props + + return selectedToken + ? ( + <div className="transaction-view-balance__balance"> + <TokenBalance + token={selectedToken} + withSymbol + className="transaction-view-balance__primary-balance" + /> + </div> + ) : ( + <Tooltip position="top" title={this.context.t('balanceOutdated')} disabled={!balanceIsCached}> + <div className="transaction-view-balance__balance"> + <div className="transaction-view-balance__primary-container"> + <UserPreferencedCurrencyDisplay + className={classnames('transaction-view-balance__primary-balance', { + 'transaction-view-balance__cached-balance': balanceIsCached, + })} + value={balance} + type={PRIMARY} + ethNumberOfDecimals={4} + hideTitle={true} + /> + { + balanceIsCached ? <span className="transaction-view-balance__cached-star">*</span> : null + } + </div> + { + showFiat && ( + <UserPreferencedCurrencyDisplay + className={classnames({ + 'transaction-view-balance__cached-secondary-balance': balanceIsCached, + 'transaction-view-balance__secondary-balance': !balanceIsCached, + })} + value={balance} + type={SECONDARY} + ethNumberOfDecimals={4} + hideTitle={true} + /> + ) + } + </div> + </Tooltip> + ) + } + + renderButtons () { + const { t, metricsEvent } = this.context + const { selectedToken, showDepositModal, history } = this.props + + return ( + <div className="transaction-view-balance__buttons"> + { + !selectedToken && ( + <Button + type="primary" + className="transaction-view-balance__button" + onClick={() => { + metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Clicked Deposit', + }, + }) + showDepositModal() + }} + > + { t('deposit') } + </Button> + ) + } + <Button + type="primary" + className="transaction-view-balance__button" + onClick={() => { + metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Clicked Send', + }, + }) + history.push(SEND_ROUTE) + }} + > + { t('send') } + </Button> + </div> + ) + } + + render () { + const { network, selectedToken, assetImage } = this.props + + return ( + <div className="transaction-view-balance"> + <div className="transaction-view-balance__balance-container"> + <Identicon + diameter={50} + address={selectedToken && selectedToken.address} + network={network} + image={assetImage} + /> + { this.renderBalance() } + </div> + { this.renderButtons() } + </div> + ) + } +} diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js b/ui/app/components/app/transaction-view-balance/transaction-view-balance.container.js index cb8078ec1..41a4525dc 100644 --- a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js +++ b/ui/app/components/app/transaction-view-balance/transaction-view-balance.container.js @@ -2,12 +2,24 @@ import { connect } from 'react-redux' import { withRouter } from 'react-router-dom' import { compose } from 'recompose' import TransactionViewBalance from './transaction-view-balance.component' -import { getSelectedToken, getSelectedAddress, getNativeCurrency, getSelectedTokenAssetImage } from '../../selectors' -import { showModal } from '../../actions' +import { + getSelectedToken, + getSelectedAddress, + getNativeCurrency, + getSelectedTokenAssetImage, + getMetaMaskAccounts, + isBalanceCached, + preferencesSelector, + getIsMainnet, +} from '../../../selectors/selectors' +import { showModal } from '../../../store/actions' const mapStateToProps = state => { + const { showFiatInTestnets } = preferencesSelector(state) + const isMainnet = getIsMainnet(state) const selectedAddress = getSelectedAddress(state) - const { metamask: { network, accounts } } = state + const { metamask: { network } } = state + const accounts = getMetaMaskAccounts(state) const account = accounts[selectedAddress] const { balance } = account @@ -17,6 +29,8 @@ const mapStateToProps = state => { balance, nativeCurrency: getNativeCurrency(state), assetImage: getSelectedTokenAssetImage(state), + balanceIsCached: isBalanceCached(state), + showFiat: (isMainnet || !!showFiatInTestnets), } } diff --git a/ui/app/components/transaction-view/index.js b/ui/app/components/app/transaction-view/index.js index 9eb0c3c83..9eb0c3c83 100644 --- a/ui/app/components/transaction-view/index.js +++ b/ui/app/components/app/transaction-view/index.js diff --git a/ui/app/components/transaction-view/index.scss b/ui/app/components/app/transaction-view/index.scss index 13187f0e5..13187f0e5 100644 --- a/ui/app/components/transaction-view/index.scss +++ b/ui/app/components/app/transaction-view/index.scss diff --git a/ui/app/components/transaction-view/transaction-view.component.js b/ui/app/components/app/transaction-view/transaction-view.component.js index 7014ca173..7014ca173 100644 --- a/ui/app/components/transaction-view/transaction-view.component.js +++ b/ui/app/components/app/transaction-view/transaction-view.component.js diff --git a/ui/app/components/app/ui-migration-annoucement/index.js b/ui/app/components/app/ui-migration-annoucement/index.js new file mode 100644 index 000000000..c6c8cc619 --- /dev/null +++ b/ui/app/components/app/ui-migration-annoucement/index.js @@ -0,0 +1 @@ +export {default} from './ui-migration-announcement.container' diff --git a/ui/app/components/app/ui-migration-annoucement/index.scss b/ui/app/components/app/ui-migration-annoucement/index.scss new file mode 100644 index 000000000..6138a3079 --- /dev/null +++ b/ui/app/components/app/ui-migration-annoucement/index.scss @@ -0,0 +1,22 @@ +.ui-migration-announcement { + position: absolute; + z-index: 9999; + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + background: $white; + + p { + box-sizing: border-box; + padding: 1em; + font-size: 12pt; + } + + p:last-of-type { + cursor: pointer; + text-decoration: underline; + font-weight: bold; + } +} diff --git a/ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js b/ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js new file mode 100644 index 000000000..7a4124972 --- /dev/null +++ b/ui/app/components/app/ui-migration-annoucement/ui-migration-annoucement.component.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types' +import React, {PureComponent} from 'react' + +export default class UiMigrationAnnouncement extends PureComponent { + static contextTypes = { + t: PropTypes.func.isRequired, + } + + static defaultProps = { + shouldShowAnnouncement: true, + }; + + static propTypes = { + onClose: PropTypes.func.isRequired, + shouldShowAnnouncement: PropTypes.bool, + } + + render () { + const { t } = this.context + const { onClose, shouldShowAnnouncement } = this.props + + if (!shouldShowAnnouncement) { + return null + } + + return ( + <div className="ui-migration-announcement"> + <p>{t('uiMigrationAnnouncement')}</p> + <p onClick={onClose}>{t('close')}</p> + </div> + ) + } +} diff --git a/ui/app/components/app/ui-migration-annoucement/ui-migration-announcement.container.js b/ui/app/components/app/ui-migration-annoucement/ui-migration-announcement.container.js new file mode 100644 index 000000000..55efd5a44 --- /dev/null +++ b/ui/app/components/app/ui-migration-annoucement/ui-migration-announcement.container.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux' +import UiMigrationAnnouncement from './ui-migration-annoucement.component' +import { setCompletedUiMigration } from '../../../store/actions' + +const mapStateToProps = (state) => { + const shouldShowAnnouncement = !state.metamask.completedUiMigration + + return { + shouldShowAnnouncement, + } +} + +const mapDispatchToProps = dispatch => { + return { + onClose () { + dispatch(setCompletedUiMigration()) + }, + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(UiMigrationAnnouncement) diff --git a/ui/app/components/user-preferenced-currency-display/index.js b/ui/app/components/app/user-preferenced-currency-display/index.js index 0deddaecf..0deddaecf 100644 --- a/ui/app/components/user-preferenced-currency-display/index.js +++ b/ui/app/components/app/user-preferenced-currency-display/index.js diff --git a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js index ead584c26..51b2a3c4f 100644 --- a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js +++ b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display.component' -import CurrencyDisplay from '../../currency-display' +import CurrencyDisplay from '../../../ui/currency-display' describe('UserPreferencedCurrencyDisplay Component', () => { describe('rendering', () => { diff --git a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js index ba1c23d83..88d63baae 100644 --- a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js +++ b/ui/app/components/app/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js @@ -21,6 +21,10 @@ describe('UserPreferencedCurrencyDisplay container', () => { nativeCurrency: 'ETH', preferences: { useNativeCurrencyAsPrimaryCurrency: true, + showFiatInTestnets: false, + }, + provider: { + type: 'mainnet', }, }, } @@ -28,6 +32,30 @@ describe('UserPreferencedCurrencyDisplay container', () => { assert.deepEqual(mapStateToProps(mockState), { nativeCurrency: 'ETH', useNativeCurrencyAsPrimaryCurrency: true, + isMainnet: true, + showFiatInTestnets: false, + }) + }) + + it('should return the correct props when not in mainnet and showFiatInTestnets is true', () => { + const mockState = { + metamask: { + nativeCurrency: 'ETH', + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + showFiatInTestnets: true, + }, + provider: { + type: 'rinkeby', + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + nativeCurrency: 'ETH', + useNativeCurrencyAsPrimaryCurrency: true, + isMainnet: false, + showFiatInTestnets: true, }) }) }) @@ -41,6 +69,8 @@ describe('UserPreferencedCurrencyDisplay container', () => { stateProps: { useNativeCurrencyAsPrimaryCurrency: true, nativeCurrency: 'ETH', + isMainnet: true, + showFiatInTestnets: false, }, ownProps: { type: 'PRIMARY', @@ -56,6 +86,8 @@ describe('UserPreferencedCurrencyDisplay container', () => { stateProps: { useNativeCurrencyAsPrimaryCurrency: false, nativeCurrency: 'ETH', + isMainnet: true, + showFiatInTestnets: false, }, ownProps: { type: 'PRIMARY', @@ -71,6 +103,8 @@ describe('UserPreferencedCurrencyDisplay container', () => { stateProps: { useNativeCurrencyAsPrimaryCurrency: true, nativeCurrency: 'ETH', + isMainnet: true, + showFiatInTestnets: false, }, ownProps: { type: 'SECONDARY', @@ -88,6 +122,8 @@ describe('UserPreferencedCurrencyDisplay container', () => { stateProps: { useNativeCurrencyAsPrimaryCurrency: false, nativeCurrency: 'ETH', + isMainnet: true, + showFiatInTestnets: false, }, ownProps: { type: 'SECONDARY', @@ -103,6 +139,57 @@ describe('UserPreferencedCurrencyDisplay container', () => { prefix: 'b', }, }, + { + stateProps: { + useNativeCurrencyAsPrimaryCurrency: false, + nativeCurrency: 'ETH', + isMainnet: false, + showFiatInTestnets: false, + }, + ownProps: { + type: 'PRIMARY', + }, + result: { + currency: 'ETH', + nativeCurrency: 'ETH', + numberOfDecimals: 6, + prefix: undefined, + }, + }, + { + stateProps: { + useNativeCurrencyAsPrimaryCurrency: false, + nativeCurrency: 'ETH', + isMainnet: false, + showFiatInTestnets: true, + }, + ownProps: { + type: 'PRIMARY', + }, + result: { + currency: undefined, + nativeCurrency: 'ETH', + numberOfDecimals: 2, + prefix: undefined, + }, + }, + { + stateProps: { + useNativeCurrencyAsPrimaryCurrency: false, + nativeCurrency: 'ETH', + isMainnet: true, + showFiatInTestnets: true, + }, + ownProps: { + type: 'PRIMARY', + }, + result: { + currency: undefined, + nativeCurrency: 'ETH', + numberOfDecimals: 2, + prefix: undefined, + }, + }, ] tests.forEach(({ stateProps, ownProps, result }) => { diff --git a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js index f2a834ea7..4b64b26c0 100644 --- a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js +++ b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import { PRIMARY, SECONDARY, ETH } from '../../constants/common' -import CurrencyDisplay from '../currency-display' +import { PRIMARY, SECONDARY, ETH } from '../../../helpers/constants/common' +import CurrencyDisplay from '../../ui/currency-display' export default class UserPreferencedCurrencyDisplay extends PureComponent { static propTypes = { @@ -10,6 +10,7 @@ export default class UserPreferencedCurrencyDisplay extends PureComponent { value: PropTypes.string, numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), hideLabel: PropTypes.bool, + hideTitle: PropTypes.bool, style: PropTypes.object, showEthLogo: PropTypes.bool, ethLogoHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), diff --git a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js index 7999301ad..42d156f92 100644 --- a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js +++ b/ui/app/components/app/user-preferenced-currency-display/user-preferenced-currency-display.container.js @@ -1,19 +1,26 @@ import { connect } from 'react-redux' import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display.component' -import { preferencesSelector } from '../../selectors' -import { ETH, PRIMARY, SECONDARY } from '../../constants/common' +import { preferencesSelector, getIsMainnet } from '../../../selectors/selectors' +import { ETH, PRIMARY, SECONDARY } from '../../../helpers/constants/common' const mapStateToProps = (state, ownProps) => { - const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state) + const { + useNativeCurrencyAsPrimaryCurrency, + showFiatInTestnets, + } = preferencesSelector(state) + + const isMainnet = getIsMainnet(state) return { useNativeCurrencyAsPrimaryCurrency, + showFiatInTestnets, + isMainnet, nativeCurrency: state.metamask.nativeCurrency, } } const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { useNativeCurrencyAsPrimaryCurrency, nativeCurrency, ...restStateProps } = stateProps + const { useNativeCurrencyAsPrimaryCurrency, showFiatInTestnets, isMainnet, nativeCurrency, ...restStateProps } = stateProps const { type, numberOfDecimals: propsNumberOfDecimals, @@ -40,6 +47,12 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { prefix = propsPrefix || fiatPrefix } + if (!isMainnet && !showFiatInTestnets) { + currency = nativeCurrency || ETH + numberOfDecimals = propsNumberOfDecimals || ethNumberOfDecimals || 6 + prefix = propsPrefix || ethPrefix + } + return { ...restStateProps, ...dispatchProps, diff --git a/ui/app/components/user-preferenced-currency-input/index.js b/ui/app/components/app/user-preferenced-currency-input/index.js index 4dc70db3d..4dc70db3d 100644 --- a/ui/app/components/user-preferenced-currency-input/index.js +++ b/ui/app/components/app/user-preferenced-currency-input/index.js diff --git a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js b/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js index 710b5d519..3802e16f3 100644 --- a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js +++ b/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import UserPreferencedCurrencyInput from '../user-preferenced-currency-input.component' -import CurrencyInput from '../../currency-input' +import CurrencyInput from '../../../ui/currency-input' describe('UserPreferencedCurrencyInput Component', () => { describe('rendering', () => { diff --git a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js b/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js index 959726443..959726443 100644 --- a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js +++ b/ui/app/components/app/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js diff --git a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js index 463e66b80..7c0ec1734 100644 --- a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js +++ b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.component.js @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import CurrencyInput from '../currency-input' +import CurrencyInput from '../../ui/currency-input' export default class UserPreferencedCurrencyInput extends PureComponent { static propTypes = { diff --git a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js index 0b88eb5a7..72f17fde4 100644 --- a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js +++ b/ui/app/components/app/user-preferenced-currency-input/user-preferenced-currency-input.container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux' import UserPreferencedCurrencyInput from './user-preferenced-currency-input.component' -import { preferencesSelector } from '../../selectors' +import { preferencesSelector } from '../../../selectors/selectors' const mapStateToProps = state => { const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state) diff --git a/ui/app/components/user-preferenced-token-input/index.js b/ui/app/components/app/user-preferenced-token-input/index.js index 54167e633..54167e633 100644 --- a/ui/app/components/user-preferenced-token-input/index.js +++ b/ui/app/components/app/user-preferenced-token-input/index.js diff --git a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js b/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js index d85bddeeb..41cfd51f9 100644 --- a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js +++ b/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import UserPreferencedTokenInput from '../user-preferenced-token-input.component' -import TokenInput from '../../token-input' +import TokenInput from '../../../ui/token-input' describe('UserPreferencedCurrencyInput Component', () => { describe('rendering', () => { diff --git a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js b/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js index 2f89fba90..2f89fba90 100644 --- a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js +++ b/ui/app/components/app/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js diff --git a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.js index 8f14231ac..24133188d 100644 --- a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js +++ b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.component.js @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import TokenInput from '../token-input' +import TokenInput from '../../ui/token-input' export default class UserPreferencedTokenInput extends PureComponent { static propTypes = { diff --git a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.js index 3305d4e29..4a20b20d9 100644 --- a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js +++ b/ui/app/components/app/user-preferenced-token-input/user-preferenced-token-input.container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux' import UserPreferencedTokenInput from './user-preferenced-token-input.component' -import { preferencesSelector } from '../../selectors' +import { preferencesSelector } from '../../../selectors/selectors' const mapStateToProps = state => { const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/app/wallet-view.js index e050e0ee6..cec8228b1 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/app/wallet-view.js @@ -6,16 +6,16 @@ const { withRouter } = require('react-router-dom') const { compose } = require('recompose') const inherits = require('util').inherits const classnames = require('classnames') -const { checksumAddress } = require('../util') -import Identicon from './identicon' +const { checksumAddress } = require('../../helpers/utils/util') +import Identicon from '../ui/identicon' // const AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns -const Tooltip = require('./tooltip-v2.js').default +const Tooltip = require('../ui/tooltip-v2.js').default const copyToClipboard = require('copy-to-clipboard') -const actions = require('../actions') -const BalanceComponent = require('./balance-component') +const actions = require('../../store/actions') +import BalanceComponent from '../ui/balance' const TokenList = require('./token-list') -const selectors = require('../selectors') -const { ADD_TOKEN_ROUTE } = require('../routes') +const selectors = require('../../selectors/selectors') +const { ADD_TOKEN_ROUTE } = require('../../helpers/constants/routes') import AddTokenButton from './add-token-button' @@ -26,6 +26,7 @@ module.exports = compose( WalletView.contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } WalletView.defaultProps = { @@ -38,8 +39,7 @@ function mapStateToProps (state) { network: state.metamask.network, sidebarOpen: state.appState.sidebar.isOpen, identities: state.metamask.identities, - accounts: state.metamask.accounts, - tokens: state.metamask.tokens, + accounts: selectors.getMetaMaskAccounts(state), keyrings: state.metamask.keyrings, selectedAddress: selectors.getSelectedAddress(state), selectedAccount: selectors.getSelectedAccount(state), @@ -106,10 +106,18 @@ WalletView.prototype.renderAddToken = function () { hideSidebar, history, } = this.props + const { metricsEvent } = this.context return h(AddTokenButton, { onClick () { history.push(ADD_TOKEN_ROUTE) + metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Token Menu', + name: 'Clicked "Add Token"', + }, + }) if (sidebarOpen) { hideSidebar() } @@ -125,10 +133,11 @@ WalletView.prototype.render = function () { showAccountDetailModal, hideSidebar, identities, + network, } = this.props // temporary logs + fake extra wallets - const checksummedAddress = checksumAddress(selectedAddress) + const checksummedAddress = checksumAddress(selectedAddress, network) if (!selectedAddress) { throw new Error('selectedAddress should not be ' + String(selectedAddress)) @@ -196,6 +205,13 @@ WalletView.prototype.render = function () { }), onClick: () => { copyToClipboard(checksummedAddress) + this.context.metricsEvent({ + eventOpts: { + category: 'Navigation', + action: 'Home', + name: 'Copied Address', + }, + }) this.setState({ hasCopied: true }) setTimeout(() => this.setState({ hasCopied: false }), 3000) }, diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js deleted file mode 100644 index 4e2769ee8..000000000 --- a/ui/app/components/balance-component.js +++ /dev/null @@ -1,111 +0,0 @@ -const Component = require('react').Component -const connect = require('react-redux').connect -const h = require('react-hyperscript') -const inherits = require('util').inherits -import TokenBalance from './token-balance' -import Identicon from './identicon' -import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display' -import { PRIMARY, SECONDARY } from '../constants/common' -const { getNativeCurrency, getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors') - -const { formatBalance } = require('../util') - -module.exports = connect(mapStateToProps)(BalanceComponent) - -function mapStateToProps (state) { - const accounts = state.metamask.accounts - const network = state.metamask.network - const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] - const account = accounts[selectedAddress] - - return { - account, - network, - nativeCurrency: getNativeCurrency(state), - conversionRate: conversionRateSelector(state), - currentCurrency: getCurrentCurrency(state), - assetImages: getAssetImages(state), - } -} - -inherits(BalanceComponent, Component) -function BalanceComponent () { - Component.call(this) -} - -BalanceComponent.prototype.render = function () { - const props = this.props - const { token, network, assetImages } = props - const address = token && token.address - const image = assetImages && address ? assetImages[token.address] : undefined - - return h('div.balance-container', {}, [ - - // TODO: balance icon needs to be passed in - // h('img.balance-icon', { - // src: '../images/eth_logo.svg', - // style: {}, - // }), - h(Identicon, { - diameter: 50, - address, - network, - image, - }), - - token ? this.renderTokenBalance() : this.renderBalance(), - ]) -} - -BalanceComponent.prototype.renderTokenBalance = function () { - const { token } = this.props - - return h('div.flex-column.balance-display', [ - h('div.token-amount', [ h(TokenBalance, { token }) ]), - ]) -} - -BalanceComponent.prototype.renderBalance = function () { - const props = this.props - const { account, nativeCurrency } = props - const balanceValue = account && account.balance - const needsParse = 'needsParse' in props ? props.needsParse : true - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse, nativeCurrency) : '...' - const showFiat = 'showFiat' in props ? props.showFiat : true - - if (formattedBalance === 'None' || formattedBalance === '...') { - return h('div.flex-column.balance-display', {}, [ - h('div.token-amount', { - style: {}, - }, formattedBalance), - ]) - } - - return h('div.flex-column.balance-display', {}, [ - h(UserPreferencedCurrencyDisplay, { - className: 'token-amount', - value: balanceValue, - type: PRIMARY, - ethNumberOfDecimals: 4, - }), - - showFiat && h(UserPreferencedCurrencyDisplay, { - value: balanceValue, - type: SECONDARY, - ethNumberOfDecimals: 4, - }), - ]) -} - -BalanceComponent.prototype.getFiatDisplayNumber = function (formattedBalance, conversionRate) { - if (formattedBalance === 'None') return formattedBalance - if (conversionRate === 0) return 'N/A' - - const splitBalance = formattedBalance.split(' ') - - const convertedNumber = (Number(splitBalance[0]) * conversionRate) - const wholePart = Math.floor(convertedNumber) - const decimalPart = convertedNumber - wholePart - - return wholePart + Number(decimalPart.toPrecision(2)) -} diff --git a/ui/app/components/confirm-page-container/index.scss b/ui/app/components/confirm-page-container/index.scss deleted file mode 100644 index af7a5b555..000000000 --- a/ui/app/components/confirm-page-container/index.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import './confirm-page-container-content/index'; - -@import './confirm-page-container-header/index'; - -@import './confirm-detail-row/index'; diff --git a/ui/app/components/currency-input/index.scss b/ui/app/components/currency-input/index.scss deleted file mode 100644 index fcb2db461..000000000 --- a/ui/app/components/currency-input/index.scss +++ /dev/null @@ -1,7 +0,0 @@ -.currency-input { - &__conversion-component { - font-size: 12px; - line-height: 12px; - padding-left: 1px; - } -} diff --git a/ui/app/components/currency-input/tests/currency-input.container.test.js b/ui/app/components/currency-input/tests/currency-input.container.test.js deleted file mode 100644 index 5d72958e6..000000000 --- a/ui/app/components/currency-input/tests/currency-input.container.test.js +++ /dev/null @@ -1,60 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' - -let mapStateToProps, mergeProps - -proxyquire('../currency-input.container.js', { - 'react-redux': { - connect: (ms, md, mp) => { - mapStateToProps = ms - mergeProps = mp - return () => ({}) - }, - }, -}) - -describe('CurrencyInput container', () => { - describe('mapStateToProps()', () => { - it('should return the correct props', () => { - const mockState = { - metamask: { - conversionRate: 280.45, - currentCurrency: 'usd', - nativeCurrency: 'ETH', - }, - } - - assert.deepEqual(mapStateToProps(mockState), { - conversionRate: 280.45, - currentCurrency: 'usd', - nativeCurrency: 'ETH', - }) - }) - }) - - describe('mergeProps()', () => { - it('should return the correct props', () => { - const mockStateProps = { - conversionRate: 280.45, - currentCurrency: 'usd', - nativeCurrency: 'ETH', - } - const mockDispatchProps = {} - - assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, { useFiat: true }), { - conversionRate: 280.45, - currentCurrency: 'usd', - nativeCurrency: 'ETH', - useFiat: true, - suffix: 'USD', - }) - - assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, {}), { - conversionRate: 280.45, - currentCurrency: 'usd', - nativeCurrency: 'ETH', - suffix: 'ETH', - }) - }) - }) -}) diff --git a/ui/app/components/dropdowns/account-dropdown-mini.js b/ui/app/components/dropdowns/account-dropdown-mini.js deleted file mode 100644 index 261eb0aa2..000000000 --- a/ui/app/components/dropdowns/account-dropdown-mini.js +++ /dev/null @@ -1,75 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const AccountListItem = require('../send/account-list-item/account-list-item.component').default - -module.exports = AccountDropdownMini - -inherits(AccountDropdownMini, Component) -function AccountDropdownMini () { - Component.call(this) -} - -AccountDropdownMini.prototype.getListItemIcon = function (currentAccount, selectedAccount) { - const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } }) - - return currentAccount.address === selectedAccount.address - ? listItemIcon - : null -} - -AccountDropdownMini.prototype.renderDropdown = function () { - const { - accounts, - selectedAccount, - closeDropdown, - onSelect, - } = this.props - - return h('div', {}, [ - - h('div.account-dropdown-mini__close-area', { - onClick: closeDropdown, - }), - - h('div.account-dropdown-mini__list', {}, [ - - ...accounts.map(account => h(AccountListItem, { - account, - displayBalance: false, - displayAddress: false, - handleClick: () => { - onSelect(account) - closeDropdown() - }, - icon: this.getListItemIcon(account, selectedAccount), - })), - - ]), - - ]) -} - -AccountDropdownMini.prototype.render = function () { - const { - selectedAccount, - openDropdown, - dropdownOpen, - } = this.props - - return h('div.account-dropdown-mini', {}, [ - - h(AccountListItem, { - account: selectedAccount, - handleClick: openDropdown, - displayBalance: false, - displayAddress: false, - icon: h(`i.fa.fa-caret-down.fa-lg`, { style: { color: '#dedede' } }), - }), - - dropdownOpen && this.renderDropdown(), - - ]) - -} - diff --git a/ui/app/components/dropdowns/components/network-dropdown-icon.js b/ui/app/components/dropdowns/components/network-dropdown-icon.js deleted file mode 100644 index a45da4c10..000000000 --- a/ui/app/components/dropdowns/components/network-dropdown-icon.js +++ /dev/null @@ -1,31 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') - - -inherits(NetworkDropdownIcon, Component) -module.exports = NetworkDropdownIcon - -function NetworkDropdownIcon () { - Component.call(this) -} - -NetworkDropdownIcon.prototype.render = function () { - const { - backgroundColor, - isSelected, - innerBorder = 'none', - diameter = '12', - } = this.props - - return h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {}, - h('div', { - style: { - background: backgroundColor, - border: innerBorder, - height: `${diameter}px`, - width: `${diameter}px`, - }, - }) - ) -} diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss deleted file mode 100644 index e27b0f182..000000000 --- a/ui/app/components/index.scss +++ /dev/null @@ -1,63 +0,0 @@ -@import './app-header/index'; - -@import './add-token-button/index'; - -@import './button-group/index'; - -@import './card/index'; - -@import './confirm-page-container/index'; - -@import './currency-input/index'; - -@import './currency-display/index'; - -@import './error-message/index'; - -@import './export-text-container/index'; - -@import './identicon/index'; - -@import './info-box/index'; - -@import './menu-bar/index'; - -@import './modal/index'; - -@import './modals/index'; - -@import './network-display/index'; - -@import './page-container/index'; - -@import './pages/index'; - -@import './provider-page-container/index'; - -@import './selected-account/index'; - -@import './sender-to-recipient/index'; - -@import './tabs/index'; - -@import './transaction-activity-log/index'; - -@import './transaction-breakdown/index'; - -@import './transaction-view/index'; - -@import './transaction-view-balance/index'; - -@import './transaction-list/index'; - -@import './transaction-list-item/index'; - -@import './transaction-list-item-details/index'; - -@import './transaction-status/index'; - -@import './app-header/index'; - -@import './sidebars/index'; - -@import './unit-input/index'; diff --git a/ui/app/components/menu-bar/menu-bar.component.js b/ui/app/components/menu-bar/menu-bar.component.js deleted file mode 100644 index 7460e8dd5..000000000 --- a/ui/app/components/menu-bar/menu-bar.component.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import Tooltip from '../tooltip' -import SelectedAccount from '../selected-account' -import AccountDetailsDropdown from '../dropdowns/account-details-dropdown.js' - -export default class MenuBar extends PureComponent { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - hideSidebar: PropTypes.func, - isMascara: PropTypes.bool, - sidebarOpen: PropTypes.bool, - showSidebar: PropTypes.func, - } - - state = { accountDetailsMenuOpen: false } - - render () { - const { t } = this.context - const { isMascara, sidebarOpen, hideSidebar, showSidebar } = this.props - const { accountDetailsMenuOpen } = this.state - - return ( - <div className="menu-bar"> - <Tooltip - title={t('menu')} - position="bottom" - > - <div - className="fa fa-bars menu-bar__sidebar-button" - onClick={() => sidebarOpen ? hideSidebar() : showSidebar()} - /> - </Tooltip> - <SelectedAccount /> - { - !isMascara && ( - <Tooltip - title={t('accountOptions')} - position="bottom" - > - <div - className="fa fa-ellipsis-h fa-lg menu-bar__open-in-browser" - onClick={() => this.setState({ accountDetailsMenuOpen: true })} - > - </div> - </Tooltip> - ) - } - { - accountDetailsMenuOpen && ( - <AccountDetailsDropdown - className="menu-bar__account-details-dropdown" - onClose={() => this.setState({ accountDetailsMenuOpen: false })} - /> - ) - } - </div> - ) - } -} diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss deleted file mode 100644 index 45453a582..000000000 --- a/ui/app/components/modals/index.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import './cancel-transaction/index'; - -@import './confirm-remove-account/index'; - -@import './customize-gas/index'; - -@import './qr-scanner/index'; - -@import './transaction-confirmed/index'; diff --git a/ui/app/components/modals/welcome-beta/index.js b/ui/app/components/modals/welcome-beta/index.js deleted file mode 100644 index 49e45b9d7..000000000 --- a/ui/app/components/modals/welcome-beta/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './welcome-beta.container' diff --git a/ui/app/components/modals/welcome-beta/welcome-beta.container.js b/ui/app/components/modals/welcome-beta/welcome-beta.container.js deleted file mode 100644 index c5123ad47..000000000 --- a/ui/app/components/modals/welcome-beta/welcome-beta.container.js +++ /dev/null @@ -1,4 +0,0 @@ -import WelcomeBeta from './welcome-beta.component' -import withModalProps from '../../../higher-order-components/with-modal-props' - -export default withModalProps(WelcomeBeta) diff --git a/ui/app/components/pages/add-token/add-token.component.js b/ui/app/components/pages/add-token/add-token.component.js deleted file mode 100644 index 3612e676c..000000000 --- a/ui/app/components/pages/add-token/add-token.component.js +++ /dev/null @@ -1,319 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import ethUtil from 'ethereumjs-util' -import { checkExistingAddresses } from './util' -import { tokenInfoGetter } from '../../../token-util' -import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes' -import TextField from '../../text-field' -import TokenList from './token-list' -import TokenSearch from './token-search' -import PageContainer from '../../page-container' -import { Tabs, Tab } from '../../tabs' - -const emptyAddr = '0x0000000000000000000000000000000000000000' -const SEARCH_TAB = 'SEARCH' -const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN' - -class AddToken extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - history: PropTypes.object, - setPendingTokens: PropTypes.func, - pendingTokens: PropTypes.object, - clearPendingTokens: PropTypes.func, - tokens: PropTypes.array, - identities: PropTypes.object, - } - - constructor (props) { - super(props) - - this.state = { - customAddress: '', - customSymbol: '', - customDecimals: 0, - searchResults: [], - selectedTokens: {}, - tokenSelectorError: null, - customAddressError: null, - customSymbolError: null, - customDecimalsError: null, - autoFilled: false, - displayedTab: SEARCH_TAB, - } - } - - componentDidMount () { - this.tokenInfoGetter = tokenInfoGetter() - const { pendingTokens = {} } = this.props - const pendingTokenKeys = Object.keys(pendingTokens) - - if (pendingTokenKeys.length > 0) { - let selectedTokens = {} - let customToken = {} - - pendingTokenKeys.forEach(tokenAddress => { - const token = pendingTokens[tokenAddress] - const { isCustom } = token - - if (isCustom) { - customToken = { ...token } - } else { - selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } } - } - }) - - const { - address: customAddress = '', - symbol: customSymbol = '', - decimals: customDecimals = 0, - } = customToken - - const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB - this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab }) - } - } - - handleToggleToken (token) { - const { address } = token - const { selectedTokens = {} } = this.state - const selectedTokensCopy = { ...selectedTokens } - - if (address in selectedTokensCopy) { - delete selectedTokensCopy[address] - } else { - selectedTokensCopy[address] = token - } - - this.setState({ - selectedTokens: selectedTokensCopy, - tokenSelectorError: null, - }) - } - - hasError () { - const { - tokenSelectorError, - customAddressError, - customSymbolError, - customDecimalsError, - } = this.state - - return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError - } - - hasSelected () { - const { customAddress = '', selectedTokens = {} } = this.state - return customAddress || Object.keys(selectedTokens).length > 0 - } - - handleNext () { - if (this.hasError()) { - return - } - - if (!this.hasSelected()) { - this.setState({ tokenSelectorError: this.context.t('mustSelectOne') }) - return - } - - const { setPendingTokens, history } = this.props - const { - customAddress: address, - customSymbol: symbol, - customDecimals: decimals, - selectedTokens, - } = this.state - - const customToken = { - address, - symbol, - decimals, - } - - setPendingTokens({ customToken, selectedTokens }) - history.push(CONFIRM_ADD_TOKEN_ROUTE) - } - - async attemptToAutoFillTokenParams (address) { - const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address) - - const autoFilled = Boolean(symbol && decimals) - this.setState({ autoFilled }) - this.handleCustomSymbolChange(symbol || '') - this.handleCustomDecimalsChange(decimals) - } - - handleCustomAddressChange (value) { - const customAddress = value.trim() - this.setState({ - customAddress, - customAddressError: null, - tokenSelectorError: null, - autoFilled: false, - }) - - const isValidAddress = ethUtil.isValidAddress(customAddress) - const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase() - - switch (true) { - case !isValidAddress: - this.setState({ - customAddressError: this.context.t('invalidAddress'), - customSymbol: '', - customDecimals: 0, - customSymbolError: null, - customDecimalsError: null, - }) - - break - case Boolean(this.props.identities[standardAddress]): - this.setState({ - customAddressError: this.context.t('personalAddressDetected'), - }) - - break - case checkExistingAddresses(customAddress, this.props.tokens): - this.setState({ - customAddressError: this.context.t('tokenAlreadyAdded'), - }) - - break - default: - if (customAddress !== emptyAddr) { - this.attemptToAutoFillTokenParams(customAddress) - } - } - } - - handleCustomSymbolChange (value) { - const customSymbol = value.trim() - const symbolLength = customSymbol.length - let customSymbolError = null - - if (symbolLength <= 0 || symbolLength >= 10) { - customSymbolError = this.context.t('symbolBetweenZeroTen') - } - - this.setState({ customSymbol, customSymbolError }) - } - - handleCustomDecimalsChange (value) { - const customDecimals = value.trim() - const validDecimals = customDecimals !== null && - customDecimals !== '' && - customDecimals >= 0 && - customDecimals <= 36 - let customDecimalsError = null - - if (!validDecimals) { - customDecimalsError = this.context.t('decimalsMustZerotoTen') - } - - this.setState({ customDecimals, customDecimalsError }) - } - - renderCustomTokenForm () { - const { - customAddress, - customSymbol, - customDecimals, - customAddressError, - customSymbolError, - customDecimalsError, - autoFilled, - } = this.state - - return ( - <div className="add-token__custom-token-form"> - <TextField - id="custom-address" - label={this.context.t('tokenAddress')} - type="text" - value={customAddress} - onChange={e => this.handleCustomAddressChange(e.target.value)} - error={customAddressError} - fullWidth - margin="normal" - /> - <TextField - id="custom-symbol" - label={this.context.t('tokenSymbol')} - type="text" - value={customSymbol} - onChange={e => this.handleCustomSymbolChange(e.target.value)} - error={customSymbolError} - fullWidth - margin="normal" - disabled={autoFilled} - /> - <TextField - id="custom-decimals" - label={this.context.t('decimal')} - type="number" - value={customDecimals} - onChange={e => this.handleCustomDecimalsChange(e.target.value)} - error={customDecimalsError} - fullWidth - margin="normal" - disabled={autoFilled} - /> - </div> - ) - } - - renderSearchToken () { - const { tokenSelectorError, selectedTokens, searchResults } = this.state - - return ( - <div className="add-token__search-token"> - <TokenSearch - onSearch={({ results = [] }) => this.setState({ searchResults: results })} - error={tokenSelectorError} - /> - <div className="add-token__token-list"> - <TokenList - results={searchResults} - selectedTokens={selectedTokens} - onToggleToken={token => this.handleToggleToken(token)} - /> - </div> - </div> - ) - } - - renderTabs () { - return ( - <Tabs> - <Tab name={this.context.t('search')}> - { this.renderSearchToken() } - </Tab> - <Tab name={this.context.t('customToken')}> - { this.renderCustomTokenForm() } - </Tab> - </Tabs> - ) - } - - render () { - const { history, clearPendingTokens } = this.props - - return ( - <PageContainer - title={this.context.t('addTokens')} - tabsComponent={this.renderTabs()} - onSubmit={() => this.handleNext()} - disabled={this.hasError() || !this.hasSelected()} - onCancel={() => { - clearPendingTokens() - history.push(DEFAULT_ROUTE) - }} - /> - ) - } -} - -export default AddToken diff --git a/ui/app/components/pages/add-token/add-token.container.js b/ui/app/components/pages/add-token/add-token.container.js deleted file mode 100644 index 87671b156..000000000 --- a/ui/app/components/pages/add-token/add-token.container.js +++ /dev/null @@ -1,22 +0,0 @@ -import { connect } from 'react-redux' -import AddToken from './add-token.component' - -const { setPendingTokens, clearPendingTokens } = require('../../../actions') - -const mapStateToProps = ({ metamask }) => { - const { identities, tokens, pendingTokens } = metamask - return { - identities, - tokens, - pendingTokens, - } -} - -const mapDispatchToProps = dispatch => { - return { - setPendingTokens: tokens => dispatch(setPendingTokens(tokens)), - clearPendingTokens: () => dispatch(clearPendingTokens()), - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(AddToken) diff --git a/ui/app/components/pages/add-token/index.js b/ui/app/components/pages/add-token/index.js deleted file mode 100644 index 3666cae82..000000000 --- a/ui/app/components/pages/add-token/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import AddToken from './add-token.container' -module.exports = AddToken diff --git a/ui/app/components/pages/add-token/index.scss b/ui/app/components/pages/add-token/index.scss deleted file mode 100644 index 39e86b97b..000000000 --- a/ui/app/components/pages/add-token/index.scss +++ /dev/null @@ -1,25 +0,0 @@ -@import './token-list/index'; - -.add-token { - &__custom-token-form { - padding: 8px 16px 16px; - - input[type="number"]::-webkit-inner-spin-button { - -webkit-appearance: none; - display: none; - } - - input[type="number"]:hover::-webkit-inner-spin-button { - -webkit-appearance: none; - display: none; - } - } - - &__search-token { - padding: 16px; - } - - &__token-list { - margin-top: 16px; - } -} diff --git a/ui/app/components/pages/add-token/token-list/index.js b/ui/app/components/pages/add-token/token-list/index.js deleted file mode 100644 index 21dd5ac72..000000000 --- a/ui/app/components/pages/add-token/token-list/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import TokenList from './token-list.container' -module.exports = TokenList diff --git a/ui/app/components/pages/add-token/token-list/index.scss b/ui/app/components/pages/add-token/token-list/index.scss deleted file mode 100644 index e32739d59..000000000 --- a/ui/app/components/pages/add-token/token-list/index.scss +++ /dev/null @@ -1,65 +0,0 @@ -@import './token-list-placeholder/index'; - -.token-list { - &__title { - font-size: .75rem; - } - - &__tokens-container { - display: flex; - flex-direction: column; - } - - &__token { - transition: 200ms ease-in-out; - display: flex; - flex-flow: row nowrap; - align-items: center; - padding: 8px; - margin-top: 8px; - box-sizing: border-box; - border-radius: 10px; - cursor: pointer; - border: 2px solid transparent; - position: relative; - - &:hover { - border: 2px solid rgba($malibu-blue, .5); - } - - &--selected { - border: 2px solid $malibu-blue !important; - } - - &--disabled { - opacity: .4; - pointer-events: none; - } - } - - &__token-icon { - width: 48px; - height: 48px; - background-repeat: no-repeat; - background-size: contain; - background-position: center; - border-radius: 50%; - background-color: $white; - box-shadow: 0 2px 4px 0 rgba($black, .24); - margin-right: 12px; - flex: 0 0 auto; - } - - &__token-data { - display: flex; - flex-direction: row; - align-items: center; - min-width: 0; - } - - &__token-name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } -} diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js deleted file mode 100644 index b82f45e93..000000000 --- a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import TokenListPlaceholder from './token-list-placeholder.component' -module.exports = TokenListPlaceholder diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss deleted file mode 100644 index cc495dfb0..000000000 --- a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss +++ /dev/null @@ -1,23 +0,0 @@ -.token-list-placeholder { - display: flex; - align-items: center; - padding-top: 36px; - flex-direction: column; - line-height: 22px; - opacity: .5; - - &__text { - color: $silver-chalice; - width: 50%; - text-align: center; - margin-top: 8px; - - @media screen and (max-width: 575px) { - width: 60%; - } - } - - &__link { - color: $curious-blue; - } -} diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js deleted file mode 100644 index 20f550927..000000000 --- a/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js +++ /dev/null @@ -1,27 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' - -export default class TokenListPlaceholder extends Component { - static contextTypes = { - t: PropTypes.func, - } - - render () { - return ( - <div className="token-list-placeholder"> - <img src="images/tokensearch.svg" /> - <div className="token-list-placeholder__text"> - { this.context.t('addAcquiredTokens') } - </div> - <a - className="token-list-placeholder__link" - href="https://metamask.zendesk.com/hc/en-us/articles/360015489031" - target="_blank" - rel="noopener noreferrer" - > - { this.context.t('learnMore') } - </a> - </div> - ) - } -} diff --git a/ui/app/components/pages/add-token/token-list/token-list.component.js b/ui/app/components/pages/add-token/token-list/token-list.component.js deleted file mode 100644 index 724a68d6e..000000000 --- a/ui/app/components/pages/add-token/token-list/token-list.component.js +++ /dev/null @@ -1,60 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import { checkExistingAddresses } from '../util' -import TokenListPlaceholder from './token-list-placeholder' - -export default class InfoBox extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - tokens: PropTypes.array, - results: PropTypes.array, - selectedTokens: PropTypes.object, - onToggleToken: PropTypes.func, - } - - render () { - const { results = [], selectedTokens = {}, onToggleToken, tokens = [] } = this.props - - return results.length === 0 - ? <TokenListPlaceholder /> - : ( - <div className="token-list"> - <div className="token-list__title"> - { this.context.t('searchResults') } - </div> - <div className="token-list__tokens-container"> - { - Array(6).fill(undefined) - .map((_, i) => { - const { logo, symbol, name, address } = results[i] || {} - const tokenAlreadyAdded = checkExistingAddresses(address, tokens) - - return Boolean(logo || symbol || name) && ( - <div - className={classnames('token-list__token', { - 'token-list__token--selected': selectedTokens[address], - 'token-list__token--disabled': tokenAlreadyAdded, - })} - onClick={() => !tokenAlreadyAdded && onToggleToken(results[i])} - key={i} - > - <div - className="token-list__token-icon" - style={{ backgroundImage: logo && `url(images/contract/${logo})` }}> - </div> - <div className="token-list__token-data"> - <span className="token-list__token-name">{ `${name} (${symbol})` }</span> - </div> - </div> - ) - }) - } - </div> - </div> - ) - } -} diff --git a/ui/app/components/pages/add-token/token-list/token-list.container.js b/ui/app/components/pages/add-token/token-list/token-list.container.js deleted file mode 100644 index cd7b07a37..000000000 --- a/ui/app/components/pages/add-token/token-list/token-list.container.js +++ /dev/null @@ -1,11 +0,0 @@ -import { connect } from 'react-redux' -import TokenList from './token-list.component' - -const mapStateToProps = ({ metamask }) => { - const { tokens } = metamask - return { - tokens, - } -} - -export default connect(mapStateToProps)(TokenList) diff --git a/ui/app/components/pages/add-token/token-search/index.js b/ui/app/components/pages/add-token/token-search/index.js deleted file mode 100644 index acaa6b084..000000000 --- a/ui/app/components/pages/add-token/token-search/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import TokenSearch from './token-search.component' -module.exports = TokenSearch diff --git a/ui/app/components/pages/add-token/token-search/token-search.component.js b/ui/app/components/pages/add-token/token-search/token-search.component.js deleted file mode 100644 index 036b2db1e..000000000 --- a/ui/app/components/pages/add-token/token-search/token-search.component.js +++ /dev/null @@ -1,85 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import contractMap from 'eth-contract-metadata' -import Fuse from 'fuse.js' -import InputAdornment from '@material-ui/core/InputAdornment' -import TextField from '../../../text-field' - -const contractList = Object.entries(contractMap) - .map(([ _, tokenData]) => tokenData) - .filter(tokenData => Boolean(tokenData.erc20)) - -const fuse = new Fuse(contractList, { - shouldSort: true, - threshold: 0.45, - location: 0, - distance: 100, - maxPatternLength: 32, - minMatchCharLength: 1, - keys: [ - { name: 'name', weight: 0.5 }, - { name: 'symbol', weight: 0.5 }, - ], -}) - -export default class TokenSearch extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static defaultProps = { - error: null, - } - - static propTypes = { - onSearch: PropTypes.func, - error: PropTypes.string, - } - - constructor (props) { - super(props) - - this.state = { - searchQuery: '', - } - } - - handleSearch (searchQuery) { - this.setState({ searchQuery }) - const fuseSearchResult = fuse.search(searchQuery) - const addressSearchResult = contractList.filter(token => { - return token.address.toLowerCase() === searchQuery.toLowerCase() - }) - const results = [...addressSearchResult, ...fuseSearchResult] - this.props.onSearch({ searchQuery, results }) - } - - renderAdornment () { - return ( - <InputAdornment - position="start" - style={{ marginRight: '12px' }} - > - <img src="images/search.svg" /> - </InputAdornment> - ) - } - - render () { - const { error } = this.props - const { searchQuery } = this.state - - return ( - <TextField - id="search-tokens" - placeholder={this.context.t('searchTokens')} - type="text" - value={searchQuery} - onChange={e => this.handleSearch(e.target.value)} - error={error} - fullWidth - startAdornment={this.renderAdornment()} - /> - ) - } -} diff --git a/ui/app/components/pages/add-token/util.js b/ui/app/components/pages/add-token/util.js deleted file mode 100644 index 579c56cc0..000000000 --- a/ui/app/components/pages/add-token/util.js +++ /dev/null @@ -1,13 +0,0 @@ -import R from 'ramda' - -export function checkExistingAddresses (address, tokenList = []) { - if (!address) { - return false - } - - const matchesAddress = existingToken => { - return existingToken.address.toLowerCase() === address.toLowerCase() - } - - return R.any(matchesAddress)(tokenList) -} diff --git a/ui/app/components/pages/authenticated.js b/ui/app/components/pages/authenticated.js deleted file mode 100644 index 1f6b0be49..000000000 --- a/ui/app/components/pages/authenticated.js +++ /dev/null @@ -1,34 +0,0 @@ -const { connect } = require('react-redux') -const PropTypes = require('prop-types') -const { Redirect } = require('react-router-dom') -const h = require('react-hyperscript') -const MetamaskRoute = require('./metamask-route') -const { UNLOCK_ROUTE, INITIALIZE_ROUTE } = require('../../routes') - -const Authenticated = props => { - const { isUnlocked, isInitialized } = props - - switch (true) { - case isUnlocked && isInitialized: - return h(MetamaskRoute, { ...props }) - case !isInitialized: - return h(Redirect, { to: { pathname: INITIALIZE_ROUTE } }) - default: - return h(Redirect, { to: { pathname: UNLOCK_ROUTE } }) - } -} - -Authenticated.propTypes = { - isUnlocked: PropTypes.bool, - isInitialized: PropTypes.bool, -} - -const mapStateToProps = state => { - const { metamask: { isUnlocked, isInitialized } } = state - return { - isUnlocked, - isInitialized, - } -} - -module.exports = connect(mapStateToProps)(Authenticated) diff --git a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js deleted file mode 100644 index ee5d6fa64..000000000 --- a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js +++ /dev/null @@ -1,122 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { DEFAULT_ROUTE } from '../../../routes' -import Button from '../../button' -import Identicon from '../../../components/identicon' -import TokenBalance from '../../token-balance' - -export default class ConfirmAddSuggestedToken extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - history: PropTypes.object, - clearPendingTokens: PropTypes.func, - addToken: PropTypes.func, - pendingTokens: PropTypes.object, - removeSuggestedTokens: PropTypes.func, - } - - componentDidMount () { - const { pendingTokens = {}, history } = this.props - - if (Object.keys(pendingTokens).length === 0) { - history.push(DEFAULT_ROUTE) - } - } - - getTokenName (name, symbol) { - return typeof name === 'undefined' - ? symbol - : `${name} (${symbol})` - } - - render () { - const { addToken, pendingTokens, removeSuggestedTokens, history } = this.props - const pendingTokenKey = Object.keys(pendingTokens)[0] - const pendingToken = pendingTokens[pendingTokenKey] - - return ( - <div className="page-container"> - <div className="page-container__header"> - <div className="page-container__title"> - { this.context.t('addSuggestedTokens') } - </div> - <div className="page-container__subtitle"> - { this.context.t('likeToAddTokens') } - </div> - </div> - <div className="page-container__content"> - <div className="confirm-add-token"> - <div className="confirm-add-token__header"> - <div className="confirm-add-token__token"> - { this.context.t('token') } - </div> - <div className="confirm-add-token__balance"> - { this.context.t('balance') } - </div> - </div> - <div className="confirm-add-token__token-list"> - { - Object.entries(pendingTokens) - .map(([ address, token ]) => { - const { name, symbol, image } = token - - return ( - <div - className="confirm-add-token__token-list-item" - key={address} - > - <div className="confirm-add-token__token confirm-add-token__data"> - <Identicon - className="confirm-add-token__token-icon" - diameter={48} - address={address} - image={image} - /> - <div className="confirm-add-token__name"> - { this.getTokenName(name, symbol) } - </div> - </div> - <div className="confirm-add-token__balance"> - <TokenBalance token={token} /> - </div> - </div> - ) - }) - } - </div> - </div> - </div> - <div className="page-container__footer"> - <header> - <Button - type="default" - large - className="page-container__footer-button" - onClick={() => { - removeSuggestedTokens() - .then(() => history.push(DEFAULT_ROUTE)) - }} - > - { this.context.t('cancel') } - </Button> - <Button - type="primary" - large - className="page-container__footer-button" - onClick={() => { - addToken(pendingToken) - .then(() => removeSuggestedTokens()) - .then(() => history.push(DEFAULT_ROUTE)) - }} - > - { this.context.t('addToken') } - </Button> - </header> - </div> - </div> - ) - } -} diff --git a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js b/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js deleted file mode 100644 index 1f2737e52..000000000 --- a/ui/app/components/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js +++ /dev/null @@ -1,29 +0,0 @@ -import { connect } from 'react-redux' -import { compose } from 'recompose' -import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component' -import { withRouter } from 'react-router-dom' - -const extend = require('xtend') - -const { addToken, removeSuggestedTokens } = require('../../../actions') - -const mapStateToProps = ({ metamask }) => { - const { pendingTokens, suggestedTokens } = metamask - const params = extend(pendingTokens, suggestedTokens) - - return { - pendingTokens: params, - } -} - -const mapDispatchToProps = dispatch => { - return { - addToken: ({address, symbol, decimals, image}) => dispatch(addToken(address, symbol, decimals, image)), - removeSuggestedTokens: () => dispatch(removeSuggestedTokens()), - } -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(ConfirmAddSuggestedToken) diff --git a/ui/app/components/pages/confirm-add-suggested-token/index.js b/ui/app/components/pages/confirm-add-suggested-token/index.js deleted file mode 100644 index 2ca56b43c..000000000 --- a/ui/app/components/pages/confirm-add-suggested-token/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import ConfirmAddSuggestedToken from './confirm-add-suggested-token.container' -module.exports = ConfirmAddSuggestedToken diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js deleted file mode 100644 index d3fec79d7..000000000 --- a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js +++ /dev/null @@ -1,117 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes' -import Button from '../../button' -import Identicon from '../../identicon' -import TokenBalance from '../../token-balance' - -export default class ConfirmAddToken extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - history: PropTypes.object, - clearPendingTokens: PropTypes.func, - addTokens: PropTypes.func, - pendingTokens: PropTypes.object, - } - - componentDidMount () { - const { pendingTokens = {}, history } = this.props - - if (Object.keys(pendingTokens).length === 0) { - history.push(DEFAULT_ROUTE) - } - } - - getTokenName (name, symbol) { - return typeof name === 'undefined' - ? symbol - : `${name} (${symbol})` - } - - render () { - const { history, addTokens, clearPendingTokens, pendingTokens } = this.props - - return ( - <div className="page-container"> - <div className="page-container__header"> - <div className="page-container__title"> - { this.context.t('addTokens') } - </div> - <div className="page-container__subtitle"> - { this.context.t('likeToAddTokens') } - </div> - </div> - <div className="page-container__content"> - <div className="confirm-add-token"> - <div className="confirm-add-token__header"> - <div className="confirm-add-token__token"> - { this.context.t('token') } - </div> - <div className="confirm-add-token__balance"> - { this.context.t('balance') } - </div> - </div> - <div className="confirm-add-token__token-list"> - { - Object.entries(pendingTokens) - .map(([ address, token ]) => { - const { name, symbol } = token - - return ( - <div - className="confirm-add-token__token-list-item" - key={address} - > - <div className="confirm-add-token__token confirm-add-token__data"> - <Identicon - className="confirm-add-token__token-icon" - diameter={48} - address={address} - /> - <div className="confirm-add-token__name"> - { this.getTokenName(name, symbol) } - </div> - </div> - <div className="confirm-add-token__balance"> - <TokenBalance token={token} /> - </div> - </div> - ) - }) - } - </div> - </div> - </div> - <div className="page-container__footer"> - <header> - <Button - type="default" - large - className="page-container__footer-button" - onClick={() => history.push(ADD_TOKEN_ROUTE)} - > - { this.context.t('back') } - </Button> - <Button - type="primary" - large - className="page-container__footer-button" - onClick={() => { - addTokens(pendingTokens) - .then(() => { - clearPendingTokens() - history.push(DEFAULT_ROUTE) - }) - }} - > - { this.context.t('addTokens') } - </Button> - </header> - </div> - </div> - ) - } -} diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js deleted file mode 100644 index 0190024d9..000000000 --- a/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js +++ /dev/null @@ -1,20 +0,0 @@ -import { connect } from 'react-redux' -import ConfirmAddToken from './confirm-add-token.component' - -const { addTokens, clearPendingTokens } = require('../../../actions') - -const mapStateToProps = ({ metamask }) => { - const { pendingTokens } = metamask - return { - pendingTokens, - } -} - -const mapDispatchToProps = dispatch => { - return { - addTokens: tokens => dispatch(addTokens(tokens)), - clearPendingTokens: () => dispatch(clearPendingTokens()), - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken) diff --git a/ui/app/components/pages/confirm-add-token/index.js b/ui/app/components/pages/confirm-add-token/index.js deleted file mode 100644 index b7decabec..000000000 --- a/ui/app/components/pages/confirm-add-token/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import ConfirmAddToken from './confirm-add-token.container' -module.exports = ConfirmAddToken diff --git a/ui/app/components/pages/confirm-add-token/index.scss b/ui/app/components/pages/confirm-add-token/index.scss deleted file mode 100644 index 66146cf78..000000000 --- a/ui/app/components/pages/confirm-add-token/index.scss +++ /dev/null @@ -1,69 +0,0 @@ -.confirm-add-token { - padding: 16px; - - &__header { - font-size: .75rem; - display: flex; - } - - &__token { - flex: 1; - min-width: 0; - } - - &__balance { - flex: 0 0 30%; - min-width: 0; - } - - &__token-list { - display: flex; - flex-flow: column nowrap; - - .token-balance { - display: flex; - flex-flow: row nowrap; - align-items: flex-start; - - &__amount { - color: $scorpion; - font-size: 43px; - line-height: 43px; - margin-right: 8px; - } - - &__symbol { - color: $scorpion; - font-size: 16px; - font-weight: 400; - line-height: 24px; - } - } - } - - &__token-list-item { - display: flex; - flex-flow: row nowrap; - align-items: center; - margin-top: 8px; - box-sizing: border-box; - } - - &__data { - display: flex; - align-items: center; - padding: 8px; - } - - &__name { - min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - &__token-icon { - margin-right: 12px; - flex: 0 0 auto; - } -} diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.component.js b/ui/app/components/pages/confirm-approve/confirm-approve.component.js deleted file mode 100644 index b71eaa1d4..000000000 --- a/ui/app/components/pages/confirm-approve/confirm-approve.component.js +++ /dev/null @@ -1,21 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' - -export default class ConfirmApprove extends Component { - static propTypes = { - tokenAmount: PropTypes.number, - tokenSymbol: PropTypes.string, - } - - render () { - const { tokenAmount, tokenSymbol } = this.props - - return ( - <ConfirmTokenTransactionBase - tokenAmount={tokenAmount} - warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`} - /> - ) - } -} diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.container.js b/ui/app/components/pages/confirm-approve/confirm-approve.container.js deleted file mode 100644 index 4ef9f4ced..000000000 --- a/ui/app/components/pages/confirm-approve/confirm-approve.container.js +++ /dev/null @@ -1,15 +0,0 @@ -import { connect } from 'react-redux' -import ConfirmApprove from './confirm-approve.component' -import { approveTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction' - -const mapStateToProps = state => { - const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state - const { tokenAmount } = approveTokenAmountAndToAddressSelector(state) - - return { - tokenAmount, - tokenSymbol, - } -} - -export default connect(mapStateToProps)(ConfirmApprove) diff --git a/ui/app/components/pages/confirm-approve/index.js b/ui/app/components/pages/confirm-approve/index.js deleted file mode 100644 index 791297be7..000000000 --- a/ui/app/components/pages/confirm-approve/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './confirm-approve.container' diff --git a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js deleted file mode 100644 index 9bc0daab9..000000000 --- a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js +++ /dev/null @@ -1,64 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import ethUtil from 'ethereumjs-util' -import ConfirmTransactionBase from '../confirm-transaction-base' - -export default class ConfirmDeployContract extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - txData: PropTypes.object, - } - - renderData () { - const { t } = this.context - const { - txData: { - origin, - txParams: { - data, - } = {}, - } = {}, - } = this.props - - return ( - <div className="confirm-page-container-content__data"> - <div className="confirm-page-container-content__data-box"> - <div className="confirm-page-container-content__data-field"> - <div className="confirm-page-container-content__data-field-label"> - { `${t('origin')}:` } - </div> - <div> - { origin } - </div> - </div> - <div className="confirm-page-container-content__data-field"> - <div className="confirm-page-container-content__data-field-label"> - { `${t('bytes')}:` } - </div> - <div> - { ethUtil.toBuffer(data).length } - </div> - </div> - </div> - <div className="confirm-page-container-content__data-box-label"> - { `${t('hexData')}:` } - </div> - <div className="confirm-page-container-content__data-box"> - { data } - </div> - </div> - ) - } - - render () { - return ( - <ConfirmTransactionBase - action={this.context.t('contractDeployment')} - dataComponent={this.renderData()} - /> - ) - } -} diff --git a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js deleted file mode 100644 index 336ee83ea..000000000 --- a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from 'react-redux' -import ConfirmDeployContract from './confirm-deploy-contract.component' - -const mapStateToProps = state => { - const { confirmTransaction: { txData } = {} } = state - - return { - txData, - } -} - -export default connect(mapStateToProps)(ConfirmDeployContract) diff --git a/ui/app/components/pages/confirm-deploy-contract/index.js b/ui/app/components/pages/confirm-deploy-contract/index.js deleted file mode 100644 index c4fb01b52..000000000 --- a/ui/app/components/pages/confirm-deploy-contract/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './confirm-deploy-contract.container' diff --git a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js deleted file mode 100644 index 442a478b8..000000000 --- a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js +++ /dev/null @@ -1,39 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import ConfirmTransactionBase from '../confirm-transaction-base' -import { SEND_ROUTE } from '../../../routes' - -export default class ConfirmSendEther extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - editTransaction: PropTypes.func, - history: PropTypes.object, - txParams: PropTypes.object, - } - - handleEdit ({ txData }) { - const { editTransaction, history } = this.props - editTransaction(txData) - history.push(SEND_ROUTE) - } - - shouldHideData () { - const { txParams = {} } = this.props - return !txParams.data - } - - render () { - const hideData = this.shouldHideData() - - return ( - <ConfirmTransactionBase - action={this.context.t('confirm')} - hideData={hideData} - onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)} - /> - ) - } -} diff --git a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js deleted file mode 100644 index e48ef54a8..000000000 --- a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js +++ /dev/null @@ -1,45 +0,0 @@ -import { connect } from 'react-redux' -import { compose } from 'recompose' -import { withRouter } from 'react-router-dom' -import { updateSend } from '../../../actions' -import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck' -import ConfirmSendEther from './confirm-send-ether.component' - -const mapStateToProps = state => { - const { confirmTransaction: { txData: { txParams } = {} } } = state - - return { - txParams, - } -} - -const mapDispatchToProps = dispatch => { - return { - editTransaction: txData => { - const { id, txParams } = txData - const { - gas: gasLimit, - gasPrice, - to, - value: amount, - } = txParams - - dispatch(updateSend({ - gasLimit, - gasPrice, - gasTotal: null, - to, - amount, - errors: { to: null, amount: null }, - editingTransactionId: id && id.toString(), - })) - - dispatch(clearConfirmTransaction()) - }, - } -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(ConfirmSendEther) diff --git a/ui/app/components/pages/confirm-send-ether/index.js b/ui/app/components/pages/confirm-send-ether/index.js deleted file mode 100644 index 2d5767c39..000000000 --- a/ui/app/components/pages/confirm-send-ether/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './confirm-send-ether.container' diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js deleted file mode 100644 index cb39e3d7b..000000000 --- a/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js +++ /dev/null @@ -1,29 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' -import { SEND_ROUTE } from '../../../routes' - -export default class ConfirmSendToken extends Component { - static propTypes = { - history: PropTypes.object, - editTransaction: PropTypes.func, - tokenAmount: PropTypes.number, - } - - handleEdit (confirmTransactionData) { - const { editTransaction, history } = this.props - editTransaction(confirmTransactionData) - history.push(SEND_ROUTE) - } - - render () { - const { tokenAmount } = this.props - - return ( - <ConfirmTokenTransactionBase - onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)} - tokenAmount={tokenAmount} - /> - ) - } -} diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js deleted file mode 100644 index d60911e59..000000000 --- a/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js +++ /dev/null @@ -1,52 +0,0 @@ -import { connect } from 'react-redux' -import { compose } from 'recompose' -import { withRouter } from 'react-router-dom' -import ConfirmSendToken from './confirm-send-token.component' -import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck' -import { setSelectedToken, updateSend, showSendTokenPage } from '../../../actions' -import { conversionUtil } from '../../../conversion-util' -import { sendTokenTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction' - -const mapStateToProps = state => { - const { tokenAmount } = sendTokenTokenAmountAndToAddressSelector(state) - - return { - tokenAmount, - } -} - -const mapDispatchToProps = dispatch => { - return { - editTransaction: ({ txData, tokenData, tokenProps }) => { - const { txParams: { to: tokenAddress, gas: gasLimit, gasPrice } = {}, id } = txData - const { params = [] } = tokenData - const { value: to } = params[0] || {} - const { value: tokenAmountInDec } = params[1] || {} - const tokenAmountInHex = conversionUtil(tokenAmountInDec, { - fromNumericBase: 'dec', - toNumericBase: 'hex', - }) - dispatch(setSelectedToken(tokenAddress)) - dispatch(updateSend({ - gasLimit, - gasPrice, - gasTotal: null, - to, - amount: tokenAmountInHex, - errors: { to: null, amount: null }, - editingTransactionId: id && id.toString(), - token: { - ...tokenProps, - address: tokenAddress, - }, - })) - dispatch(clearConfirmTransaction()) - dispatch(showSendTokenPage()) - }, - } -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(ConfirmSendToken) diff --git a/ui/app/components/pages/confirm-send-token/index.js b/ui/app/components/pages/confirm-send-token/index.js deleted file mode 100644 index 409b6ef3d..000000000 --- a/ui/app/components/pages/confirm-send-token/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './confirm-send-token.container' diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js deleted file mode 100644 index 7f1fb4e49..000000000 --- a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js +++ /dev/null @@ -1,119 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import ConfirmTransactionBase from '../confirm-transaction-base' -import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' -import { - formatCurrency, - convertTokenToFiat, - addFiat, - roundExponential, -} from '../../../helpers/confirm-transaction/util' -import { getWeiHexFromDecimalValue } from '../../../helpers/conversions.util' -import { ETH, PRIMARY } from '../../../constants/common' - -export default class ConfirmTokenTransactionBase extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - tokenAddress: PropTypes.string, - toAddress: PropTypes.string, - tokenAmount: PropTypes.number, - tokenSymbol: PropTypes.string, - fiatTransactionTotal: PropTypes.string, - ethTransactionTotal: PropTypes.string, - contractExchangeRate: PropTypes.number, - conversionRate: PropTypes.number, - currentCurrency: PropTypes.string, - } - - getFiatTransactionAmount () { - const { tokenAmount, currentCurrency, conversionRate, contractExchangeRate } = this.props - - return convertTokenToFiat({ - value: tokenAmount, - toCurrency: currentCurrency, - conversionRate, - contractExchangeRate, - }) - } - - renderSubtitleComponent () { - const { contractExchangeRate, tokenAmount } = this.props - - const decimalEthValue = (tokenAmount * contractExchangeRate) || 0 - const hexWeiValue = getWeiHexFromDecimalValue({ - value: decimalEthValue, - fromCurrency: ETH, - fromDenomination: ETH, - }) - - return typeof contractExchangeRate === 'undefined' - ? ( - <span> - { this.context.t('noConversionRateAvailable') } - </span> - ) : ( - <UserPreferencedCurrencyDisplay - value={hexWeiValue} - type={PRIMARY} - showEthLogo - hideLabel - /> - ) - } - - renderPrimaryTotalTextOverride () { - const { tokenAmount, tokenSymbol, ethTransactionTotal } = this.props - const tokensText = `${tokenAmount} ${tokenSymbol}` - - return ( - <div> - <span>{ `${tokensText} + ` }</span> - <img - src="/images/eth.svg" - height="18" - /> - <span>{ ethTransactionTotal }</span> - </div> - ) - } - - getSecondaryTotalTextOverride () { - const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props - - if (typeof contractExchangeRate === 'undefined') { - return formatCurrency(fiatTransactionTotal, currentCurrency) - } else { - const fiatTransactionAmount = this.getFiatTransactionAmount() - const fiatTotal = addFiat(fiatTransactionAmount, fiatTransactionTotal) - const roundedFiatTotal = roundExponential(fiatTotal) - return formatCurrency(roundedFiatTotal, currentCurrency) - } - } - - render () { - const { - toAddress, - tokenAddress, - tokenSymbol, - tokenAmount, - ...restProps - } = this.props - - const tokensText = `${tokenAmount} ${tokenSymbol}` - - return ( - <ConfirmTransactionBase - toAddress={toAddress} - identiconAddress={tokenAddress} - title={tokensText} - subtitleComponent={this.renderSubtitleComponent()} - primaryTotalTextOverride={this.renderPrimaryTotalTextOverride()} - secondaryTotalTextOverride={this.getSecondaryTotalTextOverride()} - {...restProps} - /> - ) - } -} diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js deleted file mode 100644 index be38acdb0..000000000 --- a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js +++ /dev/null @@ -1,34 +0,0 @@ -import { connect } from 'react-redux' -import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component' -import { - tokenAmountAndToAddressSelector, - contractExchangeRateSelector, -} from '../../../selectors/confirm-transaction' - -const mapStateToProps = (state, ownProps) => { - const { tokenAmount: ownTokenAmount } = ownProps - const { confirmTransaction, metamask: { currentCurrency, conversionRate } } = state - const { - txData: { txParams: { to: tokenAddress } = {} } = {}, - tokenProps: { tokenSymbol } = {}, - fiatTransactionTotal, - ethTransactionTotal, - } = confirmTransaction - - const { tokenAmount, toAddress } = tokenAmountAndToAddressSelector(state) - const contractExchangeRate = contractExchangeRateSelector(state) - - return { - toAddress, - tokenAddress, - tokenAmount: typeof ownTokenAmount !== 'undefined' ? ownTokenAmount : tokenAmount, - tokenSymbol, - currentCurrency, - conversionRate, - contractExchangeRate, - fiatTransactionTotal, - ethTransactionTotal, - } -} - -export default connect(mapStateToProps)(ConfirmTokenTransactionBase) diff --git a/ui/app/components/pages/confirm-token-transaction-base/index.js b/ui/app/components/pages/confirm-token-transaction-base/index.js deleted file mode 100644 index e15c5d56b..000000000 --- a/ui/app/components/pages/confirm-token-transaction-base/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './confirm-token-transaction-base.container' -export { default as ConfirmTokenTransactionBase } from './confirm-token-transaction-base.component' diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js deleted file mode 100644 index 7d01aaffb..000000000 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ /dev/null @@ -1,412 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container' -import { isBalanceSufficient } from '../../send/send.utils' -import { DEFAULT_ROUTE } from '../../../routes' -import { - INSUFFICIENT_FUNDS_ERROR_KEY, - TRANSACTION_ERROR_KEY, -} from '../../../constants/error-keys' -import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../../constants/transactions' -import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' -import { PRIMARY, SECONDARY } from '../../../constants/common' - -export default class ConfirmTransactionBase extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - // react-router props - match: PropTypes.object, - history: PropTypes.object, - // Redux props - balance: PropTypes.string, - cancelTransaction: PropTypes.func, - cancelAllTransactions: PropTypes.func, - clearConfirmTransaction: PropTypes.func, - clearSend: PropTypes.func, - conversionRate: PropTypes.number, - currentCurrency: PropTypes.string, - editTransaction: PropTypes.func, - ethTransactionAmount: PropTypes.string, - ethTransactionFee: PropTypes.string, - ethTransactionTotal: PropTypes.string, - fiatTransactionAmount: PropTypes.string, - fiatTransactionFee: PropTypes.string, - fiatTransactionTotal: PropTypes.string, - fromAddress: PropTypes.string, - fromName: PropTypes.string, - hexTransactionAmount: PropTypes.string, - hexTransactionFee: PropTypes.string, - hexTransactionTotal: PropTypes.string, - isTxReprice: PropTypes.bool, - methodData: PropTypes.object, - nonce: PropTypes.string, - assetImage: PropTypes.string, - sendTransaction: PropTypes.func, - showCustomizeGasModal: PropTypes.func, - showTransactionConfirmedModal: PropTypes.func, - showRejectTransactionsConfirmationModal: PropTypes.func, - toAddress: PropTypes.string, - tokenData: PropTypes.object, - tokenProps: PropTypes.object, - toName: PropTypes.string, - transactionStatus: PropTypes.string, - txData: PropTypes.object, - unapprovedTxCount: PropTypes.number, - // Component props - action: PropTypes.string, - contentComponent: PropTypes.node, - dataComponent: PropTypes.node, - detailsComponent: PropTypes.node, - errorKey: PropTypes.string, - errorMessage: PropTypes.string, - primaryTotalTextOverride: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), - secondaryTotalTextOverride: PropTypes.string, - hideData: PropTypes.bool, - hideDetails: PropTypes.bool, - hideSubtitle: PropTypes.bool, - identiconAddress: PropTypes.string, - onCancel: PropTypes.func, - onEdit: PropTypes.func, - onEditGas: PropTypes.func, - onSubmit: PropTypes.func, - subtitle: PropTypes.string, - subtitleComponent: PropTypes.node, - summaryComponent: PropTypes.node, - title: PropTypes.string, - titleComponent: PropTypes.node, - valid: PropTypes.bool, - warning: PropTypes.string, - } - - state = { - submitting: false, - submitError: null, - } - - componentDidUpdate () { - const { - transactionStatus, - showTransactionConfirmedModal, - history, - clearConfirmTransaction, - } = this.props - - if (transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS) { - showTransactionConfirmedModal({ - onSubmit: () => { - clearConfirmTransaction() - history.push(DEFAULT_ROUTE) - }, - }) - - return - } - } - - getErrorKey () { - const { - balance, - conversionRate, - hexTransactionFee, - txData: { - simulationFails, - txParams: { - value: amount, - } = {}, - } = {}, - } = this.props - - const insufficientBalance = balance && !isBalanceSufficient({ - amount, - gasTotal: hexTransactionFee || '0x0', - balance, - conversionRate, - }) - - if (insufficientBalance) { - return { - valid: false, - errorKey: INSUFFICIENT_FUNDS_ERROR_KEY, - } - } - - if (simulationFails) { - return { - valid: true, - errorKey: simulationFails.errorKey ? simulationFails.errorKey : TRANSACTION_ERROR_KEY, - } - } - - return { - valid: true, - } - } - - handleEditGas () { - const { onEditGas, showCustomizeGasModal } = this.props - - if (onEditGas) { - onEditGas() - } else { - showCustomizeGasModal() - } - } - - renderDetails () { - const { - detailsComponent, - primaryTotalTextOverride, - secondaryTotalTextOverride, - hexTransactionFee, - hexTransactionTotal, - hideDetails, - } = this.props - - if (hideDetails) { - return null - } - - return ( - detailsComponent || ( - <div className="confirm-page-container-content__details"> - <div className="confirm-page-container-content__gas-fee"> - <ConfirmDetailRow - label="Gas Fee" - value={hexTransactionFee} - headerText="Edit" - headerTextClassName="confirm-detail-row__header-text--edit" - onHeaderClick={() => this.handleEditGas()} - /> - </div> - <div> - <ConfirmDetailRow - label="Total" - value={hexTransactionTotal} - primaryText={primaryTotalTextOverride} - secondaryText={secondaryTotalTextOverride} - headerText="Amount + Gas Fee" - headerTextClassName="confirm-detail-row__header-text--total" - primaryValueTextColor="#2f9ae0" - /> - </div> - </div> - ) - ) - } - - renderData () { - const { t } = this.context - const { - txData: { - txParams: { - data, - } = {}, - } = {}, - methodData: { - name, - params, - } = {}, - hideData, - dataComponent, - } = this.props - - if (hideData) { - return null - } - - return dataComponent || ( - <div className="confirm-page-container-content__data"> - <div className="confirm-page-container-content__data-box-label"> - {`${t('functionType')}:`} - <span className="confirm-page-container-content__function-type"> - { name || t('notFound') } - </span> - </div> - { - params && ( - <div className="confirm-page-container-content__data-box"> - <div className="confirm-page-container-content__data-field-label"> - { `${t('parameters')}:` } - </div> - <div> - <pre>{ JSON.stringify(params, null, 2) }</pre> - </div> - </div> - ) - } - <div className="confirm-page-container-content__data-box-label"> - {`${t('hexData')}:`} - </div> - <div className="confirm-page-container-content__data-box"> - { data } - </div> - </div> - ) - } - - handleEdit () { - const { txData, tokenData, tokenProps, onEdit } = this.props - onEdit({ txData, tokenData, tokenProps }) - } - - handleCancelAll () { - const { - cancelAllTransactions, - clearConfirmTransaction, - history, - showRejectTransactionsConfirmationModal, - unapprovedTxCount, - } = this.props - - showRejectTransactionsConfirmationModal({ - unapprovedTxCount, - async onSubmit () { - await cancelAllTransactions() - clearConfirmTransaction() - history.push(DEFAULT_ROUTE) - }, - }) - } - - handleCancel () { - const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props - - if (onCancel) { - onCancel(txData) - } else { - cancelTransaction(txData) - .then(() => { - clearConfirmTransaction() - history.push(DEFAULT_ROUTE) - }) - } - } - - handleSubmit () { - const { sendTransaction, clearConfirmTransaction, txData, history, onSubmit } = this.props - const { submitting } = this.state - - if (submitting) { - return - } - - this.setState({ submitting: true, submitError: null }) - - if (onSubmit) { - Promise.resolve(onSubmit(txData)) - .then(this.setState({ submitting: false })) - } else { - sendTransaction(txData) - .then(() => { - clearConfirmTransaction() - this.setState({ submitting: false }) - history.push(DEFAULT_ROUTE) - }) - .catch(error => { - this.setState({ submitting: false, submitError: error.message }) - }) - } - } - - renderTitleComponent () { - const { title, titleComponent, hexTransactionAmount } = this.props - - // Title string passed in by props takes priority - if (title) { - return null - } - - return titleComponent || ( - <UserPreferencedCurrencyDisplay - value={hexTransactionAmount} - type={PRIMARY} - showEthLogo - ethLogoHeight="26" - hideLabel - /> - ) - } - - renderSubtitleComponent () { - const { subtitle, subtitleComponent, hexTransactionAmount } = this.props - - // Subtitle string passed in by props takes priority - if (subtitle) { - return null - } - - return subtitleComponent || ( - <UserPreferencedCurrencyDisplay - value={hexTransactionAmount} - type={SECONDARY} - showEthLogo - hideLabel - /> - ) - } - - render () { - const { - isTxReprice, - fromName, - fromAddress, - toName, - toAddress, - methodData, - valid: propsValid = true, - errorMessage, - errorKey: propsErrorKey, - action, - title, - subtitle, - hideSubtitle, - identiconAddress, - summaryComponent, - contentComponent, - onEdit, - nonce, - assetImage, - warning, - unapprovedTxCount, - } = this.props - const { submitting, submitError } = this.state - - const { name } = methodData - const { valid, errorKey } = this.getErrorKey() - - return ( - <ConfirmPageContainer - fromName={fromName} - fromAddress={fromAddress} - toName={toName} - toAddress={toAddress} - showEdit={onEdit && !isTxReprice} - action={action || name || this.context.t('unknownFunction')} - title={title} - titleComponent={this.renderTitleComponent()} - subtitle={subtitle} - subtitleComponent={this.renderSubtitleComponent()} - hideSubtitle={hideSubtitle} - summaryComponent={summaryComponent} - detailsComponent={this.renderDetails()} - dataComponent={this.renderData()} - contentComponent={contentComponent} - nonce={nonce} - unapprovedTxCount={unapprovedTxCount} - assetImage={assetImage} - identiconAddress={identiconAddress} - errorMessage={errorMessage || submitError} - errorKey={propsErrorKey || errorKey} - warning={warning} - disabled={!propsValid || !valid || submitting} - onEdit={() => this.handleEdit()} - onCancelAll={() => this.handleCancelAll()} - onCancel={() => this.handleCancel()} - onSubmit={() => this.handleSubmit()} - /> - ) - } -} diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js deleted file mode 100644 index c366d5137..000000000 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ /dev/null @@ -1,203 +0,0 @@ -import { connect } from 'react-redux' -import { compose } from 'recompose' -import { withRouter } from 'react-router-dom' -import R from 'ramda' -import contractMap from 'eth-contract-metadata' -import ConfirmTransactionBase from './confirm-transaction-base.component' -import { - clearConfirmTransaction, - updateGasAndCalculate, -} from '../../../ducks/confirm-transaction.duck' -import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal } from '../../../actions' -import { - INSUFFICIENT_FUNDS_ERROR_KEY, - GAS_LIMIT_TOO_LOW_ERROR_KEY, -} from '../../../constants/error-keys' -import { getHexGasTotal } from '../../../helpers/confirm-transaction/util' -import { isBalanceSufficient } from '../../send/send.utils' -import { conversionGreaterThan } from '../../../conversion-util' -import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants' -import { addressSlicer, valuesFor } from '../../../util' - -const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { - return { - ...acc, - [base.toLowerCase()]: contractMap[base], - } -}, {}) - -const mapStateToProps = (state, props) => { - const { toAddress: propsToAddress } = props - const { confirmTransaction, metamask } = state - const { - ethTransactionAmount, - ethTransactionFee, - ethTransactionTotal, - fiatTransactionAmount, - fiatTransactionFee, - fiatTransactionTotal, - hexTransactionAmount, - hexTransactionFee, - hexTransactionTotal, - tokenData, - methodData, - txData, - tokenProps, - nonce, - } = confirmTransaction - const { txParams = {}, lastGasPrice, id: transactionId } = txData - const { from: fromAddress, to: txParamsToAddress } = txParams - const { - conversionRate, - identities, - currentCurrency, - accounts, - selectedAddress, - selectedAddressTxList, - assetImages, - network, - unapprovedTxs, - } = metamask - const assetImage = assetImages[txParamsToAddress] - const { balance } = accounts[selectedAddress] - const { name: fromName } = identities[selectedAddress] - const toAddress = propsToAddress || txParamsToAddress - const toName = identities[toAddress] - ? identities[toAddress].name - : casedContractMap[toAddress] ? casedContractMap[toAddress].name : addressSlicer(toAddress) - - const isTxReprice = Boolean(lastGasPrice) - - const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList) - const transactionStatus = transaction ? transaction.status : '' - - const currentNetworkUnapprovedTxs = R.filter( - ({ metamaskNetworkId }) => metamaskNetworkId === network, - valuesFor(unapprovedTxs), - ) - const unapprovedTxCount = currentNetworkUnapprovedTxs.length - - return { - balance, - fromAddress, - fromName, - toAddress, - toName, - ethTransactionAmount, - ethTransactionFee, - ethTransactionTotal, - fiatTransactionAmount, - fiatTransactionFee, - fiatTransactionTotal, - hexTransactionAmount, - hexTransactionFee, - hexTransactionTotal, - txData, - tokenData, - methodData, - tokenProps, - isTxReprice, - currentCurrency, - conversionRate, - transactionStatus, - nonce, - assetImage, - unapprovedTxs, - unapprovedTxCount, - } -} - -const mapDispatchToProps = dispatch => { - return { - clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), - clearSend: () => dispatch(clearSend()), - showTransactionConfirmedModal: ({ onSubmit }) => { - return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onSubmit })) - }, - showCustomizeGasModal: ({ txData, onSubmit, validate }) => { - return dispatch(showModal({ name: 'CONFIRM_CUSTOMIZE_GAS', txData, onSubmit, validate })) - }, - updateGasAndCalculate: ({ gasLimit, gasPrice }) => { - return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) - }, - showRejectTransactionsConfirmationModal: ({ onSubmit, unapprovedTxCount }) => { - return dispatch(showModal({ name: 'REJECT_TRANSACTIONS', onSubmit, unapprovedTxCount })) - }, - cancelTransaction: ({ id }) => dispatch(cancelTx({ id })), - cancelAllTransactions: (txList) => dispatch(cancelTxs(txList)), - sendTransaction: txData => dispatch(updateAndApproveTx(txData)), - } -} - -const getValidateEditGas = ({ balance, conversionRate, txData }) => { - const { txParams: { value: amount } = {} } = txData - - return ({ gasLimit, gasPrice }) => { - const gasTotal = getHexGasTotal({ gasLimit, gasPrice }) - const hasSufficientBalance = isBalanceSufficient({ - amount, - gasTotal, - balance, - conversionRate, - }) - - if (!hasSufficientBalance) { - return { - valid: false, - errorKey: INSUFFICIENT_FUNDS_ERROR_KEY, - } - } - - const gasLimitTooLow = gasLimit && conversionGreaterThan( - { - value: MIN_GAS_LIMIT_DEC, - fromNumericBase: 'dec', - conversionRate, - }, - { - value: gasLimit, - fromNumericBase: 'hex', - }, - ) - - if (gasLimitTooLow) { - return { - valid: false, - errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY, - } - } - - return { - valid: true, - } - } -} - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { balance, conversionRate, txData, unapprovedTxs } = stateProps - const { - cancelAllTransactions: dispatchCancelAllTransactions, - showCustomizeGasModal: dispatchShowCustomizeGasModal, - updateGasAndCalculate: dispatchUpdateGasAndCalculate, - ...otherDispatchProps - } = dispatchProps - - const validateEditGas = getValidateEditGas({ balance, conversionRate, txData }) - - return { - ...stateProps, - ...otherDispatchProps, - ...ownProps, - showCustomizeGasModal: () => dispatchShowCustomizeGasModal({ - txData, - onSubmit: txData => dispatchUpdateGasAndCalculate(txData), - validate: validateEditGas, - }), - cancelAllTransactions: () => dispatchCancelAllTransactions(valuesFor(unapprovedTxs)), - } -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(ConfirmTransactionBase) diff --git a/ui/app/components/pages/confirm-transaction-base/index.js b/ui/app/components/pages/confirm-transaction-base/index.js deleted file mode 100644 index 9996e9aeb..000000000 --- a/ui/app/components/pages/confirm-transaction-base/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './confirm-transaction-base.container' diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js deleted file mode 100644 index 2c44b6094..000000000 --- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js +++ /dev/null @@ -1,92 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { Redirect } from 'react-router-dom' -import Loading from '../../loading-screen' -import { - CONFIRM_TRANSACTION_ROUTE, - CONFIRM_DEPLOY_CONTRACT_PATH, - CONFIRM_SEND_ETHER_PATH, - CONFIRM_SEND_TOKEN_PATH, - CONFIRM_APPROVE_PATH, - CONFIRM_TRANSFER_FROM_PATH, - CONFIRM_TOKEN_METHOD_PATH, - SIGNATURE_REQUEST_PATH, -} from '../../../routes' -import { isConfirmDeployContract } from '../../../helpers/transactions.util' -import { - TOKEN_METHOD_TRANSFER, - TOKEN_METHOD_APPROVE, - TOKEN_METHOD_TRANSFER_FROM, -} from '../../../constants/transactions' - -export default class ConfirmTransactionSwitch extends Component { - static propTypes = { - txData: PropTypes.object, - methodData: PropTypes.object, - fetchingData: PropTypes.bool, - isEtherTransaction: PropTypes.bool, - } - - redirectToTransaction () { - const { - txData, - methodData: { name }, - fetchingData, - isEtherTransaction, - } = this.props - const { id, txParams: { data } = {} } = txData - - if (isConfirmDeployContract(txData)) { - const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}` - return <Redirect to={{ pathname }} /> - } - - if (fetchingData) { - return <Loading /> - } - - if (isEtherTransaction) { - const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}` - return <Redirect to={{ pathname }} /> - } - - if (data) { - const methodName = name && name.toLowerCase() - - switch (methodName) { - case TOKEN_METHOD_TRANSFER: { - const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}` - return <Redirect to={{ pathname }} /> - } - case TOKEN_METHOD_APPROVE: { - const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}` - return <Redirect to={{ pathname }} /> - } - case TOKEN_METHOD_TRANSFER_FROM: { - const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}` - return <Redirect to={{ pathname }} /> - } - default: { - const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}` - return <Redirect to={{ pathname }} /> - } - } - } - - const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}` - return <Redirect to={{ pathname }} /> - } - - render () { - const { txData } = this.props - - if (txData.txParams) { - return this.redirectToTransaction() - } else if (txData.msgParams) { - const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}` - return <Redirect to={{ pathname }} /> - } - - return <Loading /> - } -} diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js deleted file mode 100644 index 7f2c36af2..000000000 --- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js +++ /dev/null @@ -1,22 +0,0 @@ -import { connect } from 'react-redux' -import ConfirmTransactionSwitch from './confirm-transaction-switch.component' - -const mapStateToProps = state => { - const { - confirmTransaction: { - txData, - methodData, - fetchingData, - toSmartContract, - }, - } = state - - return { - txData, - methodData, - fetchingData, - isEtherTransaction: !toSmartContract, - } -} - -export default connect(mapStateToProps)(ConfirmTransactionSwitch) diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js deleted file mode 100644 index 536aa5212..000000000 --- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js +++ /dev/null @@ -1,4 +0,0 @@ -export function isConfirmDeployContract (txData = {}) { - const { txParams = {} } = txData - return !txParams.to -} diff --git a/ui/app/components/pages/confirm-transaction-switch/index.js b/ui/app/components/pages/confirm-transaction-switch/index.js deleted file mode 100644 index c288acb1a..000000000 --- a/ui/app/components/pages/confirm-transaction-switch/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import ConfirmTransactionSwitch from './confirm-transaction-switch.container' -module.exports = ConfirmTransactionSwitch diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js deleted file mode 100644 index 3ac656d73..000000000 --- a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js +++ /dev/null @@ -1,157 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { Switch, Route } from 'react-router-dom' -import Loading from '../../loading-screen' -import ConfirmTransactionSwitch from '../confirm-transaction-switch' -import ConfirmTransactionBase from '../confirm-transaction-base' -import ConfirmSendEther from '../confirm-send-ether' -import ConfirmSendToken from '../confirm-send-token' -import ConfirmDeployContract from '../confirm-deploy-contract' -import ConfirmApprove from '../confirm-approve' -import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' -import ConfTx from '../../../conf-tx' -import { - DEFAULT_ROUTE, - CONFIRM_TRANSACTION_ROUTE, - CONFIRM_DEPLOY_CONTRACT_PATH, - CONFIRM_SEND_ETHER_PATH, - CONFIRM_SEND_TOKEN_PATH, - CONFIRM_APPROVE_PATH, - CONFIRM_TRANSFER_FROM_PATH, - CONFIRM_TOKEN_METHOD_PATH, - SIGNATURE_REQUEST_PATH, -} from '../../../routes' - -export default class ConfirmTransaction extends Component { - static propTypes = { - history: PropTypes.object.isRequired, - totalUnapprovedCount: PropTypes.number.isRequired, - match: PropTypes.object, - send: PropTypes.object, - unconfirmedTransactions: PropTypes.array, - setTransactionToConfirm: PropTypes.func, - confirmTransaction: PropTypes.object, - clearConfirmTransaction: PropTypes.func, - } - - getParamsTransactionId () { - const { match: { params: { id } = {} } } = this.props - return id || null - } - - componentDidMount () { - const { - totalUnapprovedCount = 0, - send = {}, - history, - confirmTransaction: { txData: { id: transactionId } = {} }, - } = this.props - - if (!totalUnapprovedCount && !send.to) { - history.replace(DEFAULT_ROUTE) - return - } - - if (!transactionId) { - this.setTransactionToConfirm() - } - } - - componentDidUpdate () { - const { - setTransactionToConfirm, - confirmTransaction: { txData: { id: transactionId } = {} }, - clearConfirmTransaction, - } = this.props - const paramsTransactionId = this.getParamsTransactionId() - - if (paramsTransactionId && transactionId && paramsTransactionId !== transactionId + '') { - clearConfirmTransaction() - setTransactionToConfirm(paramsTransactionId) - return - } - - if (!transactionId) { - this.setTransactionToConfirm() - } - } - - setTransactionToConfirm () { - const { - history, - unconfirmedTransactions, - setTransactionToConfirm, - } = this.props - const paramsTransactionId = this.getParamsTransactionId() - - if (paramsTransactionId) { - // Check to make sure params ID is valid - const tx = unconfirmedTransactions.find(({ id }) => id + '' === paramsTransactionId) - - if (!tx) { - history.replace(DEFAULT_ROUTE) - } else { - setTransactionToConfirm(paramsTransactionId) - } - } else if (unconfirmedTransactions.length) { - const totalUnconfirmed = unconfirmedTransactions.length - const transaction = unconfirmedTransactions[totalUnconfirmed - 1] - const { id: transactionId, loadingDefaults } = transaction - - if (!loadingDefaults) { - setTransactionToConfirm(transactionId) - } - } - } - - render () { - const { confirmTransaction: { txData: { id } } = {} } = this.props - const paramsTransactionId = this.getParamsTransactionId() - - // Show routes when state.confirmTransaction has been set and when either the ID in the params - // isn't specified or is specified and matches the ID in state.confirmTransaction in order to - // support URLs of /confirm-transaction or /confirm-transaction/<transactionId> - return id && (!paramsTransactionId || paramsTransactionId === id + '') - ? ( - <Switch> - <Route - exact - path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`} - component={ConfirmDeployContract} - /> - <Route - exact - path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`} - component={ConfirmTransactionBase} - /> - <Route - exact - path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`} - component={ConfirmSendEther} - /> - <Route - exact - path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_TOKEN_PATH}`} - component={ConfirmSendToken} - /> - <Route - exact - path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`} - component={ConfirmApprove} - /> - <Route - exact - path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`} - component={ConfirmTokenTransactionBase} - /> - <Route - exact - path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`} - component={ConfTx} - /> - <Route path="*" component={ConfirmTransactionSwitch} /> - </Switch> - ) - : <Loading /> - } -} diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js deleted file mode 100644 index 1bc2f1efb..000000000 --- a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js +++ /dev/null @@ -1,33 +0,0 @@ -import { connect } from 'react-redux' -import { compose } from 'recompose' -import { withRouter } from 'react-router-dom' -import { - setTransactionToConfirm, - clearConfirmTransaction, -} from '../../../ducks/confirm-transaction.duck' -import ConfirmTransaction from './confirm-transaction.component' -import { getTotalUnapprovedCount } from '../../../selectors' -import { unconfirmedTransactionsListSelector } from '../../../selectors/confirm-transaction' - -const mapStateToProps = state => { - const { metamask: { send }, confirmTransaction } = state - - return { - totalUnapprovedCount: getTotalUnapprovedCount(state), - send, - confirmTransaction, - unconfirmedTransactions: unconfirmedTransactionsListSelector(state), - } -} - -const mapDispatchToProps = dispatch => { - return { - setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)), - clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), - } -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps), -)(ConfirmTransaction) diff --git a/ui/app/components/pages/confirm-transaction/index.js b/ui/app/components/pages/confirm-transaction/index.js deleted file mode 100644 index 4bf42d85c..000000000 --- a/ui/app/components/pages/confirm-transaction/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import ConfirmTransaction from './confirm-transaction.container' -module.exports = ConfirmTransaction diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js deleted file mode 100644 index 2767b2e1f..000000000 --- a/ui/app/components/pages/create-account/connect-hardware/account-list.js +++ /dev/null @@ -1,205 +0,0 @@ -const { Component } = require('react') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const genAccountLink = require('../../../../../lib/account-link.js') -const Select = require('react-select').default -import Button from '../../../button' - -class AccountList extends Component { - constructor (props, context) { - super(props) - } - - getHdPaths () { - return [ - { - label: `Ledger Live`, - value: `m/44'/60'/0'/0/0`, - }, - { - label: `Legacy (MEW / MyCrypto)`, - value: `m/44'/60'/0'`, - }, - ] - } - - goToNextPage = () => { - // If we have < 5 accounts, it's restricted by BIP-44 - if (this.props.accounts.length === 5) { - this.props.getPage(this.props.device, 1, this.props.selectedPath) - } else { - this.props.onAccountRestriction() - } - } - - goToPreviousPage = () => { - this.props.getPage(this.props.device, -1, this.props.selectedPath) - } - - renderHdPathSelector () { - const { onPathChange, selectedPath } = this.props - - const options = this.getHdPaths() - return h('div', [ - h('h3.hw-connect__hdPath__title', {}, this.context.t('selectHdPath')), - h('p.hw-connect__msg', {}, this.context.t('selectPathHelp')), - h('div.hw-connect__hdPath', [ - h(Select, { - className: 'hw-connect__hdPath__select', - name: 'hd-path-select', - clearable: false, - value: selectedPath, - options, - onChange: (opt) => { - onPathChange(opt.value) - }, - }), - ]), - ]) - } - - capitalizeDevice (device) { - return device.slice(0, 1).toUpperCase() + device.slice(1) - } - - renderHeader () { - const { device } = this.props - return ( - h('div.hw-connect', [ - - h('h3.hw-connect__unlock-title', {}, `${this.context.t('unlock')} ${this.capitalizeDevice(device)}`), - - device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null, - - h('h3.hw-connect__hdPath__title', {}, this.context.t('selectAnAccount')), - h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')), - ]) - ) - } - - renderAccounts () { - return h('div.hw-account-list', [ - this.props.accounts.map((a, i) => { - - return h('div.hw-account-list__item', { key: a.address }, [ - h('div.hw-account-list__item__radio', [ - h('input', { - type: 'radio', - name: 'selectedAccount', - id: `address-${i}`, - value: a.index, - onChange: (e) => this.props.onAccountChange(e.target.value), - checked: this.props.selectedAccount === a.index.toString(), - }), - h( - 'label.hw-account-list__item__label', - { - htmlFor: `address-${i}`, - }, - [ - h('span.hw-account-list__item__index', a.index + 1), - `${a.address.slice(0, 4)}...${a.address.slice(-4)}`, - h('span.hw-account-list__item__balance', `${a.balance}`), - ]), - ]), - h( - 'a.hw-account-list__item__link', - { - href: genAccountLink(a.address, this.props.network), - target: '_blank', - title: this.context.t('etherscanView'), - }, - h('img', { src: 'images/popout.svg' }) - ), - ]) - }), - ]) - } - - renderPagination () { - return h('div.hw-list-pagination', [ - h( - 'button.hw-list-pagination__button', - { - onClick: this.goToPreviousPage, - }, - `< ${this.context.t('prev')}` - ), - - h( - 'button.hw-list-pagination__button', - { - onClick: this.goToNextPage, - }, - `${this.context.t('next')} >` - ), - ]) - } - - renderButtons () { - const disabled = this.props.selectedAccount === null - const buttonProps = {} - if (disabled) { - buttonProps.disabled = true - } - - return h('div.new-account-connect-form__buttons', {}, [ - h(Button, { - type: 'default', - large: true, - className: 'new-account-connect-form__button', - onClick: this.props.onCancel.bind(this), - }, [this.context.t('cancel')]), - - h(Button, { - type: 'primary', - large: true, - className: 'new-account-connect-form__button unlock', - disabled, - onClick: this.props.onUnlockAccount.bind(this, this.props.device), - }, [this.context.t('unlock')]), - ]) - } - - renderForgetDevice () { - return h('div.hw-forget-device-container', {}, [ - h('a', { - onClick: this.props.onForgetDevice.bind(this, this.props.device), - }, this.context.t('forgetDevice')), - ]) - } - - render () { - return h('div.new-account-connect-form.account-list', {}, [ - this.renderHeader(), - this.renderAccounts(), - this.renderPagination(), - this.renderButtons(), - this.renderForgetDevice(), - ]) - } - -} - - -AccountList.propTypes = { - onPathChange: PropTypes.func.isRequired, - selectedPath: PropTypes.string.isRequired, - device: PropTypes.string.isRequired, - accounts: PropTypes.array.isRequired, - onAccountChange: PropTypes.func.isRequired, - onForgetDevice: PropTypes.func.isRequired, - getPage: PropTypes.func.isRequired, - network: PropTypes.string, - selectedAccount: PropTypes.string, - history: PropTypes.object, - onUnlockAccount: PropTypes.func, - onCancel: PropTypes.func, - onAccountRestriction: PropTypes.func, -} - -AccountList.contextTypes = { - t: PropTypes.func, -} - -module.exports = AccountList diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js deleted file mode 100644 index d3abf3119..000000000 --- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js +++ /dev/null @@ -1,197 +0,0 @@ -const { Component } = require('react') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -import Button from '../../../button' - -class ConnectScreen extends Component { - constructor (props, context) { - super(props) - this.state = { - selectedDevice: null, - } - } - - connect = () => { - if (this.state.selectedDevice) { - this.props.connectToHardwareWallet(this.state.selectedDevice) - } - return null - } - - renderConnectToTrezorButton () { - return h( - `button.hw-connect__btn${this.state.selectedDevice === 'trezor' ? '.selected' : ''}`, - { onClick: _ => this.setState({selectedDevice: 'trezor'}) }, - h('img.hw-connect__btn__img', { - src: 'images/trezor-logo.svg', - }) - ) - } - - renderConnectToLedgerButton () { - return h( - `button.hw-connect__btn${this.state.selectedDevice === 'ledger' ? '.selected' : ''}`, - { onClick: _ => this.setState({selectedDevice: 'ledger'}) }, - h('img.hw-connect__btn__img', { - src: 'images/ledger-logo.svg', - }) - ) - } - - renderButtons () { - return ( - h('div', {}, [ - h('div.hw-connect__btn-wrapper', {}, [ - this.renderConnectToLedgerButton(), - this.renderConnectToTrezorButton(), - ]), - h( - `button.hw-connect__connect-btn${!this.state.selectedDevice ? '.disabled' : ''}`, - { onClick: this.connect }, - this.context.t('connect') - ), - ]) - ) - } - - renderUnsupportedBrowser () { - return ( - h('div.new-account-connect-form.unsupported-browser', {}, [ - h('div.hw-connect', [ - h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')), - h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')), - ]), - h(Button, { - type: 'primary', - large: true, - onClick: () => global.platform.openWindow({ - url: 'https://google.com/chrome', - }), - }, - this.context.t('downloadGoogleChrome') - ), - ]) - ) - } - - renderHeader () { - return ( - h('div.hw-connect__header', {}, [ - h('h3.hw-connect__header__title', {}, this.context.t(`hardwareWallets`)), - h('p.hw-connect__header__msg', {}, this.context.t(`hardwareWalletsMsg`)), - ]) - ) - } - - getAffiliateLinks () { - const links = { - trezor: `<a class='hw-connect__get-hw__link' href='https://shop.trezor.io/?a=metamask' target='_blank'>Trezor</a>`, - ledger: `<a class='hw-connect__get-hw__link' href='https://www.ledger.com/products/ledger-nano-s?r=17c4991a03fa&tracker=MY_TRACKER' target='_blank'>Ledger</a>`, - } - - const text = this.context.t('orderOneHere') - const response = text.replace('Trezor', links.trezor).replace('Ledger', links.ledger) - - return h('div.hw-connect__get-hw__msg', { dangerouslySetInnerHTML: {__html: response }}) - } - - renderTrezorAffiliateLink () { - return h('div.hw-connect__get-hw', {}, [ - h('p.hw-connect__get-hw__msg', {}, this.context.t(`dontHaveAHardwareWallet`)), - this.getAffiliateLinks(), - ]) - } - - - scrollToTutorial = (e) => { - if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'}) - } - - renderLearnMore () { - return ( - h('p.hw-connect__learn-more', { - onClick: this.scrollToTutorial, - }, [ - this.context.t('learnMore'), - h('img.hw-connect__learn-more__arrow', { src: 'images/caret-right.svg'}), - ]) - ) - } - - renderTutorialSteps () { - const steps = [ - { - asset: 'hardware-wallet-step-1', - dimensions: {width: '225px', height: '75px'}, - }, - { - asset: 'hardware-wallet-step-2', - dimensions: {width: '300px', height: '100px'}, - }, - { - asset: 'hardware-wallet-step-3', - dimensions: {width: '120px', height: '90px'}, - }, - ] - - return h('.hw-tutorial', { - ref: node => { this.referenceNode = node }, - }, - steps.map((step, i) => ( - h('div.hw-connect', {}, [ - h('h3.hw-connect__title', {}, this.context.t(`step${i + 1}HardwareWallet`)), - h('p.hw-connect__msg', {}, this.context.t(`step${i + 1}HardwareWalletMsg`)), - h('img.hw-connect__step-asset', { src: `images/${step.asset}.svg`, ...step.dimensions }), - ]) - )) - ) - } - - renderFooter () { - return ( - h('div.hw-connect__footer', {}, [ - h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)), - this.renderButtons(), - h('p.hw-connect__footer__msg', {}, [ - this.context.t(`havingTroubleConnecting`), - h('a.hw-connect__footer__link', { - href: 'https://support.metamask.io/', - target: '_blank', - }, this.context.t('getHelp')), - ]), - ]) - ) - } - - renderConnectScreen () { - return ( - h('div.new-account-connect-form', {}, [ - this.renderHeader(), - this.renderButtons(), - this.renderTrezorAffiliateLink(), - this.renderLearnMore(), - this.renderTutorialSteps(), - this.renderFooter(), - ]) - ) - } - - render () { - if (this.props.browserSupported) { - return this.renderConnectScreen() - } - return this.renderUnsupportedBrowser() - } -} - -ConnectScreen.propTypes = { - connectToHardwareWallet: PropTypes.func.isRequired, - browserSupported: PropTypes.bool.isRequired, -} - -ConnectScreen.contextTypes = { - t: PropTypes.func, -} - -module.exports = ConnectScreen - diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js deleted file mode 100644 index 547df5223..000000000 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ /dev/null @@ -1,274 +0,0 @@ -const { Component } = require('react') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../../../actions') -const ConnectScreen = require('./connect-screen') -const AccountList = require('./account-list') -const { DEFAULT_ROUTE } = require('../../../../routes') -const { formatBalance } = require('../../../../util') -const { getPlatform } = require('../../../../../../app/scripts/lib/util') -const { PLATFORM_FIREFOX } = require('../../../../../../app/scripts/lib/enums') - -class ConnectHardwareForm extends Component { - constructor (props, context) { - super(props) - this.state = { - error: null, - selectedAccount: null, - accounts: [], - browserSupported: true, - unlocked: false, - device: null, - } - } - - componentWillReceiveProps (nextProps) { - const { accounts } = nextProps - const newAccounts = this.state.accounts.map(a => { - const normalizedAddress = a.address.toLowerCase() - const balanceValue = accounts[normalizedAddress] && accounts[normalizedAddress].balance || null - a.balance = balanceValue ? formatBalance(balanceValue, 6) : '...' - return a - }) - this.setState({accounts: newAccounts}) - } - - - componentDidMount () { - this.checkIfUnlocked() - } - - async checkIfUnlocked () { - ['trezor', 'ledger'].forEach(async device => { - const unlocked = await this.props.checkHardwareStatus(device, this.props.defaultHdPaths[device]) - if (unlocked) { - this.setState({unlocked: true}) - this.getPage(device, 0, this.props.defaultHdPaths[device]) - } - }) - } - - connectToHardwareWallet = (device) => { - // None of the hardware wallets are supported - // At least for now - if (getPlatform() === PLATFORM_FIREFOX) { - this.setState({ browserSupported: false, error: null}) - return null - } - - if (this.state.accounts.length) { - return null - } - - // Default values - this.getPage(device, 0, this.props.defaultHdPaths[device]) - } - - onPathChange = (path) => { - this.props.setHardwareWalletDefaultHdPath({device: this.state.device, path}) - this.getPage(this.state.device, 0, path) - } - - onAccountChange = (account) => { - this.setState({selectedAccount: account.toString(), error: null}) - } - - onAccountRestriction = () => { - this.setState({error: this.context.t('ledgerAccountRestriction') }) - } - - showTemporaryAlert () { - this.props.showAlert(this.context.t('hardwareWalletConnected')) - // Autohide the alert after 5 seconds - setTimeout(_ => { - this.props.hideAlert() - }, 5000) - } - - getPage = (device, page, hdPath) => { - this.props - .connectHardware(device, page, hdPath) - .then(accounts => { - if (accounts.length) { - - // If we just loaded the accounts for the first time - // (device previously locked) show the global alert - if (this.state.accounts.length === 0 && !this.state.unlocked) { - this.showTemporaryAlert() - } - - const newState = { unlocked: true, device, error: null } - // Default to the first account - if (this.state.selectedAccount === null) { - accounts.forEach((a, i) => { - if (a.address.toLowerCase() === this.props.address) { - newState.selectedAccount = a.index.toString() - } - }) - // If the page doesn't contain the selected account, let's deselect it - } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) { - newState.selectedAccount = null - } - - - // Map accounts with balances - newState.accounts = accounts.map(account => { - const normalizedAddress = account.address.toLowerCase() - const balanceValue = this.props.accounts[normalizedAddress] && this.props.accounts[normalizedAddress].balance || null - account.balance = balanceValue ? formatBalance(balanceValue, 6) : '...' - return account - }) - - this.setState(newState) - } - }) - .catch(e => { - if (e === 'Window blocked') { - this.setState({ browserSupported: false, error: null}) - } else if (e !== 'Window closed') { - this.setState({ error: e.toString() }) - } - }) - } - - onForgetDevice = (device) => { - this.props.forgetDevice(device) - .then(_ => { - this.setState({ - error: null, - selectedAccount: null, - accounts: [], - unlocked: false, - }) - }).catch(e => { - this.setState({ error: e.toString() }) - }) - } - - onUnlockAccount = (device) => { - - if (this.state.selectedAccount === null) { - this.setState({ error: this.context.t('accountSelectionRequired') }) - } - - this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device) - .then(_ => { - this.props.history.push(DEFAULT_ROUTE) - }).catch(e => { - this.setState({ error: e.toString() }) - }) - } - - onCancel = () => { - this.props.history.push(DEFAULT_ROUTE) - } - - renderError () { - return this.state.error - ? h('span.error', { style: { margin: '20px 20px 10px', display: 'block', textAlign: 'center' } }, this.state.error) - : null - } - - renderContent () { - if (!this.state.accounts.length) { - return h(ConnectScreen, { - connectToHardwareWallet: this.connectToHardwareWallet, - browserSupported: this.state.browserSupported, - }) - } - - return h(AccountList, { - onPathChange: this.onPathChange, - selectedPath: this.props.defaultHdPaths[this.state.device], - device: this.state.device, - accounts: this.state.accounts, - selectedAccount: this.state.selectedAccount, - onAccountChange: this.onAccountChange, - network: this.props.network, - getPage: this.getPage, - history: this.props.history, - onUnlockAccount: this.onUnlockAccount, - onForgetDevice: this.onForgetDevice, - onCancel: this.onCancel, - onAccountRestriction: this.onAccountRestriction, - }) - } - - render () { - return h('div', [ - this.renderError(), - this.renderContent(), - ]) - } -} - -ConnectHardwareForm.propTypes = { - hideModal: PropTypes.func, - showImportPage: PropTypes.func, - showConnectPage: PropTypes.func, - connectHardware: PropTypes.func, - checkHardwareStatus: PropTypes.func, - forgetDevice: PropTypes.func, - showAlert: PropTypes.func, - hideAlert: PropTypes.func, - unlockHardwareWalletAccount: PropTypes.func, - setHardwareWalletDefaultHdPath: PropTypes.func, - numberOfExistingAccounts: PropTypes.number, - history: PropTypes.object, - t: PropTypes.func, - network: PropTypes.string, - accounts: PropTypes.object, - address: PropTypes.string, - defaultHdPaths: PropTypes.object, -} - -const mapStateToProps = state => { - const { - metamask: { network, selectedAddress, identities = {}, accounts = [] }, - } = state - const numberOfExistingAccounts = Object.keys(identities).length - const { - appState: { defaultHdPaths }, - } = state - - return { - network, - accounts, - address: selectedAddress, - numberOfExistingAccounts, - defaultHdPaths, - } -} - -const mapDispatchToProps = dispatch => { - return { - setHardwareWalletDefaultHdPath: ({device, path}) => { - return dispatch(actions.setHardwareWalletDefaultHdPath({device, path})) - }, - connectHardware: (deviceName, page, hdPath) => { - return dispatch(actions.connectHardware(deviceName, page, hdPath)) - }, - checkHardwareStatus: (deviceName, hdPath) => { - return dispatch(actions.checkHardwareStatus(deviceName, hdPath)) - }, - forgetDevice: (deviceName) => { - return dispatch(actions.forgetDevice(deviceName)) - }, - unlockHardwareWalletAccount: (index, deviceName, hdPath) => { - return dispatch(actions.unlockHardwareWalletAccount(index, deviceName, hdPath)) - }, - showImportPage: () => dispatch(actions.showImportPage()), - showConnectPage: () => dispatch(actions.showConnectPage()), - showAlert: (msg) => dispatch(actions.showAlert(msg)), - hideAlert: () => dispatch(actions.hideAlert()), - } -} - -ConnectHardwareForm.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps, mapDispatchToProps)( - ConnectHardwareForm -) diff --git a/ui/app/components/pages/create-account/import-account/index.js b/ui/app/components/pages/create-account/import-account/index.js deleted file mode 100644 index 48d8f8838..000000000 --- a/ui/app/components/pages/create-account/import-account/index.js +++ /dev/null @@ -1,96 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const PropTypes = require('prop-types') -const connect = require('react-redux').connect -import Select from 'react-select' - -// Subviews -const JsonImportView = require('./json.js') -const PrivateKeyImportView = require('./private-key.js') - - -AccountImportSubview.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect()(AccountImportSubview) - - -inherits(AccountImportSubview, Component) -function AccountImportSubview () { - Component.call(this) -} - -AccountImportSubview.prototype.getMenuItemTexts = function () { - return [ - this.context.t('privateKey'), - this.context.t('jsonFile'), - ] -} - -AccountImportSubview.prototype.render = function () { - const state = this.state || {} - const menuItems = this.getMenuItemTexts() - const { type } = state - - return ( - h('div.new-account-import-form', [ - - h('.new-account-import-disclaimer', [ - h('span', this.context.t('importAccountMsg')), - h('span', { - style: { - cursor: 'pointer', - textDecoration: 'underline', - }, - onClick: () => { - global.platform.openWindow({ - url: 'https://metamask.zendesk.com/hc/en-us/articles/360015289932', - }) - }, - }, this.context.t('here')), - ]), - - h('div.new-account-import-form__select-section', [ - - h('div.new-account-import-form__select-label', this.context.t('selectType')), - - h(Select, { - className: 'new-account-import-form__select', - name: 'import-type-select', - clearable: false, - value: type || menuItems[0], - options: menuItems.map((type) => { - return { - value: type, - label: type, - } - }), - onChange: (opt) => { - this.setState({ type: opt.value }) - }, - }), - - ]), - - this.renderImportView(), - ]) - ) -} - -AccountImportSubview.prototype.renderImportView = function () { - const state = this.state || {} - const { type } = state - const menuItems = this.getMenuItemTexts() - const current = type || menuItems[0] - - switch (current) { - case this.context.t('privateKey'): - return h(PrivateKeyImportView) - case this.context.t('jsonFile'): - return h(JsonImportView) - default: - return h(JsonImportView) - } -} diff --git a/ui/app/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js deleted file mode 100644 index 90279bbbd..000000000 --- a/ui/app/components/pages/create-account/import-account/json.js +++ /dev/null @@ -1,159 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const { withRouter } = require('react-router-dom') -const { compose } = require('recompose') -const connect = require('react-redux').connect -const actions = require('../../../../actions') -const FileInput = require('react-simple-file-input').default -const { DEFAULT_ROUTE } = require('../../../../routes') -const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts' -import Button from '../../../button' - -class JsonImportSubview extends Component { - constructor (props) { - super(props) - - this.state = { - file: null, - fileContents: '', - } - } - - render () { - const { error } = this.props - - return ( - h('div.new-account-import-form__json', [ - - h('p', this.context.t('usedByClients')), - h('a.warning', { - href: HELP_LINK, - target: '_blank', - }, this.context.t('fileImportFail')), - - h(FileInput, { - readAs: 'text', - onLoad: this.onLoad.bind(this), - style: { - margin: '20px 0px 12px 34%', - fontSize: '15px', - display: 'flex', - justifyContent: 'center', - }, - }), - - h('input.new-account-import-form__input-password', { - type: 'password', - placeholder: this.context.t('enterPassword'), - id: 'json-password-box', - onKeyPress: this.createKeyringOnEnter.bind(this), - }), - - h('div.new-account-create-form__buttons', {}, [ - - h(Button, { - type: 'default', - large: true, - className: 'new-account-create-form__button', - onClick: () => this.props.history.push(DEFAULT_ROUTE), - }, [this.context.t('cancel')]), - - h(Button, { - type: 'primary', - large: true, - className: 'new-account-create-form__button', - onClick: () => this.createNewKeychain(), - }, [this.context.t('import')]), - - ]), - - error ? h('span.error', error) : null, - ]) - ) - } - - onLoad (event, file) { - this.setState({file: file, fileContents: event.target.result}) - } - - createKeyringOnEnter (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewKeychain() - } - } - - createNewKeychain () { - const { firstAddress, displayWarning, importNewJsonAccount, setSelectedAddress, history } = this.props - const state = this.state - - if (!state) { - const message = this.context.t('validFileImport') - return displayWarning(message) - } - - const { fileContents } = state - - if (!fileContents) { - const message = this.context.t('needImportFile') - return displayWarning(message) - } - - const passwordInput = document.getElementById('json-password-box') - const password = passwordInput.value - - if (!password) { - const message = this.context.t('needImportPassword') - return displayWarning(message) - } - - importNewJsonAccount([ fileContents, password ]) - .then(({ selectedAddress }) => { - if (selectedAddress) { - history.push(DEFAULT_ROUTE) - displayWarning(null) - } else { - displayWarning('Error importing account.') - setSelectedAddress(firstAddress) - } - }) - .catch(err => err && displayWarning(err.message || err)) - } -} - -JsonImportSubview.propTypes = { - error: PropTypes.string, - goHome: PropTypes.func, - displayWarning: PropTypes.func, - firstAddress: PropTypes.string, - importNewJsonAccount: PropTypes.func, - history: PropTypes.object, - setSelectedAddress: PropTypes.func, - t: PropTypes.func, -} - -const mapStateToProps = state => { - return { - error: state.appState.warning, - firstAddress: Object.keys(state.metamask.accounts)[0], - } -} - -const mapDispatchToProps = dispatch => { - return { - goHome: () => dispatch(actions.goHome()), - displayWarning: warning => dispatch(actions.displayWarning(warning)), - importNewJsonAccount: options => dispatch(actions.importNewAccount('JSON File', options)), - setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)), - } -} - -JsonImportSubview.contextTypes = { - t: PropTypes.func, -} - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(JsonImportSubview) diff --git a/ui/app/components/pages/create-account/import-account/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js deleted file mode 100644 index 8db1bfbdd..000000000 --- a/ui/app/components/pages/create-account/import-account/private-key.js +++ /dev/null @@ -1,112 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const { withRouter } = require('react-router-dom') -const { compose } = require('recompose') -const PropTypes = require('prop-types') -const connect = require('react-redux').connect -const actions = require('../../../../actions') -const { DEFAULT_ROUTE } = require('../../../../routes') -import Button from '../../../button' - -PrivateKeyImportView.contextTypes = { - t: PropTypes.func, -} - -module.exports = compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(PrivateKeyImportView) - - -function mapStateToProps (state) { - return { - error: state.appState.warning, - firstAddress: Object.keys(state.metamask.accounts)[0], - } -} - -function mapDispatchToProps (dispatch) { - return { - importNewAccount: (strategy, [ privateKey ]) => { - return dispatch(actions.importNewAccount(strategy, [ privateKey ])) - }, - displayWarning: (message) => dispatch(actions.displayWarning(message || null)), - setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)), - } -} - -inherits(PrivateKeyImportView, Component) -function PrivateKeyImportView () { - this.createKeyringOnEnter = this.createKeyringOnEnter.bind(this) - Component.call(this) -} - -PrivateKeyImportView.prototype.render = function () { - const { error, displayWarning } = this.props - - return ( - h('div.new-account-import-form__private-key', [ - - h('span.new-account-create-form__instruction', this.context.t('pastePrivateKey')), - - h('div.new-account-import-form__private-key-password-container', [ - - h('input.new-account-import-form__input-password', { - type: 'password', - id: 'private-key-box', - onKeyPress: e => this.createKeyringOnEnter(e), - }), - - ]), - - h('div.new-account-import-form__buttons', {}, [ - - h(Button, { - type: 'default', - large: true, - className: 'new-account-create-form__button', - onClick: () => { - displayWarning(null) - this.props.history.push(DEFAULT_ROUTE) - }, - }, [this.context.t('cancel')]), - - h(Button, { - type: 'primary', - large: true, - className: 'new-account-create-form__button', - onClick: () => this.createNewKeychain(), - }, [this.context.t('import')]), - - ]), - - error ? h('span.error', error) : null, - ]) - ) -} - -PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { - if (event.key === 'Enter') { - event.preventDefault() - this.createNewKeychain() - } -} - -PrivateKeyImportView.prototype.createNewKeychain = function () { - const input = document.getElementById('private-key-box') - const privateKey = input.value - const { importNewAccount, history, displayWarning, setSelectedAddress, firstAddress } = this.props - - importNewAccount('Private Key', [ privateKey ]) - .then(({ selectedAddress }) => { - if (selectedAddress) { - history.push(DEFAULT_ROUTE) - displayWarning(null) - } else { - displayWarning('Error importing account.') - setSelectedAddress(firstAddress) - } - }) - .catch(err => err && displayWarning(err.message || err)) -} diff --git a/ui/app/components/pages/create-account/import-account/seed.js b/ui/app/components/pages/create-account/import-account/seed.js deleted file mode 100644 index d98909baa..000000000 --- a/ui/app/components/pages/create-account/import-account/seed.js +++ /dev/null @@ -1,35 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const PropTypes = require('prop-types') -const connect = require('react-redux').connect - -SeedImportSubview.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps)(SeedImportSubview) - - -function mapStateToProps (state) { - return {} -} - -inherits(SeedImportSubview, Component) -function SeedImportSubview () { - Component.call(this) -} - -SeedImportSubview.prototype.render = function () { - return ( - h('div', { - style: { - }, - }, [ - this.context.t('pasteSeed'), - h('textarea'), - h('br'), - h('button', this.context.t('submit')), - ]) - ) -} diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js deleted file mode 100644 index d3de1ea01..000000000 --- a/ui/app/components/pages/create-account/index.js +++ /dev/null @@ -1,113 +0,0 @@ -const Component = require('react').Component -const { Switch, Route, matchPath } = require('react-router-dom') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../../actions') -const { getCurrentViewContext } = require('../../../selectors') -const classnames = require('classnames') -const NewAccountCreateForm = require('./new-account') -const NewAccountImportForm = require('./import-account') -const ConnectHardwareForm = require('./connect-hardware') -const { - NEW_ACCOUNT_ROUTE, - IMPORT_ACCOUNT_ROUTE, - CONNECT_HARDWARE_ROUTE, -} = require('../../../routes') - -class CreateAccountPage extends Component { - renderTabs () { - const { history, location } = this.props - - return h('div.new-account__tabs', [ - h('div.new-account__tabs__tab', { - className: classnames('new-account__tabs__tab', { - 'new-account__tabs__selected': matchPath(location.pathname, { - path: NEW_ACCOUNT_ROUTE, exact: true, - }), - }), - onClick: () => history.push(NEW_ACCOUNT_ROUTE), - }, [ - this.context.t('create'), - ]), - - h('div.new-account__tabs__tab', { - className: classnames('new-account__tabs__tab', { - 'new-account__tabs__selected': matchPath(location.pathname, { - path: IMPORT_ACCOUNT_ROUTE, exact: true, - }), - }), - onClick: () => history.push(IMPORT_ACCOUNT_ROUTE), - }, [ - this.context.t('import'), - ]), - h( - 'div.new-account__tabs__tab', - { - className: classnames('new-account__tabs__tab', { - 'new-account__tabs__selected': matchPath(location.pathname, { - path: CONNECT_HARDWARE_ROUTE, - exact: true, - }), - }), - onClick: () => history.push(CONNECT_HARDWARE_ROUTE), - }, - this.context.t('connect') - ), - ]) - } - - render () { - return h('div.new-account', {}, [ - h('div.new-account__header', [ - h('div.new-account__title', this.context.t('newAccount')), - this.renderTabs(), - ]), - h('div.new-account__form', [ - h(Switch, [ - h(Route, { - exact: true, - path: NEW_ACCOUNT_ROUTE, - component: NewAccountCreateForm, - }), - h(Route, { - exact: true, - path: IMPORT_ACCOUNT_ROUTE, - component: NewAccountImportForm, - }), - h(Route, { - exact: true, - path: CONNECT_HARDWARE_ROUTE, - component: ConnectHardwareForm, - }), - ]), - ]), - ]) - } -} - -CreateAccountPage.propTypes = { - location: PropTypes.object, - history: PropTypes.object, - t: PropTypes.func, -} - -CreateAccountPage.contextTypes = { - t: PropTypes.func, -} - -const mapStateToProps = state => ({ - displayedForm: getCurrentViewContext(state), -}) - -const mapDispatchToProps = dispatch => ({ - displayForm: form => dispatch(actions.setNewAccountForm(form)), - showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), - showExportPrivateKeyModal: () => { - dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' })) - }, - hideModal: () => dispatch(actions.hideModal()), - setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)), -}) - -module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage) diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js deleted file mode 100644 index 94a5fa487..000000000 --- a/ui/app/components/pages/create-account/new-account.js +++ /dev/null @@ -1,108 +0,0 @@ -const { Component } = require('react') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const connect = require('react-redux').connect -const actions = require('../../../actions') -const { DEFAULT_ROUTE } = require('../../../routes') -import Button from '../../button' - -class NewAccountCreateForm extends Component { - constructor (props, context) { - super(props) - - const { numberOfExistingAccounts = 0 } = props - const newAccountNumber = numberOfExistingAccounts + 1 - - this.state = { - newAccountName: '', - defaultAccountName: context.t('newAccountNumberName', [newAccountNumber]), - } - } - - render () { - const { newAccountName, defaultAccountName } = this.state - const { history, createAccount } = this.props - - return h('div.new-account-create-form', [ - - h('div.new-account-create-form__input-label', {}, [ - this.context.t('accountName'), - ]), - - h('div.new-account-create-form__input-wrapper', {}, [ - h('input.new-account-create-form__input', { - value: newAccountName, - placeholder: defaultAccountName, - onChange: event => this.setState({ newAccountName: event.target.value }), - }, []), - ]), - - h('div.new-account-create-form__buttons', {}, [ - - h(Button, { - type: 'default', - large: true, - className: 'new-account-create-form__button', - onClick: () => history.push(DEFAULT_ROUTE), - }, [this.context.t('cancel')]), - - h(Button, { - type: 'primary', - large: true, - className: 'new-account-create-form__button', - onClick: () => { - createAccount(newAccountName || defaultAccountName) - .then(() => history.push(DEFAULT_ROUTE)) - }, - }, [this.context.t('create')]), - - ]), - - ]) - } -} - -NewAccountCreateForm.propTypes = { - hideModal: PropTypes.func, - showImportPage: PropTypes.func, - showConnectPage: PropTypes.func, - createAccount: PropTypes.func, - numberOfExistingAccounts: PropTypes.number, - history: PropTypes.object, - t: PropTypes.func, -} - -const mapStateToProps = state => { - const { metamask: { network, selectedAddress, identities = {} } } = state - const numberOfExistingAccounts = Object.keys(identities).length - - return { - network, - address: selectedAddress, - numberOfExistingAccounts, - } -} - -const mapDispatchToProps = dispatch => { - return { - toCoinbase: address => dispatch(actions.buyEth({ network: '1', address, amount: 0 })), - hideModal: () => dispatch(actions.hideModal()), - createAccount: newAccountName => { - return dispatch(actions.addNewAccount()) - .then(newAccountAddress => { - if (newAccountName) { - dispatch(actions.setAccountLabel(newAccountAddress, newAccountName)) - } - }) - }, - showImportPage: () => dispatch(actions.showImportPage()), - showConnectPage: () => dispatch(actions.showConnectPage()), - } -} - -NewAccountCreateForm.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm) - diff --git a/ui/app/components/pages/home/home.component.js b/ui/app/components/pages/home/home.component.js deleted file mode 100644 index b9ec3c258..000000000 --- a/ui/app/components/pages/home/home.component.js +++ /dev/null @@ -1,87 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import Media from 'react-media' -import { Redirect } from 'react-router-dom' -import WalletView from '../../wallet-view' -import TransactionView from '../../transaction-view' -import ProviderApproval from '../provider-approval' - -import { - INITIALIZE_BACKUP_PHRASE_ROUTE, - RESTORE_VAULT_ROUTE, - CONFIRM_TRANSACTION_ROUTE, - NOTICE_ROUTE, - CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, -} from '../../../routes' - -export default class Home extends PureComponent { - static propTypes = { - history: PropTypes.object, - noActiveNotices: PropTypes.bool, - lostAccounts: PropTypes.array, - forgottenPassword: PropTypes.bool, - seedWords: PropTypes.string, - suggestedTokens: PropTypes.object, - unconfirmedTransactionsCount: PropTypes.number, - providerRequests: PropTypes.array, - } - - componentDidMount () { - const { - history, - suggestedTokens = {}, - unconfirmedTransactionsCount = 0, - } = this.props - - // suggested new tokens - if (Object.keys(suggestedTokens).length > 0) { - history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE) - } - - if (unconfirmedTransactionsCount > 0) { - history.push(CONFIRM_TRANSACTION_ROUTE) - } - } - - render () { - const { - noActiveNotices, - lostAccounts, - forgottenPassword, - seedWords, - providerRequests, - } = this.props - - // notices - if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) { - return <Redirect to={{ pathname: NOTICE_ROUTE }} /> - } - - // seed words - if (seedWords) { - return <Redirect to={{ pathname: INITIALIZE_BACKUP_PHRASE_ROUTE }}/> - } - - if (forgottenPassword) { - return <Redirect to={{ pathname: RESTORE_VAULT_ROUTE }} /> - } - - if (providerRequests && providerRequests.length > 0) { - return ( - <ProviderApproval providerRequest={providerRequests[0]} /> - ) - } - - return ( - <div className="main-container"> - <div className="account-and-transaction-details"> - <Media - query="(min-width: 576px)" - render={() => <WalletView />} - /> - <TransactionView /> - </div> - </div> - ) - } -} diff --git a/ui/app/components/pages/home/home.container.js b/ui/app/components/pages/home/home.container.js deleted file mode 100644 index bb8cf5e81..000000000 --- a/ui/app/components/pages/home/home.container.js +++ /dev/null @@ -1,32 +0,0 @@ -import Home from './home.component' -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 { - noActiveNotices, - lostAccounts, - seedWords, - suggestedTokens, - providerRequests, - } = metamask - const { forgottenPassword } = appState - - return { - noActiveNotices, - lostAccounts, - forgottenPassword, - seedWords, - suggestedTokens, - unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state), - providerRequests, - } -} - -export default compose( - withRouter, - connect(mapStateToProps) -)(Home) diff --git a/ui/app/components/pages/home/index.js b/ui/app/components/pages/home/index.js deleted file mode 100644 index 4474ba5b8..000000000 --- a/ui/app/components/pages/home/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './home.container' diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss deleted file mode 100644 index 6551278f5..000000000 --- a/ui/app/components/pages/index.scss +++ /dev/null @@ -1,7 +0,0 @@ -@import './unlock-page/index'; - -@import './add-token/index'; - -@import './confirm-add-token/index'; - -@import './settings/index'; diff --git a/ui/app/components/pages/initialized.js b/ui/app/components/pages/initialized.js deleted file mode 100644 index 3adf67b28..000000000 --- a/ui/app/components/pages/initialized.js +++ /dev/null @@ -1,25 +0,0 @@ -const { connect } = require('react-redux') -const PropTypes = require('prop-types') -const { Redirect } = require('react-router-dom') -const h = require('react-hyperscript') -const { INITIALIZE_ROUTE } = require('../../routes') -const MetamaskRoute = require('./metamask-route') - -const Initialized = props => { - return props.isInitialized - ? h(MetamaskRoute, { ...props }) - : h(Redirect, { to: { pathname: INITIALIZE_ROUTE } }) -} - -Initialized.propTypes = { - isInitialized: PropTypes.bool, -} - -const mapStateToProps = state => { - const { metamask: { isInitialized } } = state - return { - isInitialized, - } -} - -module.exports = connect(mapStateToProps)(Initialized) diff --git a/ui/app/components/pages/keychains/restore-vault.js b/ui/app/components/pages/keychains/restore-vault.js deleted file mode 100644 index d90a33e49..000000000 --- a/ui/app/components/pages/keychains/restore-vault.js +++ /dev/null @@ -1,189 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import {connect} from 'react-redux' -import { - createNewVaultAndRestore, - unMarkPasswordForgotten, -} from '../../../actions' -import { DEFAULT_ROUTE } from '../../../routes' -import TextField from '../../text-field' - -class RestoreVaultPage extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - warning: PropTypes.string, - createNewVaultAndRestore: PropTypes.func.isRequired, - leaveImportSeedScreenState: PropTypes.func, - history: PropTypes.object, - isLoading: PropTypes.bool, - }; - - state = { - seedPhrase: '', - password: '', - confirmPassword: '', - seedPhraseError: null, - passwordError: null, - confirmPasswordError: null, - } - - parseSeedPhrase = (seedPhrase) => { - return seedPhrase - .match(/\w+/g) - .join(' ') - } - - handleSeedPhraseChange (seedPhrase) { - let seedPhraseError = null - - if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) { - seedPhraseError = this.context.t('seedPhraseReq') - } - - this.setState({ seedPhrase, seedPhraseError }) - } - - handlePasswordChange (password) { - const { confirmPassword } = this.state - let confirmPasswordError = null - let passwordError = null - - if (password && password.length < 8) { - passwordError = this.context.t('passwordNotLongEnough') - } - - if (confirmPassword && password !== confirmPassword) { - confirmPasswordError = this.context.t('passwordsDontMatch') - } - - this.setState({ password, passwordError, confirmPasswordError }) - } - - handleConfirmPasswordChange (confirmPassword) { - const { password } = this.state - let confirmPasswordError = null - - if (password !== confirmPassword) { - confirmPasswordError = this.context.t('passwordsDontMatch') - } - - this.setState({ confirmPassword, confirmPasswordError }) - } - - onClick = () => { - const { password, seedPhrase } = this.state - const { - createNewVaultAndRestore, - leaveImportSeedScreenState, - history, - } = this.props - - leaveImportSeedScreenState() - createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase)) - .then(() => history.push(DEFAULT_ROUTE)) - } - - hasError () { - const { passwordError, confirmPasswordError, seedPhraseError } = this.state - return passwordError || confirmPasswordError || seedPhraseError - } - - render () { - const { - seedPhrase, - password, - confirmPassword, - seedPhraseError, - passwordError, - confirmPasswordError, - } = this.state - const { t } = this.context - const { isLoading } = this.props - const disabled = !seedPhrase || !password || !confirmPassword || isLoading || this.hasError() - - return ( - <div className="first-view-main-wrapper"> - <div className="first-view-main"> - <div className="import-account"> - <a - className="import-account__back-button" - onClick={e => { - e.preventDefault() - this.props.history.goBack() - }} - href="#" - > - {`< Back`} - </a> - <div className="import-account__title"> - { this.context.t('restoreAccountWithSeed') } - </div> - <div className="import-account__selector-label"> - { this.context.t('secretPhrase') } - </div> - <div className="import-account__input-wrapper"> - <label className="import-account__input-label">Wallet Seed</label> - <textarea - className="import-account__secret-phrase" - onChange={e => this.handleSeedPhraseChange(e.target.value)} - value={this.state.seedPhrase} - placeholder={this.context.t('separateEachWord')} - /> - </div> - <span className="error"> - { seedPhraseError } - </span> - <TextField - id="password" - label={t('newPassword')} - type="password" - className="first-time-flow__input" - value={this.state.password} - onChange={event => this.handlePasswordChange(event.target.value)} - error={passwordError} - autoComplete="new-password" - margin="normal" - largeLabel - /> - <TextField - id="confirm-password" - label={t('confirmPassword')} - type="password" - className="first-time-flow__input" - value={this.state.confirmPassword} - onChange={event => this.handleConfirmPasswordChange(event.target.value)} - error={confirmPasswordError} - autoComplete="confirm-password" - margin="normal" - largeLabel - /> - <button - className="first-time-flow__button" - onClick={() => !disabled && this.onClick()} - disabled={disabled} - > - {this.context.t('restore')} - </button> - </div> - </div> - </div> - ) - } -} - -RestoreVaultPage.contextTypes = { - t: PropTypes.func, -} - -export default connect( - ({ appState: { warning, isLoading } }) => ({ warning, isLoading }), - dispatch => ({ - leaveImportSeedScreenState: () => { - dispatch(unMarkPasswordForgotten()) - }, - createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)), - }) -)(RestoreVaultPage) diff --git a/ui/app/components/pages/keychains/reveal-seed.js b/ui/app/components/pages/keychains/reveal-seed.js deleted file mode 100644 index 32557066f..000000000 --- a/ui/app/components/pages/keychains/reveal-seed.js +++ /dev/null @@ -1,177 +0,0 @@ -const { Component } = require('react') -const { connect } = require('react-redux') -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const classnames = require('classnames') - -const { requestRevealSeedWords } = require('../../../actions') -const { DEFAULT_ROUTE } = require('../../../routes') -const ExportTextContainer = require('../../export-text-container') - -import Button from '../../button' - -const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN' -const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN' - -class RevealSeedPage extends Component { - constructor (props) { - super(props) - - this.state = { - screen: PASSWORD_PROMPT_SCREEN, - password: '', - seedWords: null, - error: null, - } - } - - componentDidMount () { - const passwordBox = document.getElementById('password-box') - if (passwordBox) { - passwordBox.focus() - } - } - - handleSubmit (event) { - event.preventDefault() - this.setState({ seedWords: null, error: null }) - this.props.requestRevealSeedWords(this.state.password) - .then(seedWords => this.setState({ seedWords, screen: REVEAL_SEED_SCREEN })) - .catch(error => this.setState({ error: error.message })) - } - - renderWarning () { - return ( - h('.page-container__warning-container', [ - h('img.page-container__warning-icon', { - src: 'images/warning.svg', - }), - h('.page-container__warning-message', [ - h('.page-container__warning-title', [this.context.t('revealSeedWordsWarningTitle')]), - h('div', [this.context.t('revealSeedWordsWarning')]), - ]), - ]) - ) - } - - renderContent () { - return this.state.screen === PASSWORD_PROMPT_SCREEN - ? this.renderPasswordPromptContent() - : this.renderRevealSeedContent() - } - - renderPasswordPromptContent () { - const { t } = this.context - - return ( - h('form', { - onSubmit: event => this.handleSubmit(event), - }, [ - h('label.input-label', { - htmlFor: 'password-box', - }, t('enterPasswordContinue')), - h('.input-group', [ - h('input.form-control', { - type: 'password', - placeholder: t('password'), - id: 'password-box', - value: this.state.password, - onChange: event => this.setState({ password: event.target.value }), - className: classnames({ 'form-control--error': this.state.error }), - }), - ]), - this.state.error && h('.reveal-seed__error', this.state.error), - ]) - ) - } - - renderRevealSeedContent () { - const { t } = this.context - - return ( - h('div', [ - h('label.reveal-seed__label', t('yourPrivateSeedPhrase')), - h(ExportTextContainer, { - text: this.state.seedWords, - filename: t('metamaskSeedWords'), - }), - ]) - ) - } - - renderFooter () { - return this.state.screen === PASSWORD_PROMPT_SCREEN - ? this.renderPasswordPromptFooter() - : this.renderRevealSeedFooter() - } - - renderPasswordPromptFooter () { - return ( - h('.page-container__footer', [ - h('header', [ - h(Button, { - type: 'default', - large: true, - className: 'page-container__footer-button', - onClick: () => this.props.history.push(DEFAULT_ROUTE), - }, this.context.t('cancel')), - h(Button, { - type: 'primary', - large: true, - className: 'page-container__footer-button', - onClick: event => this.handleSubmit(event), - disabled: this.state.password === '', - }, this.context.t('next')), - ]), - ]) - ) - } - - renderRevealSeedFooter () { - return ( - h('.page-container__footer', [ - h(Button, { - type: 'default', - large: true, - className: 'page-container__footer-button', - onClick: () => this.props.history.push(DEFAULT_ROUTE), - }, this.context.t('close')), - ]) - ) - } - - render () { - return ( - h('.page-container', [ - h('.page-container__header', [ - h('.page-container__title', this.context.t('revealSeedWordsTitle')), - h('.page-container__subtitle', this.context.t('revealSeedWordsDescription')), - ]), - h('.page-container__content', [ - this.renderWarning(), - h('.reveal-seed__content', [ - this.renderContent(), - ]), - ]), - this.renderFooter(), - ]) - ) - } -} - -RevealSeedPage.propTypes = { - requestRevealSeedWords: PropTypes.func, - history: PropTypes.object, -} - -RevealSeedPage.contextTypes = { - t: PropTypes.func, -} - -const mapDispatchToProps = dispatch => { - return { - requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)), - } -} - -module.exports = connect(null, mapDispatchToProps)(RevealSeedPage) diff --git a/ui/app/components/pages/metamask-route.js b/ui/app/components/pages/metamask-route.js deleted file mode 100644 index 23c5b5199..000000000 --- a/ui/app/components/pages/metamask-route.js +++ /dev/null @@ -1,28 +0,0 @@ -const { connect } = require('react-redux') -const PropTypes = require('prop-types') -const { Route } = require('react-router-dom') -const h = require('react-hyperscript') - -const MetamaskRoute = ({ component, mascaraComponent, isMascara, ...props }) => { - return ( - h(Route, { - ...props, - component: isMascara && mascaraComponent ? mascaraComponent : component, - }) - ) -} - -MetamaskRoute.propTypes = { - component: PropTypes.func, - mascaraComponent: PropTypes.func, - isMascara: PropTypes.bool, -} - -const mapStateToProps = state => { - const { metamask: { isMascara } } = state - return { - isMascara, - } -} - -module.exports = connect(mapStateToProps)(MetamaskRoute) diff --git a/ui/app/components/pages/notice.js b/ui/app/components/pages/notice.js deleted file mode 100644 index a9077b98b..000000000 --- a/ui/app/components/pages/notice.js +++ /dev/null @@ -1,203 +0,0 @@ -const { Component } = require('react') -const h = require('react-hyperscript') -const { connect } = require('react-redux') -const PropTypes = require('prop-types') -const ReactMarkdown = require('react-markdown') -const linker = require('extension-link-enabler') -const generateLostAccountsNotice = require('../../../lib/lost-accounts-notice') -const findDOMNode = require('react-dom').findDOMNode -const actions = require('../../actions') -const { DEFAULT_ROUTE } = require('../../routes') - -class Notice extends Component { - constructor (props) { - super(props) - - this.state = { - disclaimerDisabled: true, - } - } - - componentWillMount () { - if (!this.props.notice) { - this.props.history.push(DEFAULT_ROUTE) - } - } - - componentDidMount () { - // eslint-disable-next-line react/no-find-dom-node - var node = findDOMNode(this) - linker.setupListener(node) - if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { - this.setState({ disclaimerDisabled: false }) - } - } - - componentWillReceiveProps (nextProps) { - if (!nextProps.notice) { - this.props.history.push(DEFAULT_ROUTE) - } - } - - componentWillUnmount () { - // eslint-disable-next-line react/no-find-dom-node - var node = findDOMNode(this) - linker.teardownListener(node) - } - - handleAccept () { - this.setState({ disclaimerDisabled: true }) - this.props.onConfirm() - } - - render () { - const { notice = {} } = this.props - const { title, date, body } = notice - const { disclaimerDisabled } = this.state - - return ( - h('.flex-column.flex-center.flex-grow', { - style: { - width: '100%', - }, - }, [ - h('h3.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - title, - ]), - - h('h5.flex-center.text-transform-uppercase.terms-header', { - style: { - background: '#EBEBEB', - color: '#AEAEAE', - marginBottom: 24, - width: '100%', - fontSize: '20px', - textAlign: 'center', - padding: 6, - }, - }, [ - date, - ]), - - h('style', ` - - .markdown { - overflow-x: hidden; - } - - .markdown h1, .markdown h2, .markdown h3 { - margin: 10px 0; - font-weight: bold; - } - - .markdown strong { - font-weight: bold; - } - .markdown em { - font-style: italic; - } - - .markdown p { - margin: 10px 0; - } - - .markdown a { - color: #df6b0e; - } - - `), - - h('div.markdown', { - onScroll: (e) => { - var object = e.currentTarget - if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { - this.setState({ disclaimerDisabled: false }) - } - }, - style: { - background: 'rgb(235, 235, 235)', - height: '310px', - padding: '6px', - width: '90%', - overflowY: 'scroll', - scroll: 'auto', - }, - }, [ - h(ReactMarkdown, { - className: 'notice-box', - source: body, - skipHtml: true, - }), - ]), - - h('button.primary', { - disabled: disclaimerDisabled, - onClick: () => this.handleAccept(), - style: { - marginTop: '18px', - }, - }, 'Accept'), - ]) - ) - } - -} - -const mapStateToProps = state => { - const { metamask } = state - const { noActiveNotices, nextUnreadNotice, lostAccounts } = metamask - - return { - noActiveNotices, - nextUnreadNotice, - lostAccounts, - } -} - -Notice.propTypes = { - notice: PropTypes.object, - onConfirm: PropTypes.func, - history: PropTypes.object, -} - -const mapDispatchToProps = dispatch => { - return { - markNoticeRead: nextUnreadNotice => dispatch(actions.markNoticeRead(nextUnreadNotice)), - markAccountsFound: () => dispatch(actions.markAccountsFound()), - } -} - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { noActiveNotices, nextUnreadNotice, lostAccounts } = stateProps - const { markNoticeRead, markAccountsFound } = dispatchProps - - let notice - let onConfirm - - if (!noActiveNotices) { - notice = nextUnreadNotice - onConfirm = () => markNoticeRead(nextUnreadNotice) - } else if (lostAccounts && lostAccounts.length > 0) { - notice = generateLostAccountsNotice(lostAccounts) - onConfirm = () => markAccountsFound() - } - - return { - ...stateProps, - ...dispatchProps, - ...ownProps, - notice, - onConfirm, - } -} - -module.exports = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Notice) diff --git a/ui/app/components/pages/provider-approval/index.js b/ui/app/components/pages/provider-approval/index.js deleted file mode 100644 index 4162f3155..000000000 --- a/ui/app/components/pages/provider-approval/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './provider-approval.container' diff --git a/ui/app/components/pages/provider-approval/provider-approval.component.js b/ui/app/components/pages/provider-approval/provider-approval.component.js deleted file mode 100644 index da98bc3fc..000000000 --- a/ui/app/components/pages/provider-approval/provider-approval.component.js +++ /dev/null @@ -1,28 +0,0 @@ -import PropTypes from 'prop-types' -import React, { Component } from 'react' -import ProviderPageContainer from '../../provider-page-container' - -export default class ProviderApproval extends Component { - static propTypes = { - approveProviderRequest: PropTypes.func.isRequired, - providerRequest: PropTypes.object.isRequired, - rejectProviderRequest: PropTypes.func.isRequired, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - render () { - const { approveProviderRequest, providerRequest, rejectProviderRequest } = this.props - return ( - <ProviderPageContainer - approveProviderRequest={approveProviderRequest} - origin={providerRequest.origin} - rejectProviderRequest={rejectProviderRequest} - siteImage={providerRequest.siteImage} - siteTitle={providerRequest.siteTitle} - /> - ) - } -} diff --git a/ui/app/components/pages/provider-approval/provider-approval.container.js b/ui/app/components/pages/provider-approval/provider-approval.container.js deleted file mode 100644 index b223244a1..000000000 --- a/ui/app/components/pages/provider-approval/provider-approval.container.js +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from 'react-redux' -import ProviderApproval from './provider-approval.component' -import { approveProviderRequest, rejectProviderRequest } from '../../../actions' - -function mapDispatchToProps (dispatch) { - return { - approveProviderRequest: origin => dispatch(approveProviderRequest(origin)), - rejectProviderRequest: origin => dispatch(rejectProviderRequest(origin)), - } -} - -export default connect(null, mapDispatchToProps)(ProviderApproval) diff --git a/ui/app/components/pages/settings/index.js b/ui/app/components/pages/settings/index.js deleted file mode 100644 index 44a9ffa63..000000000 --- a/ui/app/components/pages/settings/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './settings.component' diff --git a/ui/app/components/pages/settings/index.scss b/ui/app/components/pages/settings/index.scss deleted file mode 100644 index 138ebcfc5..000000000 --- a/ui/app/components/pages/settings/index.scss +++ /dev/null @@ -1,80 +0,0 @@ -@import './info-tab/index'; - -@import './settings-tab/index'; - -.settings-page { - position: relative; - background: $white; - display: flex; - flex-flow: column nowrap; - - &__header { - padding: 25px 25px 0; - } - - &__close-button::after { - content: '\00D7'; - font-size: 40px; - color: $dusty-gray; - position: absolute; - top: 25px; - right: 30px; - cursor: pointer; - } - - &__content { - padding: 25px; - height: auto; - overflow: auto; - } - - &__content-row { - display: flex; - flex-direction: row; - padding: 10px 0 20px; - - @media screen and (max-width: 575px) { - flex-direction: column; - padding: 10px 0; - } - } - - &__content-item { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - padding: 0 5px; - min-height: 71px; - - @media screen and (max-width: 575px) { - height: initial; - padding: 5px 0; - } - - &--without-height { - height: initial; - } - } - - &__content-label { - text-transform: capitalize; - } - - &__content-description { - font-size: 14px; - color: $dusty-gray; - padding-top: 5px; - } - - &__content-item-col { - max-width: 300px; - display: flex; - flex-direction: column; - - @media screen and (max-width: 575px) { - max-width: 100%; - width: 100%; - } - } -} diff --git a/ui/app/components/pages/settings/info-tab/index.js b/ui/app/components/pages/settings/info-tab/index.js deleted file mode 100644 index 7556a258d..000000000 --- a/ui/app/components/pages/settings/info-tab/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './info-tab.component' diff --git a/ui/app/components/pages/settings/info-tab/index.scss b/ui/app/components/pages/settings/info-tab/index.scss deleted file mode 100644 index 43ad6f652..000000000 --- a/ui/app/components/pages/settings/info-tab/index.scss +++ /dev/null @@ -1,56 +0,0 @@ -.info-tab { - &__logo-wrapper { - height: 80px; - margin-bottom: 20px; - } - - &__logo { - max-height: 100%; - max-width: 100%; - } - - &__item { - padding: 10px 0; - } - - &__link-header { - padding-bottom: 15px; - - @media screen and (max-width: 575px) { - padding-bottom: 5px; - } - } - - &__link-item { - padding: 15px 0; - - @media screen and (max-width: 575px) { - padding: 5px 0; - } - } - - &__link-text { - color: $curious-blue; - } - - &__version-number { - padding-top: 5px; - font-size: 13px; - color: $dusty-gray; - } - - &__separator { - margin: 15px 0; - width: 80px; - border-color: $alto; - border: none; - height: 1px; - background-color: $alto; - color: $alto; - } - - &__about { - color: $dusty-gray; - margin-bottom: 15px; - } -} diff --git a/ui/app/components/pages/settings/info-tab/info-tab.component.js b/ui/app/components/pages/settings/info-tab/info-tab.component.js deleted file mode 100644 index 72f7d835e..000000000 --- a/ui/app/components/pages/settings/info-tab/info-tab.component.js +++ /dev/null @@ -1,136 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' - -export default class InfoTab extends PureComponent { - state = { - version: global.platform.getVersion(), - } - - static propTypes = { - tab: PropTypes.string, - metamask: PropTypes.object, - setCurrentCurrency: PropTypes.func, - setRpcTarget: PropTypes.func, - displayWarning: PropTypes.func, - revealSeedConfirmation: PropTypes.func, - warning: PropTypes.string, - location: PropTypes.object, - history: PropTypes.object, - } - - static contextTypes = { - t: PropTypes.func, - } - - renderInfoLinks () { - const { t } = this.context - - return ( - <div className="settings-page__content-item settings-page__content-item--without-height"> - <div className="info-tab__link-header"> - { t('links') } - </div> - <div className="info-tab__link-item"> - <a - href="https://metamask.io/privacy.html" - target="_blank" - rel="noopener noreferrer" - > - <span className="info-tab__link-text"> - { t('privacyMsg') } - </span> - </a> - </div> - <div className="info-tab__link-item"> - <a - href="https://metamask.io/terms.html" - target="_blank" - rel="noopener noreferrer" - > - <span className="info-tab__link-text"> - { t('terms') } - </span> - </a> - </div> - <div className="info-tab__link-item"> - <a - href="https://metamask.io/attributions.html" - target="_blank" - rel="noopener noreferrer" - > - <span className="info-tab__link-text"> - { t('attributions') } - </span> - </a> - </div> - <hr className="info-tab__separator" /> - <div className="info-tab__link-item"> - <a - href="https://support.metamask.io" - target="_blank" - rel="noopener noreferrer" - > - <span className="info-tab__link-text"> - { t('supportCenter') } - </span> - </a> - </div> - <div className="info-tab__link-item"> - <a - href="https://metamask.io/" - target="_blank" - rel="noopener noreferrer" - > - <span className="info-tab__link-text"> - { t('visitWebSite') } - </span> - </a> - </div> - <div className="info-tab__link-item"> - <a - href="mailto:help@metamask.io?subject=Feedback" - target="_blank" - rel="noopener noreferrer" - > - <span className="info-tab__link-text"> - { t('emailUs') } - </span> - </a> - </div> - </div> - ) - } - - render () { - const { t } = this.context - - return ( - <div className="settings-page__content"> - <div className="settings-page__content-row"> - <div className="settings-page__content-item settings-page__content-item--without-height"> - <div className="info-tab__logo-wrapper"> - <img - src="images/info-logo.png" - className="info-tab__logo" - /> - </div> - <div className="info-tab__item"> - <div className="info-tab__version-header"> - { t('metamaskVersion') } - </div> - <div className="info-tab__version-number"> - { this.state.version } - </div> - </div> - <div className="info-tab__item"> - <div className="info-tab__about"> - { t('builtInCalifornia') } - </div> - </div> - </div> - { this.renderInfoLinks() } - </div> - </div> - ) - } -} diff --git a/ui/app/components/pages/settings/settings-tab/index.js b/ui/app/components/pages/settings/settings-tab/index.js deleted file mode 100644 index 9fdaafd3f..000000000 --- a/ui/app/components/pages/settings/settings-tab/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './settings-tab.container' diff --git a/ui/app/components/pages/settings/settings-tab/index.scss b/ui/app/components/pages/settings/settings-tab/index.scss deleted file mode 100644 index ef32b0e4c..000000000 --- a/ui/app/components/pages/settings/settings-tab/index.scss +++ /dev/null @@ -1,69 +0,0 @@ -.settings-tab { - &__error { - padding-bottom: 20px; - text-align: center; - color: $crimson; - } - - &__advanced-link { - color: $curious-blue; - padding-left: 5px; - } - - &__rpc-save-button { - align-self: flex-end; - padding: 5px; - text-transform: uppercase; - color: $dusty-gray; - cursor: pointer; - width: 25%; - min-width: 80px; - height: 33px; - } - - &__button--red { - border-color: lighten($monzo, 10%); - color: $monzo; - - &:active { - background: lighten($monzo, 55%); - border-color: $monzo; - } - - &:hover { - border-color: $monzo; - } - } - - &__button--orange { - border-color: lighten($ecstasy, 20%); - color: $ecstasy; - - &:active { - background: lighten($ecstasy, 40%); - border-color: $ecstasy; - } - - &:hover { - border-color: $ecstasy; - } - } - - &__radio-buttons { - display: flex; - align-items: center; - } - - &__radio-button { - display: flex; - align-items: center; - - &:not(:last-child) { - margin-right: 16px; - } - } - - &__radio-label { - padding-left: 4px; - } -} diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js deleted file mode 100644 index a0a8ed47e..000000000 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js +++ /dev/null @@ -1,544 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import infuraCurrencies from '../../../../infura-conversion.json' -import validUrl from 'valid-url' -import { exportAsFile } from '../../../../util' -import SimpleDropdown from '../../../dropdowns/simple-dropdown' -import ToggleButton from 'react-toggle-button' -import { REVEAL_SEED_ROUTE } from '../../../../routes' -import locales from '../../../../../../app/_locales/index.json' -import TextField from '../../../text-field' -import Button from '../../../button' - -const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => { - return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase()) -}) - -const infuraCurrencyOptions = sortedCurrencies.map(({ quote: { code, name } }) => { - return { - displayValue: `${code.toUpperCase()} - ${name}`, - key: code, - value: code, - } -}) - -const localeOptions = locales.map(locale => { - return { - displayValue: `${locale.name}`, - key: locale.code, - value: locale.code, - } -}) - -export default class SettingsTab extends PureComponent { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - metamask: PropTypes.object, - setUseBlockie: PropTypes.func, - setHexDataFeatureFlag: PropTypes.func, - setPrivacyMode: PropTypes.func, - privacyMode: PropTypes.bool, - setCurrentCurrency: PropTypes.func, - setRpcTarget: PropTypes.func, - delRpcTarget: PropTypes.func, - displayWarning: PropTypes.func, - revealSeedConfirmation: PropTypes.func, - setFeatureFlagToBeta: PropTypes.func, - showClearApprovalModal: PropTypes.func, - showResetAccountConfirmationModal: PropTypes.func, - warning: PropTypes.string, - history: PropTypes.object, - isMascara: PropTypes.bool, - updateCurrentLocale: PropTypes.func, - currentLocale: PropTypes.string, - useBlockie: PropTypes.bool, - sendHexData: PropTypes.bool, - currentCurrency: PropTypes.string, - conversionDate: PropTypes.number, - nativeCurrency: PropTypes.string, - useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, - setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func, - } - - state = { - newRpc: '', - chainId: '', - showOptions: false, - ticker: '', - nickname: '', - } - - renderCurrentConversion () { - const { t } = this.context - const { currentCurrency, conversionDate, setCurrentCurrency } = this.props - - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span>{ t('currentConversion') }</span> - <span className="settings-page__content-description"> - { t('updatedWithDate', [Date(conversionDate)]) } - </span> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <SimpleDropdown - placeholder={t('selectCurrency')} - options={infuraCurrencyOptions} - selectedOption={currentCurrency} - onSelect={newCurrency => setCurrentCurrency(newCurrency)} - /> - </div> - </div> - </div> - ) - } - - renderCurrentLocale () { - const { t } = this.context - const { updateCurrentLocale, currentLocale } = this.props - const currentLocaleMeta = locales.find(locale => locale.code === currentLocale) - const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : '' - - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span className="settings-page__content-label"> - { t('currentLanguage') } - </span> - <span className="settings-page__content-description"> - { currentLocaleName } - </span> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <SimpleDropdown - placeholder={t('selectLocale')} - options={localeOptions} - selectedOption={currentLocale} - onSelect={async newLocale => updateCurrentLocale(newLocale)} - /> - </div> - </div> - </div> - ) - } - - renderNewRpcUrl () { - const { t } = this.context - const { newRpc, chainId, ticker, nickname } = this.state - - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span>{ t('newNetwork') }</span> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <TextField - type="text" - id="new-rpc" - placeholder={t('rpcURL')} - value={newRpc} - onChange={e => this.setState({ newRpc: e.target.value })} - onKeyPress={e => { - if (e.key === 'Enter') { - this.validateRpc(newRpc, chainId, ticker, nickname) - } - }} - fullWidth - margin="dense" - /> - <TextField - type="text" - id="chainid" - placeholder={t('optionalChainId')} - value={chainId} - onChange={e => this.setState({ chainId: e.target.value })} - onKeyPress={e => { - if (e.key === 'Enter') { - this.validateRpc(newRpc, chainId, ticker, nickname) - } - }} - style={{ - display: this.state.showOptions ? null : 'none', - }} - fullWidth - margin="dense" - /> - <TextField - type="text" - id="ticker" - placeholder={t('optionalSymbol')} - value={ticker} - onChange={e => this.setState({ ticker: e.target.value })} - onKeyPress={e => { - if (e.key === 'Enter') { - this.validateRpc(newRpc, chainId, ticker, nickname) - } - }} - style={{ - display: this.state.showOptions ? null : 'none', - }} - fullWidth - margin="dense" - /> - <TextField - type="text" - id="nickname" - placeholder={t('optionalNickname')} - value={nickname} - onChange={e => this.setState({ nickname: e.target.value })} - onKeyPress={e => { - if (e.key === 'Enter') { - this.validateRpc(newRpc, chainId, ticker, nickname) - } - }} - style={{ - display: this.state.showOptions ? null : 'none', - }} - fullWidth - margin="dense" - /> - <div className="flex-row flex-align-center space-between"> - <span className="settings-tab__advanced-link" - onClick={e => { - e.preventDefault() - this.setState({ showOptions: !this.state.showOptions }) - }} - > - { t(this.state.showOptions ? 'hideAdvancedOptions' : 'showAdvancedOptions') } - </span> - <button - className="button btn-primary settings-tab__rpc-save-button" - onClick={e => { - e.preventDefault() - this.validateRpc(newRpc, chainId, ticker, nickname) - }} - > - { t('save') } - </button> - </div> - </div> - </div> - </div> - ) - } - - validateRpc (newRpc, chainId, ticker = 'ETH', nickname) { - const { setRpcTarget, displayWarning } = this.props - - if (validUrl.isWebUri(newRpc)) { - setRpcTarget(newRpc, chainId, ticker, nickname) - } else { - const appendedRpc = `http://${newRpc}` - - if (validUrl.isWebUri(appendedRpc)) { - displayWarning(this.context.t('uriErrorMsg')) - } else { - displayWarning(this.context.t('invalidRPC')) - } - } - } - - renderStateLogs () { - const { t } = this.context - const { displayWarning } = this.props - - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span>{ t('stateLogs') }</span> - <span className="settings-page__content-description"> - { t('stateLogsDescription') } - </span> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <Button - type="primary" - large - onClick={() => { - window.logStateString((err, result) => { - if (err) { - displayWarning(t('stateLogError')) - } else { - exportAsFile('MetaMask State Logs.json', result) - } - }) - }} - > - { t('downloadStateLogs') } - </Button> - </div> - </div> - </div> - ) - } - - renderClearApproval () { - const { t } = this.context - const { showClearApprovalModal } = this.props - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span>{ t('approvalData') }</span> - <span className="settings-page__content-description"> - { t('approvalDataDescription') } - </span> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <Button - type="secondary" - large - className="settings-tab__button--orange" - onClick={event => { - event.preventDefault() - showClearApprovalModal() - }} - > - { t('clearApprovalData') } - </Button> - </div> - </div> - </div> - ) - } - - renderSeedWords () { - const { t } = this.context - const { history } = this.props - - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span>{ t('revealSeedWords') }</span> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <Button - type="secondary" - large - onClick={event => { - event.preventDefault() - history.push(REVEAL_SEED_ROUTE) - }} - > - { t('revealSeedWords') } - </Button> - </div> - </div> - </div> - ) - } - - renderOldUI () { - const { t } = this.context - const { setFeatureFlagToBeta } = this.props - - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span>{ t('useOldUI') }</span> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <Button - type="secondary" - large - className="settings-tab__button--orange" - onClick={event => { - event.preventDefault() - setFeatureFlagToBeta() - }} - > - { t('useOldUI') } - </Button> - </div> - </div> - </div> - ) - } - - renderResetAccount () { - const { t } = this.context - const { showResetAccountConfirmationModal } = this.props - - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span>{ t('resetAccount') }</span> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <Button - type="secondary" - large - className="settings-tab__button--orange" - onClick={event => { - event.preventDefault() - showResetAccountConfirmationModal() - }} - > - { t('resetAccount') } - </Button> - </div> - </div> - </div> - ) - } - - renderBlockieOptIn () { - const { useBlockie, setUseBlockie } = this.props - - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span>{ this.context.t('blockiesIdenticon') }</span> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <ToggleButton - value={useBlockie} - onToggle={value => setUseBlockie(!value)} - activeLabel="" - inactiveLabel="" - /> - </div> - </div> - </div> - ) - } - - renderHexDataOptIn () { - const { t } = this.context - const { sendHexData, setHexDataFeatureFlag } = this.props - - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span>{ t('showHexData') }</span> - <div className="settings-page__content-description"> - { t('showHexDataDescription') } - </div> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <ToggleButton - value={sendHexData} - onToggle={value => setHexDataFeatureFlag(!value)} - activeLabel="" - inactiveLabel="" - /> - </div> - </div> - </div> - ) - } - - renderUsePrimaryCurrencyOptions () { - const { t } = this.context - const { - nativeCurrency, - setUseNativeCurrencyAsPrimaryCurrencyPreference, - useNativeCurrencyAsPrimaryCurrency, - } = this.props - - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span>{ t('primaryCurrencySetting') }</span> - <div className="settings-page__content-description"> - { t('primaryCurrencySettingDescription') } - </div> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <div className="settings-tab__radio-buttons"> - <div className="settings-tab__radio-button"> - <input - type="radio" - id="native-primary-currency" - onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(true)} - checked={Boolean(useNativeCurrencyAsPrimaryCurrency)} - /> - <label - htmlFor="native-primary-currency" - className="settings-tab__radio-label" - > - { nativeCurrency } - </label> - </div> - <div className="settings-tab__radio-button"> - <input - type="radio" - id="fiat-primary-currency" - onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(false)} - checked={!useNativeCurrencyAsPrimaryCurrency} - /> - <label - htmlFor="fiat-primary-currency" - className="settings-tab__radio-label" - > - { t('fiat') } - </label> - </div> - </div> - </div> - </div> - </div> - ) - } - - renderPrivacyOptIn () { - const { t } = this.context - const { privacyMode, setPrivacyMode } = this.props - - return ( - <div className="settings-page__content-row"> - <div className="settings-page__content-item"> - <span>{ t('privacyMode') }</span> - <div className="settings-page__content-description"> - { t('privacyModeDescription') } - </div> - </div> - <div className="settings-page__content-item"> - <div className="settings-page__content-item-col"> - <ToggleButton - value={privacyMode} - onToggle={value => setPrivacyMode(!value)} - activeLabel="" - inactiveLabel="" - /> - </div> - </div> - </div> - ) - } - - render () { - const { warning, isMascara } = this.props - - return ( - <div className="settings-page__content"> - { warning && <div className="settings-tab__error">{ warning }</div> } - { this.renderCurrentConversion() } - { this.renderUsePrimaryCurrencyOptions() } - { this.renderCurrentLocale() } - { this.renderNewRpcUrl() } - { this.renderStateLogs() } - { this.renderSeedWords() } - { !isMascara && this.renderOldUI() } - { this.renderResetAccount() } - { this.renderClearApproval() } - { this.renderPrivacyOptIn() } - { this.renderHexDataOptIn() } - { this.renderBlockieOptIn() } - </div> - ) - } -} diff --git a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js deleted file mode 100644 index b6c33a5b2..000000000 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js +++ /dev/null @@ -1,74 +0,0 @@ -import SettingsTab from './settings-tab.component' -import { compose } from 'recompose' -import { connect } from 'react-redux' -import { withRouter } from 'react-router-dom' -import { - setCurrentCurrency, - setRpcTarget, - displayWarning, - revealSeedConfirmation, - setUseBlockie, - updateCurrentLocale, - setFeatureFlag, - showModal, - setUseNativeCurrencyAsPrimaryCurrencyPreference, -} from '../../../../actions' -import { preferencesSelector } from '../../../../selectors' - -const mapStateToProps = state => { - const { appState: { warning }, metamask } = state - const { - currentCurrency, - conversionDate, - nativeCurrency, - useBlockie, - featureFlags: { - sendHexData, - privacyMode, - } = {}, - provider = {}, - isMascara, - currentLocale, - } = metamask - const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state) - - return { - warning, - isMascara, - currentLocale, - currentCurrency, - conversionDate, - nativeCurrency, - useBlockie, - sendHexData, - privacyMode, - provider, - useNativeCurrencyAsPrimaryCurrency, - } -} - -const mapDispatchToProps = dispatch => { - return { - setCurrentCurrency: currency => dispatch(setCurrentCurrency(currency)), - setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(setRpcTarget(newRpc, chainId, ticker, nickname)), - displayWarning: warning => dispatch(displayWarning(warning)), - revealSeedConfirmation: () => dispatch(revealSeedConfirmation()), - setUseBlockie: value => dispatch(setUseBlockie(value)), - updateCurrentLocale: key => dispatch(updateCurrentLocale(key)), - setFeatureFlagToBeta: () => { - return dispatch(setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')) - }, - setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)), - setPrivacyMode: enabled => dispatch(setFeatureFlag('privacyMode', enabled)), - showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })), - setUseNativeCurrencyAsPrimaryCurrencyPreference: value => { - return dispatch(setUseNativeCurrencyAsPrimaryCurrencyPreference(value)) - }, - showClearApprovalModal: () => dispatch(showModal({ name: 'CLEAR_APPROVED_ORIGINS' })), - } -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(SettingsTab) diff --git a/ui/app/components/pages/settings/settings.component.js b/ui/app/components/pages/settings/settings.component.js deleted file mode 100644 index 94a97bba1..000000000 --- a/ui/app/components/pages/settings/settings.component.js +++ /dev/null @@ -1,54 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import { Switch, Route, matchPath } from 'react-router-dom' -import TabBar from '../../tab-bar' -import SettingsTab from './settings-tab' -import InfoTab from './info-tab' -import { DEFAULT_ROUTE, SETTINGS_ROUTE, INFO_ROUTE } from '../../../routes' - -export default class SettingsPage extends PureComponent { - static propTypes = { - location: PropTypes.object, - history: PropTypes.object, - t: PropTypes.func, - } - - static contextTypes = { - t: PropTypes.func, - } - - render () { - const { history, location } = this.props - - return ( - <div className="main-container settings-page"> - <div className="settings-page__header"> - <div - className="settings-page__close-button" - onClick={() => history.push(DEFAULT_ROUTE)} - /> - <TabBar - tabs={[ - { content: this.context.t('settings'), key: SETTINGS_ROUTE }, - { content: this.context.t('info'), key: INFO_ROUTE }, - ]} - isActive={key => matchPath(location.pathname, { path: key, exact: true })} - onSelect={key => history.push(key)} - /> - </div> - <Switch> - <Route - exact - path={INFO_ROUTE} - component={InfoTab} - /> - <Route - exact - path={SETTINGS_ROUTE} - component={SettingsTab} - /> - </Switch> - </div> - ) - } -} diff --git a/ui/app/components/pages/unlock-page/index.js b/ui/app/components/pages/unlock-page/index.js deleted file mode 100644 index be80cde4f..000000000 --- a/ui/app/components/pages/unlock-page/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import UnlockPage from './unlock-page.container' -module.exports = UnlockPage diff --git a/ui/app/components/pages/unlock-page/index.scss b/ui/app/components/pages/unlock-page/index.scss deleted file mode 100644 index 6bd52282d..000000000 --- a/ui/app/components/pages/unlock-page/index.scss +++ /dev/null @@ -1,52 +0,0 @@ -.unlock-page { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: center; - width: 357px; - padding: 30px; - font-weight: 400; - color: $silver-chalice; - - &__container { - background: $white; - display: flex; - align-self: stretch; - justify-content: center; - flex: 1 0 auto; - height: 100vh; - } - - &__mascot-container { - margin-top: 24px; - } - - &__title { - margin-top: 5px; - font-size: 2rem; - font-weight: 800; - color: $tundora; - } - - &__form { - width: 100%; - margin: 56px 0 8px; - } - - &__links { - margin-top: 25px; - width: 100%; - } - - &__link { - cursor: pointer; - - &--import { - color: $ecstasy; - } - - &--use-classic { - margin-top: 10px; - } - } -} diff --git a/ui/app/components/pages/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js deleted file mode 100644 index 94915df76..000000000 --- a/ui/app/components/pages/unlock-page/unlock-page.component.js +++ /dev/null @@ -1,179 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import Button from '@material-ui/core/Button' -import TextField from '../../text-field' -import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' -import { getEnvironmentType } from '../../../../../app/scripts/lib/util' -import getCaretCoordinates from 'textarea-caret' -import { EventEmitter } from 'events' -import Mascot from '../../mascot' -import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../../routes' - -export default class UnlockPage extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - forgotPassword: PropTypes.func, - tryUnlockMetamask: PropTypes.func, - markPasswordForgotten: PropTypes.func, - history: PropTypes.object, - isUnlocked: PropTypes.bool, - useOldInterface: PropTypes.func, - } - - constructor (props) { - super(props) - - this.state = { - password: '', - error: null, - } - - this.submitting = false - this.animationEventEmitter = new EventEmitter() - } - - componentWillMount () { - const { isUnlocked, history } = this.props - - if (isUnlocked) { - history.push(DEFAULT_ROUTE) - } - } - - async handleSubmit (event) { - event.preventDefault() - event.stopPropagation() - - const { password } = this.state - const { tryUnlockMetamask, history } = this.props - - if (password === '' || this.submitting) { - return - } - - this.setState({ error: null }) - this.submitting = true - - try { - await tryUnlockMetamask(password) - this.submitting = false - history.push(DEFAULT_ROUTE) - } catch ({ message }) { - this.setState({ error: message }) - this.submitting = false - } - } - - handleInputChange ({ target }) { - this.setState({ password: target.value, error: null }) - - // tell mascot to look at page action - const element = target - const boundingRect = element.getBoundingClientRect() - const coordinates = getCaretCoordinates(element, element.selectionEnd) - this.animationEventEmitter.emit('point', { - x: boundingRect.left + coordinates.left - element.scrollLeft, - y: boundingRect.top + coordinates.top - element.scrollTop, - }) - } - - renderSubmitButton () { - const style = { - backgroundColor: '#f7861c', - color: 'white', - marginTop: '20px', - height: '60px', - fontWeight: '400', - boxShadow: 'none', - borderRadius: '4px', - } - - return ( - <Button - type="submit" - style={style} - disabled={!this.state.password} - fullWidth - variant="raised" - size="large" - onClick={event => this.handleSubmit(event)} - disableRipple - > - { this.context.t('login') } - </Button> - ) - } - - render () { - const { password, error } = this.state - const { t } = this.context - const { markPasswordForgotten, history } = this.props - - return ( - <div className="unlock-page__container"> - <div className="unlock-page"> - <div className="unlock-page__mascot-container"> - <Mascot - animationEventEmitter={this.animationEventEmitter} - width="120" - height="120" - /> - </div> - <h1 className="unlock-page__title"> - { t('welcomeBack') } - </h1> - <div>{ t('unlockMessage') }</div> - <form - className="unlock-page__form" - onSubmit={event => this.handleSubmit(event)} - > - <TextField - id="password" - label={t('password')} - type="password" - value={password} - onChange={event => this.handleInputChange(event)} - error={error} - autoFocus - autoComplete="current-password" - material - fullWidth - /> - </form> - { this.renderSubmitButton() } - <div className="unlock-page__links"> - <div - className="unlock-page__link" - onClick={() => { - markPasswordForgotten() - history.push(RESTORE_VAULT_ROUTE) - - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser() - } - }} - > - { t('restoreFromSeed') } - </div> - <div - className="unlock-page__link unlock-page__link--import" - onClick={() => { - markPasswordForgotten() - history.push(RESTORE_VAULT_ROUTE) - - if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { - global.platform.openExtensionInBrowser() - } - }} - > - { t('importUsingSeed') } - </div> - </div> - </div> - </div> - ) - } -} diff --git a/ui/app/components/pages/unlock-page/unlock-page.container.js b/ui/app/components/pages/unlock-page/unlock-page.container.js deleted file mode 100644 index 18fed9b2e..000000000 --- a/ui/app/components/pages/unlock-page/unlock-page.container.js +++ /dev/null @@ -1,31 +0,0 @@ -import { connect } from 'react-redux' -import { withRouter } from 'react-router-dom' -import { compose } from 'recompose' - -const { - tryUnlockMetamask, - forgotPassword, - markPasswordForgotten, -} = require('../../../actions') - -import UnlockPage from './unlock-page.component' - -const mapStateToProps = state => { - const { metamask: { isUnlocked } } = state - return { - isUnlocked, - } -} - -const mapDispatchToProps = dispatch => { - return { - forgotPassword: () => dispatch(forgotPassword()), - tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)), - markPasswordForgotten: () => dispatch(markPasswordForgotten()), - } -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps) -)(UnlockPage) diff --git a/ui/app/components/send/account-list-item/account-list-item.component.js b/ui/app/components/send/account-list-item/account-list-item.component.js deleted file mode 100644 index 14bb7471f..000000000 --- a/ui/app/components/send/account-list-item/account-list-item.component.js +++ /dev/null @@ -1,76 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import { checksumAddress } from '../../../util' -import Identicon from '../../identicon' -import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' -import { PRIMARY, SECONDARY } from '../../../constants/common' - -export default class AccountListItem extends Component { - - static propTypes = { - account: PropTypes.object, - className: PropTypes.string, - conversionRate: PropTypes.number, - currentCurrency: PropTypes.string, - displayAddress: PropTypes.bool, - displayBalance: PropTypes.bool, - handleClick: PropTypes.func, - icon: PropTypes.node, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - render () { - const { - account, - className, - displayAddress = false, - displayBalance = true, - handleClick, - icon = null, - } = this.props - - const { name, address, balance } = account || {} - - return (<div - className={`account-list-item ${className}`} - onClick={() => handleClick({ name, address, balance })} - > - - <div className="account-list-item__top-row"> - <Identicon - address={address} - className="account-list-item__identicon" - diameter={18} - /> - - <div className="account-list-item__account-name">{ name || address }</div> - - {icon && <div className="account-list-item__icon">{ icon }</div>} - - </div> - - {displayAddress && name && <div className="account-list-item__account-address"> - { checksumAddress(address) } - </div>} - - { - displayBalance && ( - <div className="account-list-item__account-balances"> - <UserPreferencedCurrencyDisplay - type={PRIMARY} - value={balance} - /> - <UserPreferencedCurrencyDisplay - type={SECONDARY} - value={balance} - /> - </div> - ) - } - - </div>) - } -} diff --git a/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js deleted file mode 100644 index 7c2f5fcb2..000000000 --- a/ui/app/components/send/account-list-item/tests/account-list-item-container.test.js +++ /dev/null @@ -1,34 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' - -let mapStateToProps - -proxyquire('../account-list-item.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - return () => ({}) - }, - }, - '../send.selectors.js': { - getConversionRate: (s) => `mockConversionRate:${s}`, - getCurrentCurrency: (s) => `mockCurrentCurrency:${s}`, - getNativeCurrency: (s) => `mockNativeCurrency:${s}`, - }, -}) - -describe('account-list-item container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - conversionRate: 'mockConversionRate:mockState', - currentCurrency: 'mockCurrentCurrency:mockState', - nativeCurrency: 'mockNativeCurrency:mockState', - }) - }) - - }) - -}) diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js deleted file mode 100644 index b490a7fd7..000000000 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils.js +++ /dev/null @@ -1,22 +0,0 @@ -const { - multiplyCurrencies, - subtractCurrencies, -} = require('../../../../../conversion-util') -const ethUtil = require('ethereumjs-util') - -function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) { - const { decimals } = selectedToken || {} - const multiplier = Math.pow(10, Number(decimals || 0)) - - return selectedToken - ? multiplyCurrencies(tokenBalance, multiplier, {toNumericBase: 'hex'}) - : subtractCurrencies( - ethUtil.addHexPrefix(balance), - ethUtil.addHexPrefix(gasTotal), - { toNumericBase: 'hex' } - ) -} - -module.exports = { - calcMaxAmount, -} diff --git a/ui/app/components/send/send-content/send-content-README.md b/ui/app/components/send/send-content/send-content-README.md deleted file mode 100644 index e69de29bb..000000000 --- a/ui/app/components/send/send-content/send-content-README.md +++ /dev/null diff --git a/ui/app/components/send/send-content/send-content.scss b/ui/app/components/send/send-content/send-content.scss deleted file mode 100644 index e69de29bb..000000000 --- a/ui/app/components/send/send-content/send-content.scss +++ /dev/null diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md deleted file mode 100644 index e69de29bb..000000000 --- a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown-README.md +++ /dev/null diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js deleted file mode 100644 index 4f43a9d61..000000000 --- a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.component.js +++ /dev/null @@ -1,46 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import AccountListItem from '../../../account-list-item/' -import SendDropdownList from '../../send-dropdown-list/' - -export default class FromDropdown extends Component { - - static propTypes = { - accounts: PropTypes.array, - closeDropdown: PropTypes.func, - dropdownOpen: PropTypes.bool, - onSelect: PropTypes.func, - openDropdown: PropTypes.func, - selectedAccount: PropTypes.object, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - render () { - const { - accounts, - closeDropdown, - dropdownOpen, - openDropdown, - selectedAccount, - onSelect, - } = this.props - - return <div className="send-v2__from-dropdown"> - <AccountListItem - account={selectedAccount} - handleClick={openDropdown} - icon={<i className={`fa fa-caret-down fa-lg`} style={ { color: '#dedede' } }/>} - /> - {dropdownOpen && <SendDropdownList - accounts={accounts} - closeDropdown={closeDropdown} - onSelect={onSelect} - activeAddress={selectedAccount.address} - />} - </div> - } - -} diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss b/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss deleted file mode 100644 index e69de29bb..000000000 --- a/ui/app/components/send/send-content/send-from-row/from-dropdown/from-dropdown.scss +++ /dev/null diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js deleted file mode 100644 index 2314ef4e3..000000000 --- a/ui/app/components/send/send-content/send-from-row/from-dropdown/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './from-dropdown.component' diff --git a/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js b/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js deleted file mode 100644 index 84fcb281e..000000000 --- a/ui/app/components/send/send-content/send-from-row/from-dropdown/tests/from-dropdown-component.test.js +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -import FromDropdown from '../from-dropdown.component.js' - -import AccountListItem from '../../../../account-list-item/account-list-item.container' -import SendDropdownList from '../../../send-dropdown-list/send-dropdown-list.component' - -const propsMethodSpies = { - closeDropdown: sinon.spy(), - openDropdown: sinon.spy(), - onSelect: sinon.spy(), -} - -describe('FromDropdown Component', function () { - let wrapper - - beforeEach(() => { - wrapper = shallow(<FromDropdown - accounts={['mockAccount']} - closeDropdown={propsMethodSpies.closeDropdown} - dropdownOpen={false} - onSelect={propsMethodSpies.onSelect} - openDropdown={propsMethodSpies.openDropdown} - selectedAccount={ { address: 'mockAddress' } } - />, { context: { t: str => str + '_t' } }) - }) - - afterEach(() => { - propsMethodSpies.closeDropdown.resetHistory() - propsMethodSpies.openDropdown.resetHistory() - propsMethodSpies.onSelect.resetHistory() - }) - - describe('render', () => { - it('should render a div with a .send-v2__from-dropdown class', () => { - assert.equal(wrapper.find('.send-v2__from-dropdown').length, 1) - }) - - it('should render an AccountListItem as the first child of the .send-v2__from-dropdown div', () => { - assert(wrapper.find('.send-v2__from-dropdown').childAt(0).is(AccountListItem)) - }) - - it('should pass the correct props to AccountListItem', () => { - const { - account, - handleClick, - icon, - } = wrapper.find('.send-v2__from-dropdown').childAt(0).props() - assert.deepEqual(account, { address: 'mockAddress' }) - assert.deepEqual( - icon, - <i className={`fa fa-caret-down fa-lg`} style={ { color: '#dedede' } }/> - ) - assert.equal(propsMethodSpies.openDropdown.callCount, 0) - handleClick() - assert.equal(propsMethodSpies.openDropdown.callCount, 1) - }) - - it('should not render a SendDropdownList when dropdownOpen is false', () => { - assert.equal(wrapper.find(SendDropdownList).length, 0) - }) - - it('should render a SendDropdownList when dropdownOpen is true', () => { - wrapper.setProps({ dropdownOpen: true }) - assert(wrapper.find(SendDropdownList).length, 1) - }) - - it('should pass the correct props to the SendDropdownList]', () => { - wrapper.setProps({ dropdownOpen: true }) - const { - accounts, - closeDropdown, - onSelect, - activeAddress, - } = wrapper.find(SendDropdownList).props() - assert.deepEqual(accounts, ['mockAccount']) - assert.equal(activeAddress, 'mockAddress') - assert.equal(propsMethodSpies.closeDropdown.callCount, 0) - closeDropdown() - assert.equal(propsMethodSpies.closeDropdown.callCount, 1) - assert.equal(propsMethodSpies.onSelect.callCount, 0) - onSelect() - assert.equal(propsMethodSpies.onSelect.callCount, 1) - }) - }) -}) diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row-README.md b/ui/app/components/send/send-content/send-from-row/send-from-row-README.md deleted file mode 100644 index e69de29bb..000000000 --- a/ui/app/components/send/send-content/send-from-row/send-from-row-README.md +++ /dev/null diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row.component.js b/ui/app/components/send/send-content/send-from-row/send-from-row.component.js deleted file mode 100644 index 3e0e0de22..000000000 --- a/ui/app/components/send/send-content/send-from-row/send-from-row.component.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import SendRowWrapper from '../send-row-wrapper/' -import FromDropdown from './from-dropdown/' - -export default class SendFromRow extends Component { - - static propTypes = { - closeFromDropdown: PropTypes.func, - conversionRate: PropTypes.number, - from: PropTypes.object, - fromAccounts: PropTypes.array, - fromDropdownOpen: PropTypes.bool, - openFromDropdown: PropTypes.func, - tokenContract: PropTypes.object, - updateSendFrom: PropTypes.func, - setSendTokenBalance: PropTypes.func, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - async handleFromChange (newFrom) { - const { - updateSendFrom, - tokenContract, - setSendTokenBalance, - } = this.props - - if (tokenContract) { - const usersToken = await tokenContract.balanceOf(newFrom.address) - setSendTokenBalance(usersToken) - } - updateSendFrom(newFrom) - } - - render () { - const { - closeFromDropdown, - conversionRate, - from, - fromAccounts, - fromDropdownOpen, - openFromDropdown, - } = this.props - - return ( - <SendRowWrapper label={`${this.context.t('from')}:`}> - <FromDropdown - accounts={fromAccounts} - closeDropdown={() => closeFromDropdown()} - conversionRate={conversionRate} - dropdownOpen={fromDropdownOpen} - onSelect={newFrom => this.handleFromChange(newFrom)} - openDropdown={() => openFromDropdown()} - selectedAccount={from} - /> - </SendRowWrapper> - ) - } - -} diff --git a/ui/app/components/send/send-content/send-from-row/send-from-row.container.js b/ui/app/components/send/send-content/send-from-row/send-from-row.container.js deleted file mode 100644 index 33cb63b43..000000000 --- a/ui/app/components/send/send-content/send-from-row/send-from-row.container.js +++ /dev/null @@ -1,46 +0,0 @@ -import { connect } from 'react-redux' -import { - accountsWithSendEtherInfoSelector, - getConversionRate, - getSelectedTokenContract, - getSendFromObject, -} from '../../send.selectors.js' -import { - getFromDropdownOpen, -} from './send-from-row.selectors.js' -import { calcTokenBalance } from '../../send.utils.js' -import { - updateSendFrom, - setSendTokenBalance, -} from '../../../../actions' -import { - closeFromDropdown, - openFromDropdown, -} from '../../../../ducks/send.duck' -import SendFromRow from './send-from-row.component' - -export default connect(mapStateToProps, mapDispatchToProps)(SendFromRow) - -function mapStateToProps (state) { - return { - conversionRate: getConversionRate(state), - from: getSendFromObject(state), - fromAccounts: accountsWithSendEtherInfoSelector(state), - fromDropdownOpen: getFromDropdownOpen(state), - tokenContract: getSelectedTokenContract(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - closeFromDropdown: () => dispatch(closeFromDropdown()), - openFromDropdown: () => dispatch(openFromDropdown()), - updateSendFrom: newFrom => dispatch(updateSendFrom(newFrom)), - setSendTokenBalance: (usersToken, selectedToken) => { - if (!usersToken) return - - const tokenBalance = calcTokenBalance({ usersToken, selectedToken }) - dispatch(setSendTokenBalance(tokenBalance)) - }, - } -} diff --git a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js deleted file mode 100644 index 9ba8d1739..000000000 --- a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-component.test.js +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import sinon from 'sinon' -import SendFromRow from '../send-from-row.component.js' - -import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' -import FromDropdown from '../from-dropdown/from-dropdown.component' - -const propsMethodSpies = { - closeFromDropdown: sinon.spy(), - openFromDropdown: sinon.spy(), - updateSendFrom: sinon.spy(), - setSendTokenBalance: sinon.spy(), -} - -sinon.spy(SendFromRow.prototype, 'handleFromChange') - -describe('SendFromRow Component', function () { - let wrapper - let instance - - beforeEach(() => { - wrapper = shallow(<SendFromRow - closeFromDropdown={propsMethodSpies.closeFromDropdown} - conversionRate={15} - from={ { address: 'mockAddress' } } - fromAccounts={['mockAccount']} - fromDropdownOpen={false} - openFromDropdown={propsMethodSpies.openFromDropdown} - setSendTokenBalance={propsMethodSpies.setSendTokenBalance} - tokenContract={null} - updateSendFrom={propsMethodSpies.updateSendFrom} - />, { context: { t: str => str + '_t' } }) - instance = wrapper.instance() - }) - - afterEach(() => { - propsMethodSpies.closeFromDropdown.resetHistory() - propsMethodSpies.openFromDropdown.resetHistory() - propsMethodSpies.updateSendFrom.resetHistory() - propsMethodSpies.setSendTokenBalance.resetHistory() - SendFromRow.prototype.handleFromChange.resetHistory() - }) - - describe('handleFromChange', () => { - - it('should call updateSendFrom', () => { - assert.equal(propsMethodSpies.updateSendFrom.callCount, 0) - instance.handleFromChange('mockFrom') - assert.equal(propsMethodSpies.updateSendFrom.callCount, 1) - assert.deepEqual( - propsMethodSpies.updateSendFrom.getCall(0).args, - ['mockFrom'] - ) - }) - - it('should call tokenContract.balanceOf and setSendTokenBalance if tokenContract is defined', async () => { - wrapper.setProps({ - tokenContract: { - balanceOf: () => new Promise((resolve) => resolve('mockUsersToken')), - }, - }) - assert.equal(propsMethodSpies.setSendTokenBalance.callCount, 0) - await instance.handleFromChange('mockFrom') - assert.equal(propsMethodSpies.setSendTokenBalance.callCount, 1) - assert.deepEqual( - propsMethodSpies.setSendTokenBalance.getCall(0).args, - ['mockUsersToken'] - ) - }) - - }) - - describe('render', () => { - it('should render a SendRowWrapper component', () => { - assert.equal(wrapper.find(SendRowWrapper).length, 1) - }) - - it('should pass the correct props to SendRowWrapper', () => { - const { - label, - } = wrapper.find(SendRowWrapper).props() - - assert.equal(label, 'from_t:') - }) - - it('should render an FromDropdown as a child of the SendRowWrapper', () => { - assert(wrapper.find(SendRowWrapper).childAt(0).is(FromDropdown)) - }) - - it('should render the FromDropdown with the correct props', () => { - const { - accounts, - closeDropdown, - conversionRate, - dropdownOpen, - onSelect, - openDropdown, - selectedAccount, - } = wrapper.find(SendRowWrapper).childAt(0).props() - assert.deepEqual(accounts, ['mockAccount']) - assert.equal(dropdownOpen, false) - assert.equal(conversionRate, 15) - assert.deepEqual(selectedAccount, { address: 'mockAddress' }) - assert.equal(propsMethodSpies.closeFromDropdown.callCount, 0) - closeDropdown() - assert.equal(propsMethodSpies.closeFromDropdown.callCount, 1) - assert.equal(propsMethodSpies.openFromDropdown.callCount, 0) - openDropdown() - assert.equal(propsMethodSpies.openFromDropdown.callCount, 1) - assert.equal(SendFromRow.prototype.handleFromChange.callCount, 0) - onSelect('mockNewFrom') - assert.equal(SendFromRow.prototype.handleFromChange.callCount, 1) - assert.deepEqual( - SendFromRow.prototype.handleFromChange.getCall(0).args, - ['mockNewFrom'] - ) - }) - }) -}) diff --git a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js b/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js deleted file mode 100644 index e080b2fe3..000000000 --- a/ui/app/components/send/send-content/send-from-row/tests/send-from-row-container.test.js +++ /dev/null @@ -1,110 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -let mapStateToProps -let mapDispatchToProps - -const actionSpies = { - updateSendFrom: sinon.spy(), - setSendTokenBalance: sinon.spy(), -} -const duckActionSpies = { - closeFromDropdown: sinon.spy(), - openFromDropdown: sinon.spy(), -} - -proxyquire('../send-from-row.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - }, - '../../send.selectors.js': { - accountsWithSendEtherInfoSelector: (s) => `mockFromAccounts:${s}`, - getConversionRate: (s) => `mockConversionRate:${s}`, - getSelectedTokenContract: (s) => `mockTokenContract:${s}`, - getSendFromObject: (s) => `mockFrom:${s}`, - }, - './send-from-row.selectors.js': { getFromDropdownOpen: (s) => `mockFromDropdownOpen:${s}` }, - '../../send.utils.js': { calcTokenBalance: ({ usersToken, selectedToken }) => usersToken + selectedToken }, - '../../../../actions': actionSpies, - '../../../../ducks/send.duck': duckActionSpies, -}) - -describe('send-from-row container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - conversionRate: 'mockConversionRate:mockState', - from: 'mockFrom:mockState', - fromAccounts: 'mockFromAccounts:mockState', - fromDropdownOpen: 'mockFromDropdownOpen:mockState', - tokenContract: 'mockTokenContract:mockState', - }) - }) - - }) - - describe('mapDispatchToProps()', () => { - let dispatchSpy - let mapDispatchToPropsObject - - beforeEach(() => { - dispatchSpy = sinon.spy() - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) - }) - - describe('closeFromDropdown()', () => { - it('should dispatch a closeFromDropdown action', () => { - mapDispatchToPropsObject.closeFromDropdown() - assert(dispatchSpy.calledOnce) - assert(duckActionSpies.closeFromDropdown.calledOnce) - assert.equal( - duckActionSpies.closeFromDropdown.getCall(0).args[0], - undefined - ) - }) - }) - - describe('openFromDropdown()', () => { - it('should dispatch a openFromDropdown action', () => { - mapDispatchToPropsObject.openFromDropdown() - assert(dispatchSpy.calledOnce) - assert(duckActionSpies.openFromDropdown.calledOnce) - assert.equal( - duckActionSpies.openFromDropdown.getCall(0).args[0], - undefined - ) - }) - }) - - describe('updateSendFrom()', () => { - it('should dispatch an updateSendFrom action', () => { - mapDispatchToPropsObject.updateSendFrom('mockFrom') - assert(dispatchSpy.calledOnce) - assert.equal( - actionSpies.updateSendFrom.getCall(0).args[0], - 'mockFrom' - ) - }) - }) - - describe('setSendTokenBalance()', () => { - it('should dispatch an setSendTokenBalance action', () => { - mapDispatchToPropsObject.setSendTokenBalance('mockUsersToken', 'mockSelectedToken') - assert(dispatchSpy.calledOnce) - assert.equal( - actionSpies.setSendTokenBalance.getCall(0).args[0], - 'mockUsersTokenmockSelectedToken' - ) - }) - }) - - }) - -}) diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js deleted file mode 100644 index 91b58cfd0..000000000 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.component.js +++ /dev/null @@ -1,48 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import SendRowWrapper from '../send-row-wrapper/' -import GasFeeDisplay from './gas-fee-display/gas-fee-display.component' - -export default class SendGasRow extends Component { - - static propTypes = { - conversionRate: PropTypes.number, - convertedCurrency: PropTypes.string, - gasFeeError: PropTypes.bool, - gasLoadingError: PropTypes.bool, - gasTotal: PropTypes.string, - showCustomizeGasModal: PropTypes.func, - }; - - static contextTypes = { - t: PropTypes.func, - }; - - render () { - const { - conversionRate, - convertedCurrency, - gasLoadingError, - gasTotal, - gasFeeError, - showCustomizeGasModal, - } = this.props - - return ( - <SendRowWrapper - label={`${this.context.t('gasFee')}:`} - showError={gasFeeError} - errorType={'gasFee'} - > - <GasFeeDisplay - conversionRate={conversionRate} - convertedCurrency={convertedCurrency} - gasLoadingError={gasLoadingError} - gasTotal={gasTotal} - onClick={() => showCustomizeGasModal()} - /> - </SendRowWrapper> - ) - } - -} diff --git a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js deleted file mode 100644 index 8f8e3e4dd..000000000 --- a/ui/app/components/send/send-content/send-gas-row/send-gas-row.container.js +++ /dev/null @@ -1,27 +0,0 @@ -import { connect } from 'react-redux' -import { - getConversionRate, - getCurrentCurrency, - getGasTotal, -} from '../../send.selectors.js' -import { getGasLoadingError, gasFeeIsInError } from './send-gas-row.selectors.js' -import { showModal } from '../../../../actions' -import SendGasRow from './send-gas-row.component' - -export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow) - -function mapStateToProps (state) { - return { - conversionRate: getConversionRate(state), - convertedCurrency: getCurrentCurrency(state), - gasTotal: getGasTotal(state), - gasFeeError: gasFeeIsInError(state), - gasLoadingError: getGasLoadingError(state), - } -} - -function mapDispatchToProps (dispatch) { - return { - showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS' })), - } -} diff --git a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js deleted file mode 100644 index 2ce062505..000000000 --- a/ui/app/components/send/send-content/send-gas-row/tests/send-gas-row-container.test.js +++ /dev/null @@ -1,70 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -let mapStateToProps -let mapDispatchToProps - -const actionSpies = { - showModal: sinon.spy(), -} - -proxyquire('../send-gas-row.container.js', { - 'react-redux': { - connect: (ms, md) => { - mapStateToProps = ms - mapDispatchToProps = md - return () => ({}) - }, - }, - '../../send.selectors.js': { - getConversionRate: (s) => `mockConversionRate:${s}`, - getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, - getGasTotal: (s) => `mockGasTotal:${s}`, - }, - './send-gas-row.selectors.js': { - getGasLoadingError: (s) => `mockGasLoadingError:${s}`, - gasFeeIsInError: (s) => `mockGasFeeError:${s}`, - }, - '../../../../actions': actionSpies, -}) - -describe('send-gas-row container', () => { - - describe('mapStateToProps()', () => { - - it('should map the correct properties to props', () => { - assert.deepEqual(mapStateToProps('mockState'), { - conversionRate: 'mockConversionRate:mockState', - convertedCurrency: 'mockConvertedCurrency:mockState', - gasTotal: 'mockGasTotal:mockState', - gasFeeError: 'mockGasFeeError:mockState', - gasLoadingError: 'mockGasLoadingError:mockState', - }) - }) - - }) - - describe('mapDispatchToProps()', () => { - let dispatchSpy - let mapDispatchToPropsObject - - beforeEach(() => { - dispatchSpy = sinon.spy() - mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy) - }) - - describe('showCustomizeGasModal()', () => { - it('should dispatch an action', () => { - mapDispatchToPropsObject.showCustomizeGasModal() - assert(dispatchSpy.calledOnce) - assert.deepEqual( - actionSpies.showModal.getCall(0).args[0], - { name: 'CUSTOMIZE_GAS' } - ) - }) - }) - - }) - -}) diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js deleted file mode 100644 index 0eeaa3a11..000000000 --- a/ui/app/components/send/send-content/send-to-row/send-to-row.utils.js +++ /dev/null @@ -1,21 +0,0 @@ -const { - REQUIRED_ERROR, - INVALID_RECIPIENT_ADDRESS_ERROR, -} = require('../../send.constants') -const { isValidAddress } = require('../../../../util') - -function getToErrorObject (to, toError = null, hasHexData = false) { - if (!to) { - if (!hasHexData) { - toError = REQUIRED_ERROR - } - } else if (!isValidAddress(to) && !toError) { - toError = INVALID_RECIPIENT_ADDRESS_ERROR - } - - return { to: toError } -} - -module.exports = { - getToErrorObject, -} diff --git a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js deleted file mode 100644 index c779aeb76..000000000 --- a/ui/app/components/send/send-content/send-to-row/tests/send-to-row-utils.test.js +++ /dev/null @@ -1,57 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' -import sinon from 'sinon' - -import { - REQUIRED_ERROR, - INVALID_RECIPIENT_ADDRESS_ERROR, -} from '../../../send.constants' - -const stubs = { - isValidAddress: sinon.stub().callsFake(to => Boolean(to.match(/^[0xabcdef123456798]+$/))), -} - -const toRowUtils = proxyquire('../send-to-row.utils.js', { - '../../../../util': { - isValidAddress: stubs.isValidAddress, - }, -}) -const { - getToErrorObject, -} = toRowUtils - -describe('send-to-row utils', () => { - - describe('getToErrorObject()', () => { - it('should return a required error if to is falsy', () => { - assert.deepEqual(getToErrorObject(null), { - to: REQUIRED_ERROR, - }) - }) - - it('should return null if to is falsy and hexData is truthy', () => { - assert.deepEqual(getToErrorObject(null, undefined, true), { - to: null, - }) - }) - - it('should return an invalid recipient error if to is truthy but invalid', () => { - assert.deepEqual(getToErrorObject('mockInvalidTo'), { - to: INVALID_RECIPIENT_ADDRESS_ERROR, - }) - }) - - it('should return null if to is truthy and valid', () => { - assert.deepEqual(getToErrorObject('0xabc123'), { - to: null, - }) - }) - - it('should return the passed error if to is truthy but invalid if to is truthy and valid', () => { - assert.deepEqual(getToErrorObject('invalid #$ 345878', 'someExplicitError'), { - to: 'someExplicitError', - }) - }) - }) - -}) diff --git a/ui/app/components/sidebars/sidebar.component.js b/ui/app/components/sidebars/sidebar.component.js deleted file mode 100644 index 57cdd7111..000000000 --- a/ui/app/components/sidebars/sidebar.component.js +++ /dev/null @@ -1,49 +0,0 @@ -import React, { Component } from 'react' -import PropTypes from 'prop-types' -import ReactCSSTransitionGroup from 'react-addons-css-transition-group' -import WalletView from '../wallet-view' -import { WALLET_VIEW_SIDEBAR } from './sidebar.constants' - -export default class Sidebar extends Component { - - static propTypes = { - sidebarOpen: PropTypes.bool, - hideSidebar: PropTypes.func, - transitionName: PropTypes.string, - type: PropTypes.string, - }; - - renderOverlay () { - return <div className="sidebar-overlay" onClick={() => this.props.hideSidebar()} /> - } - - renderSidebarContent () { - const { type } = this.props - - switch (type) { - case WALLET_VIEW_SIDEBAR: - return <WalletView responsiveDisplayClassname={'sidebar-right' } /> - default: - return null - } - - } - - render () { - const { transitionName, sidebarOpen } = this.props - - return ( - <div> - <ReactCSSTransitionGroup - transitionName={transitionName} - transitionEnterTimeout={300} - transitionLeaveTimeout={200} - > - { sidebarOpen ? this.renderSidebarContent() : null } - </ReactCSSTransitionGroup> - { sidebarOpen ? this.renderOverlay() : null } - </div> - ) - } - -} diff --git a/ui/app/components/tab-bar.js b/ui/app/components/tab-bar.js deleted file mode 100644 index 0016a09c1..000000000 --- a/ui/app/components/tab-bar.js +++ /dev/null @@ -1,33 +0,0 @@ -const { Component } = require('react') -const h = require('react-hyperscript') -const PropTypes = require('prop-types') -const classnames = require('classnames') - -class TabBar extends Component { - render () { - const { tabs = [], onSelect, isActive } = this.props - - return ( - h('.tab-bar', {}, [ - tabs.map(({ key, content }) => { - return h('div', { - className: classnames('tab-bar__tab pointer', { - 'tab-bar__tab--active': isActive(key, content), - }), - onClick: () => onSelect(key), - key, - }, content) - }), - h('div.tab-bar__tab.tab-bar__grow-tab'), - ]) - ) - } -} - -TabBar.propTypes = { - isActive: PropTypes.func.isRequired, - tabs: PropTypes.array, - onSelect: PropTypes.func, -} - -module.exports = TabBar diff --git a/ui/app/components/token-input/tests/token-input.container.test.js b/ui/app/components/token-input/tests/token-input.container.test.js deleted file mode 100644 index d73bc9a94..000000000 --- a/ui/app/components/token-input/tests/token-input.container.test.js +++ /dev/null @@ -1,129 +0,0 @@ -import assert from 'assert' -import proxyquire from 'proxyquire' - -let mapStateToProps, mergeProps - -proxyquire('../token-input.container.js', { - 'react-redux': { - connect: (ms, md, mp) => { - mapStateToProps = ms - mergeProps = mp - return () => ({}) - }, - }, -}) - -describe('TokenInput container', () => { - describe('mapStateToProps()', () => { - it('should return the correct props when send is empty', () => { - const mockState = { - metamask: { - currentCurrency: 'usd', - tokens: [ - { - address: '0x1', - decimals: '4', - symbol: 'ABC', - }, - ], - selectedTokenAddress: '0x1', - contractExchangeRates: {}, - send: {}, - }, - } - - assert.deepEqual(mapStateToProps(mockState), { - currentCurrency: 'usd', - selectedToken: { - address: '0x1', - decimals: '4', - symbol: 'ABC', - }, - selectedTokenExchangeRate: 0, - }) - }) - - it('should return the correct props when selectedTokenAddress is not found and send is populated', () => { - const mockState = { - metamask: { - currentCurrency: 'usd', - tokens: [ - { - address: '0x1', - decimals: '4', - symbol: 'ABC', - }, - ], - selectedTokenAddress: '0x2', - contractExchangeRates: {}, - send: { - token: { address: 'test' }, - }, - }, - } - - assert.deepEqual(mapStateToProps(mockState), { - currentCurrency: 'usd', - selectedToken: { - address: 'test', - }, - selectedTokenExchangeRate: 0, - }) - }) - - it('should return the correct props when contractExchangeRates is populated', () => { - const mockState = { - metamask: { - currentCurrency: 'usd', - tokens: [ - { - address: '0x1', - decimals: '4', - symbol: 'ABC', - }, - ], - selectedTokenAddress: '0x1', - contractExchangeRates: { - '0x1': 5, - }, - send: {}, - }, - } - - assert.deepEqual(mapStateToProps(mockState), { - currentCurrency: 'usd', - selectedToken: { - address: '0x1', - decimals: '4', - symbol: 'ABC', - }, - selectedTokenExchangeRate: 5, - }) - }) - }) - - describe('mergeProps()', () => { - it('should return the correct props', () => { - const mockStateProps = { - currentCurrency: 'usd', - selectedToken: { - address: '0x1', - decimals: '4', - symbol: 'ABC', - }, - selectedTokenExchangeRate: 5, - } - - assert.deepEqual(mergeProps(mockStateProps, {}, {}), { - currentCurrency: 'usd', - selectedToken: { - address: '0x1', - decimals: '4', - symbol: 'ABC', - }, - selectedTokenExchangeRate: 5, - suffix: 'ABC', - }) - }) - }) -}) diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js deleted file mode 100644 index 8687dbbc7..000000000 --- a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.component.test.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react' -import assert from 'assert' -import { shallow } from 'enzyme' -import TransactionActivityLog from '../transaction-activity-log.component' -import Card from '../../card' - -describe('TransactionActivityLog Component', () => { - it('should render properly', () => { - const transaction = { - history: [], - id: 1, - status: 'confirmed', - txParams: { - from: '0x1', - gas: '0x5208', - gasPrice: '0x3b9aca00', - nonce: '0xa4', - to: '0x2', - value: '0x2386f26fc10000', - }, - } - - const wrapper = shallow( - <TransactionActivityLog - transaction={transaction} - className="test-class" - />, - { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } } - ) - - assert.ok(wrapper.hasClass('transaction-activity-log')) - assert.ok(wrapper.hasClass('test-class')) - assert.equal(wrapper.find(Card).length, 1) - }) -}) diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js deleted file mode 100644 index 586500408..000000000 --- a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.util.test.js +++ /dev/null @@ -1,208 +0,0 @@ -import assert from 'assert' -import { getActivities } from '../transaction-activity-log.util' - -describe('getActivities', () => { - it('should return no activities for an empty history', () => { - const transaction = { - history: [], - id: 1, - status: 'confirmed', - txParams: { - from: '0x1', - gas: '0x5208', - gasPrice: '0x3b9aca00', - nonce: '0xa4', - to: '0x2', - value: '0x2386f26fc10000', - }, - } - - assert.deepEqual(getActivities(transaction), []) - }) - - it('should return activities for a transaction\'s history', () => { - const transaction = { - history: [ - { - id: 5559712943815343, - loadingDefaults: true, - metamaskNetworkId: '3', - status: 'unapproved', - time: 1535507561452, - txParams: { - from: '0x1', - gas: '0x5208', - gasPrice: '0x3b9aca00', - nonce: '0xa4', - to: '0x2', - value: '0x2386f26fc10000', - }, - }, - [ - { - op: 'replace', - path: '/loadingDefaults', - timestamp: 1535507561515, - value: false, - }, - { - op: 'add', - path: '/gasPriceSpecified', - value: true, - }, - { - op: 'add', - path: '/gasLimitSpecified', - value: true, - }, - { - op: 'add', - path: '/estimatedGas', - value: '0x5208', - }, - ], - [ - { - note: '#newUnapprovedTransaction - adding the origin', - op: 'add', - path: '/origin', - timestamp: 1535507561516, - value: 'MetaMask', - }, - [], - ], - [ - { - note: 'confTx: user approved transaction', - op: 'replace', - path: '/txParams/gasPrice', - timestamp: 1535664571504, - value: '0x77359400', - }, - ], - [ - { - note: 'txStateManager: setting status to approved', - op: 'replace', - path: '/status', - timestamp: 1535507564302, - value: 'approved', - }, - ], - [ - { - note: 'transactions#approveTransaction', - op: 'add', - path: '/txParams/nonce', - timestamp: 1535507564439, - value: '0xa4', - }, - { - op: 'add', - path: '/nonceDetails', - value: { - local: {}, - network: {}, - params: {}, - }, - }, - ], - [ - { - note: 'transactions#publishTransaction', - op: 'replace', - path: '/status', - timestamp: 1535507564518, - value: 'signed', - }, - { - op: 'add', - path: '/rawTx', - value: '0xf86b81a4843b9aca008252089450a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706872386f26fc10000802aa007b30119fc4fc5954fad727895b7e3ba80a78d197e95703cc603bcf017879151a01c50beda40ffaee541da9c05b9616247074f25f392800e0ad6c7a835d5366edf', - }, - ], - [], - [ - { - note: 'transactions#setTxHash', - op: 'add', - path: '/hash', - timestamp: 1535507564658, - value: '0x7acc4987b5c0dfa8d423798a8c561138259de1f98a62e3d52e7e83c0e0dd9fb7', - }, - ], - [ - { - note: 'txStateManager - add submitted time stamp', - op: 'add', - path: '/submittedTime', - timestamp: 1535507564660, - value: 1535507564660, - }, - ], - [ - { - note: 'txStateManager: setting status to submitted', - op: 'replace', - path: '/status', - timestamp: 1535507564665, - value: 'submitted', - }, - ], - [ - { - note: 'transactions/pending-tx-tracker#event: tx:block-update', - op: 'add', - path: '/firstRetryBlockNumber', - timestamp: 1535507575476, - value: '0x3bf624', - }, - ], - [ - { - note: 'txStateManager: setting status to confirmed', - op: 'replace', - path: '/status', - timestamp: 1535507615993, - value: 'confirmed', - }, - ], - ], - id: 1, - status: 'confirmed', - txParams: { - from: '0x1', - gas: '0x5208', - gasPrice: '0x3b9aca00', - nonce: '0xa4', - to: '0x2', - value: '0x2386f26fc10000', - }, - } - - const expectedResult = [ - { - 'eventKey': 'transactionCreated', - 'timestamp': 1535507561452, - 'value': '0x2386f26fc10000', - }, - { - 'eventKey': 'transactionUpdatedGas', - 'timestamp': 1535664571504, - 'value': '0x77359400', - }, - { - 'eventKey': 'transactionSubmitted', - 'timestamp': 1535507564665, - 'value': undefined, - }, - { - 'eventKey': 'transactionConfirmed', - 'timestamp': 1535507615993, - 'value': undefined, - }, - ] - - assert.deepEqual(getActivities(transaction), expectedResult) - }) -}) diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js deleted file mode 100644 index 58d932a0f..000000000 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js +++ /dev/null @@ -1,96 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import { getActivities } from './transaction-activity-log.util' -import Card from '../card' -import { getEthConversionFromWeiHex, getValueFromWeiHex } from '../../helpers/conversions.util' -import { formatDate } from '../../util' - -export default class TransactionActivityLog extends PureComponent { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - transaction: PropTypes.object, - className: PropTypes.string, - conversionRate: PropTypes.number, - nativeCurrency: PropTypes.string, - } - - state = { - activities: [], - } - - componentDidMount () { - this.setActivites() - } - - componentDidUpdate (prevProps) { - const { - transaction: { history: prevHistory = [], txReceipt: { status: prevStatus } = {} } = {}, - } = prevProps - const { - transaction: { history = [], txReceipt: { status } = {} } = {}, - } = this.props - - if (prevHistory.length !== history.length || prevStatus !== status) { - this.setActivites() - } - } - - setActivites () { - const activities = getActivities(this.props.transaction) - this.setState({ activities }) - } - - renderActivity (activity, index) { - const { conversionRate, nativeCurrency } = this.props - const { eventKey, value, timestamp } = activity - const ethValue = index === 0 - ? `${getValueFromWeiHex({ - value, - fromCurrency: nativeCurrency, - toCurrency: nativeCurrency, - conversionRate, - numberOfDecimals: 6, - })} ${nativeCurrency}` - : getEthConversionFromWeiHex({ value, fromCurrency: nativeCurrency, conversionRate }) - const formattedTimestamp = formatDate(timestamp) - const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp]) - - return ( - <div - key={index} - className="transaction-activity-log__activity" - > - <div className="transaction-activity-log__activity-icon" /> - <div - className="transaction-activity-log__activity-text" - title={activityText} - > - { activityText } - </div> - </div> - ) - } - - render () { - const { t } = this.context - const { className } = this.props - const { activities } = this.state - - return ( - <div className={classnames('transaction-activity-log', className)}> - <Card - title={t('activityLog')} - className="transaction-activity-log__card" - > - <div className="transaction-activity-log__activities-container"> - { activities.map((activity, index) => this.renderActivity(activity, index)) } - </div> - </Card> - </div> - ) - } -} diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.container.js b/ui/app/components/transaction-activity-log/transaction-activity-log.container.js deleted file mode 100644 index 622f77df1..000000000 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.container.js +++ /dev/null @@ -1,12 +0,0 @@ -import { connect } from 'react-redux' -import TransactionActivityLog from './transaction-activity-log.component' -import { conversionRateSelector, getNativeCurrency } from '../../selectors' - -const mapStateToProps = state => { - return { - conversionRate: conversionRateSelector(state), - nativeCurrency: getNativeCurrency(state), - } -} - -export default connect(mapStateToProps)(TransactionActivityLog) diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js deleted file mode 100644 index 16597ae1a..000000000 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js +++ /dev/null @@ -1,93 +0,0 @@ -// path constants -const STATUS_PATH = '/status' -const GAS_PRICE_PATH = '/txParams/gasPrice' - -// status constants -const UNAPPROVED_STATUS = 'unapproved' -const SUBMITTED_STATUS = 'submitted' -const CONFIRMED_STATUS = 'confirmed' -const DROPPED_STATUS = 'dropped' - -// op constants -const REPLACE_OP = 'replace' - -// event constants -const TRANSACTION_CREATED_EVENT = 'transactionCreated' -const TRANSACTION_UPDATED_GAS_EVENT = 'transactionUpdatedGas' -const TRANSACTION_SUBMITTED_EVENT = 'transactionSubmitted' -const TRANSACTION_CONFIRMED_EVENT = 'transactionConfirmed' -const TRANSACTION_DROPPED_EVENT = 'transactionDropped' -const TRANSACTION_UPDATED_EVENT = 'transactionUpdated' -const TRANSACTION_ERRORED_EVENT = 'transactionErrored' - -const eventPathsHash = { - [STATUS_PATH]: true, - [GAS_PRICE_PATH]: true, -} - -const statusHash = { - [SUBMITTED_STATUS]: TRANSACTION_SUBMITTED_EVENT, - [CONFIRMED_STATUS]: TRANSACTION_CONFIRMED_EVENT, - [DROPPED_STATUS]: TRANSACTION_DROPPED_EVENT, -} - -function eventCreator (eventKey, timestamp, value) { - return { - eventKey, - timestamp, - value, - } -} - -export function getActivities (transaction) { - const { history = [], txReceipt: { status } = {} } = transaction - - const historyActivities = history.reduce((acc, base) => { - // First history item should be transaction creation - if (!Array.isArray(base) && base.status === UNAPPROVED_STATUS && base.txParams) { - const { time, txParams: { value } = {} } = base - return acc.concat(eventCreator(TRANSACTION_CREATED_EVENT, time, value)) - // An entry in the history may be an array of more sub-entries. - } else if (Array.isArray(base)) { - const events = [] - - base.forEach(entry => { - const { op, path, value, timestamp: entryTimestamp } = entry - // Not all sub-entries in a history entry have a timestamp. If the sub-entry does not have a - // timestamp, the first sub-entry in a history entry should. - const timestamp = entryTimestamp || base[0] && base[0].timestamp - - if (path in eventPathsHash && op === REPLACE_OP) { - switch (path) { - case STATUS_PATH: { - if (value in statusHash) { - events.push(eventCreator(statusHash[value], timestamp)) - } - - break - } - - case GAS_PRICE_PATH: { - events.push(eventCreator(TRANSACTION_UPDATED_GAS_EVENT, timestamp, value)) - break - } - - default: { - events.push(eventCreator(TRANSACTION_UPDATED_EVENT, timestamp)) - } - } - } - }) - - return acc.concat(events) - } - - return acc - }, []) - - // If txReceipt.status is '0x0', that means that an on-chain error occured for the transaction, - // so we add an error entry to the Activity Log. - return status === '0x0' - ? historyActivities.concat(eventCreator(TRANSACTION_ERRORED_EVENT)) - : historyActivities -} diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js deleted file mode 100644 index 3a7647873..000000000 --- a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js +++ /dev/null @@ -1,100 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import TransactionBreakdownRow from './transaction-breakdown-row' -import Card from '../card' -import CurrencyDisplay from '../currency-display' -import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' -import HexToDecimal from '../hex-to-decimal' -import { GWEI, PRIMARY, SECONDARY } from '../../constants/common' -import { getHexGasTotal } from '../../helpers/confirm-transaction/util' -import { sumHexes } from '../../helpers/transactions.util' - -export default class TransactionBreakdown extends PureComponent { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - transaction: PropTypes.object, - className: PropTypes.string, - nativeCurrency: PropTypes.string.isRequired, - } - - static defaultProps = { - transaction: {}, - } - - render () { - const { t } = this.context - const { transaction, className, nativeCurrency } = this.props - const { txParams: { gas, gasPrice, value } = {}, txReceipt: { gasUsed } = {} } = transaction - - const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas - - const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice }) - const totalInHex = sumHexes(hexGasTotal, value) - - return ( - <div className={classnames('transaction-breakdown', className)}> - <Card - title={t('transaction')} - className="transaction-breakdown__card" - > - <TransactionBreakdownRow title={t('amount')}> - <UserPreferencedCurrencyDisplay - className="transaction-breakdown__value" - type={PRIMARY} - value={value} - /> - </TransactionBreakdownRow> - <TransactionBreakdownRow - title={`${t('gasLimit')} (${t('units')})`} - className="transaction-breakdown__row-title" - > - <HexToDecimal - className="transaction-breakdown__value" - value={gas} - /> - </TransactionBreakdownRow> - { - typeof gasUsed === 'string' && ( - <TransactionBreakdownRow - title={`${t('gasUsed')} (${t('units')})`} - className="transaction-breakdown__row-title" - > - <HexToDecimal - className="transaction-breakdown__value" - value={gasUsed} - /> - </TransactionBreakdownRow> - ) - } - <TransactionBreakdownRow title={t('gasPrice')}> - <CurrencyDisplay - className="transaction-breakdown__value" - currency={nativeCurrency} - denomination={GWEI} - value={gasPrice} - hideLabel - /> - </TransactionBreakdownRow> - <TransactionBreakdownRow title={t('total')}> - <div> - <UserPreferencedCurrencyDisplay - className="transaction-breakdown__value transaction-breakdown__value--eth-total" - type={PRIMARY} - value={totalInHex} - /> - <UserPreferencedCurrencyDisplay - className="transaction-breakdown__value" - type={SECONDARY} - value={totalInHex} - /> - </div> - </TransactionBreakdownRow> - </Card> - </div> - ) - } -} diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.container.js b/ui/app/components/transaction-breakdown/transaction-breakdown.container.js deleted file mode 100644 index ed2708e03..000000000 --- a/ui/app/components/transaction-breakdown/transaction-breakdown.container.js +++ /dev/null @@ -1,11 +0,0 @@ -import { connect } from 'react-redux' -import TransactionBreakdown from './transaction-breakdown.component' -import { getNativeCurrency } from '../../selectors' - -const mapStateToProps = (state) => { - return { - nativeCurrency: getNativeCurrency(state), - } -} - -export default connect(mapStateToProps)(TransactionBreakdown) diff --git a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js deleted file mode 100644 index a4f28fd63..000000000 --- a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js +++ /dev/null @@ -1,111 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import SenderToRecipient from '../sender-to-recipient' -import { CARDS_VARIANT } from '../sender-to-recipient/sender-to-recipient.constants' -import TransactionActivityLog from '../transaction-activity-log' -import TransactionBreakdown from '../transaction-breakdown' -import Button from '../button' -import Tooltip from '../tooltip' -import prefixForNetwork from '../../../lib/etherscan-prefix-for-network' - -export default class TransactionListItemDetails extends PureComponent { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - onCancel: PropTypes.func, - onRetry: PropTypes.func, - showCancel: PropTypes.bool, - showRetry: PropTypes.bool, - transaction: PropTypes.object, - } - - handleEtherscanClick = () => { - const { hash, metamaskNetworkId } = this.props.transaction - - const prefix = prefixForNetwork(metamaskNetworkId) - const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}` - global.platform.openWindow({ url: etherscanUrl }) - this.setState({ showTransactionDetails: true }) - } - - handleCancel = event => { - const { onCancel } = this.props - - event.stopPropagation() - onCancel() - } - - handleRetry = event => { - const { onRetry } = this.props - - event.stopPropagation() - onRetry() - } - - render () { - const { t } = this.context - const { transaction, showCancel, showRetry } = this.props - const { txParams: { to, from } = {} } = transaction - - return ( - <div className="transaction-list-item-details"> - <div className="transaction-list-item-details__header"> - <div>Details</div> - <div className="transaction-list-item-details__header-buttons"> - { - showRetry && ( - <Button - type="raised" - onClick={this.handleRetry} - className="transaction-list-item-details__header-button" - > - { t('speedUp') } - </Button> - ) - } - { - showCancel && ( - <Button - type="raised" - onClick={this.handleCancel} - className="transaction-list-item-details__header-button" - > - { t('cancel') } - </Button> - ) - } - <Tooltip title={t('viewOnEtherscan')}> - <Button - type="raised" - onClick={this.handleEtherscanClick} - className="transaction-list-item-details__header-button" - > - <img src="/images/arrow-popout.svg" /> - </Button> - </Tooltip> - </div> - </div> - <div className="transaction-list-item-details__sender-to-recipient-container"> - <SenderToRecipient - variant={CARDS_VARIANT} - addressOnly - recipientAddress={to} - senderAddress={from} - /> - </div> - <div className="transaction-list-item-details__cards-container"> - <TransactionBreakdown - transaction={transaction} - className="transaction-list-item-details__transaction-breakdown" - /> - <TransactionActivityLog - transaction={transaction} - className="transaction-list-item-details__transaction-activity-log" - /> - </div> - </div> - ) - } -} diff --git a/ui/app/components/transaction-list-item/transaction-list-item.container.js b/ui/app/components/transaction-list-item/transaction-list-item.container.js deleted file mode 100644 index 62ed7a73f..000000000 --- a/ui/app/components/transaction-list-item/transaction-list-item.container.js +++ /dev/null @@ -1,38 +0,0 @@ -import { connect } from 'react-redux' -import { withRouter } from 'react-router-dom' -import { compose } from 'recompose' -import withMethodData from '../../higher-order-components/with-method-data' -import TransactionListItem from './transaction-list-item.component' -import { setSelectedToken, retryTransaction, showModal } from '../../actions' -import { hexToDecimal } from '../../helpers/conversions.util' -import { getTokenData } from '../../helpers/transactions.util' -import { formatDate } from '../../util' - -const mapStateToProps = (state, ownProps) => { - const { transaction: { txParams: { value, nonce, data } = {}, time } = {} } = ownProps - - const tokenData = data && getTokenData(data) - const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time) - - return { - value, - nonceAndDate, - tokenData, - } -} - -const mapDispatchToProps = dispatch => { - return { - setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)), - retryTransaction: transactionId => dispatch(retryTransaction(transactionId)), - showCancelModal: (transactionId, originalGasPrice) => { - return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice })) - }, - } -} - -export default compose( - withRouter, - connect(mapStateToProps, mapDispatchToProps), - withMethodData, -)(TransactionListItem) diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js deleted file mode 100644 index 402b0f486..000000000 --- a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js +++ /dev/null @@ -1,98 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import Button from '../button' -import Identicon from '../identicon' -import TokenBalance from '../token-balance' -import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' -import { SEND_ROUTE } from '../../routes' -import { PRIMARY, SECONDARY } from '../../constants/common' - -export default class TransactionViewBalance extends PureComponent { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - showDepositModal: PropTypes.func, - selectedToken: PropTypes.object, - history: PropTypes.object, - network: PropTypes.string, - balance: PropTypes.string, - assetImage: PropTypes.string, - } - - renderBalance () { - const { selectedToken, balance } = this.props - - return selectedToken - ? ( - <TokenBalance - token={selectedToken} - withSymbol - className="transaction-view-balance__token-balance" - /> - ) : ( - <div className="transaction-view-balance__balance"> - <UserPreferencedCurrencyDisplay - className="transaction-view-balance__primary-balance" - value={balance} - type={PRIMARY} - ethNumberOfDecimals={4} - /> - <UserPreferencedCurrencyDisplay - className="transaction-view-balance__secondary-balance" - value={balance} - type={SECONDARY} - ethNumberOfDecimals={4} - /> - </div> - ) - } - - renderButtons () { - const { t } = this.context - const { selectedToken, showDepositModal, history } = this.props - - return ( - <div className="transaction-view-balance__buttons"> - { - !selectedToken && ( - <Button - type="primary" - className="transaction-view-balance__button" - onClick={() => showDepositModal()} - > - { t('deposit') } - </Button> - ) - } - <Button - type="primary" - className="transaction-view-balance__button" - onClick={() => history.push(SEND_ROUTE)} - > - { t('send') } - </Button> - </div> - ) - } - - render () { - const { network, selectedToken, assetImage } = this.props - - return ( - <div className="transaction-view-balance"> - <div className="transaction-view-balance__balance-container"> - <Identicon - diameter={50} - address={selectedToken && selectedToken.address} - network={network} - image={assetImage} - /> - { this.renderBalance() } - </div> - { this.renderButtons() } - </div> - ) - } -} diff --git a/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js b/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js new file mode 100644 index 000000000..8abe1ab18 --- /dev/null +++ b/ui/app/components/ui/account-dropdown-mini/account-dropdown-mini.component.js @@ -0,0 +1,84 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import AccountListItem from '../../app/send/account-list-item/account-list-item.component' + +export default class AccountDropdownMini extends PureComponent { + static propTypes = { + accounts: PropTypes.array.isRequired, + closeDropdown: PropTypes.func, + disabled: PropTypes.bool, + dropdownOpen: PropTypes.bool, + onSelect: PropTypes.func, + openDropdown: PropTypes.func, + selectedAccount: PropTypes.object.isRequired, + } + + static defaultProps = { + closeDropdown: () => {}, + disabled: false, + dropdownOpen: false, + onSelect: () => {}, + openDropdown: () => {}, + } + + getListItemIcon (currentAccount, selectedAccount) { + return currentAccount.address === selectedAccount.address && ( + <i + className="fa fa-check fa-lg" + style={{ color: '#02c9b1' }} + /> + ) + } + + renderDropdown () { + const { accounts, selectedAccount, closeDropdown, onSelect } = this.props + + return ( + <div> + <div + className="account-dropdown-mini__close-area" + onClick={closeDropdown} + /> + <div className="account-dropdown-mini__list"> + { + accounts.map(account => ( + <AccountListItem + key={account.address} + account={account} + displayBalance={false} + displayAddress={false} + handleClick={() => { + onSelect(account) + closeDropdown() + }} + icon={this.getListItemIcon(account, selectedAccount)} + /> + )) + } + </div> + </div> + ) + } + + render () { + const { disabled, selectedAccount, openDropdown, dropdownOpen } = this.props + + return ( + <div className="account-dropdown-mini"> + <AccountListItem + account={selectedAccount} + handleClick={() => !disabled && openDropdown()} + displayBalance={false} + displayAddress={false} + icon={ + !disabled && <i + className="fa fa-caret-down fa-lg" + style={{ color: '#dedede' }} + /> + } + /> + { !disabled && dropdownOpen && this.renderDropdown() } + </div> + ) + } +} diff --git a/ui/app/components/ui/account-dropdown-mini/index.js b/ui/app/components/ui/account-dropdown-mini/index.js new file mode 100644 index 000000000..cb0839e72 --- /dev/null +++ b/ui/app/components/ui/account-dropdown-mini/index.js @@ -0,0 +1 @@ +export { default } from './account-dropdown-mini.component' diff --git a/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js b/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js new file mode 100644 index 000000000..bc74ceb3c --- /dev/null +++ b/ui/app/components/ui/account-dropdown-mini/tests/account-dropdown-mini.component.test.js @@ -0,0 +1,107 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import AccountDropdownMini from '../account-dropdown-mini.component' +import AccountListItem from '../../../app/send/account-list-item/account-list-item.component' + +describe('AccountDropdownMini', () => { + it('should render an account with an icon', () => { + const accounts = [ + { + address: '0x1', + name: 'account1', + balance: '0x1', + }, + { + address: '0x2', + name: 'account2', + balance: '0x2', + }, + { + address: '0x3', + name: 'account3', + balance: '0x3', + }, + ] + + const wrapper = shallow( + <AccountDropdownMini + selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }} + accounts={accounts} + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(AccountListItem).length, 1) + const accountListItemProps = wrapper.find(AccountListItem).at(0).props() + assert.equal(accountListItemProps.account.address, '0x1') + const iconProps = accountListItemProps.icon.props + assert.equal(iconProps.className, 'fa fa-caret-down fa-lg') + }) + + it('should render a list of accounts', () => { + const accounts = [ + { + address: '0x1', + name: 'account1', + balance: '0x1', + }, + { + address: '0x2', + name: 'account2', + balance: '0x2', + }, + { + address: '0x3', + name: 'account3', + balance: '0x3', + }, + ] + + const wrapper = shallow( + <AccountDropdownMini + selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }} + accounts={accounts} + dropdownOpen={true} + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(AccountListItem).length, 4) + }) + + it('should render a single account when disabled', () => { + const accounts = [ + { + address: '0x1', + name: 'account1', + balance: '0x1', + }, + { + address: '0x2', + name: 'account2', + balance: '0x2', + }, + { + address: '0x3', + name: 'account3', + balance: '0x3', + }, + ] + + const wrapper = shallow( + <AccountDropdownMini + selectedAccount={{ address: '0x1', name: 'account1', balance: '0x1' }} + accounts={accounts} + dropdownOpen={false} + disabled={true} + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(AccountListItem).length, 1) + const accountListItemProps = wrapper.find(AccountListItem).at(0).props() + assert.equal(accountListItemProps.account.address, '0x1') + assert.equal(accountListItemProps.icon, false) + }) +}) diff --git a/ui/app/components/alert/index.js b/ui/app/components/ui/alert/index.js index 5620d847a..5620d847a 100644 --- a/ui/app/components/alert/index.js +++ b/ui/app/components/ui/alert/index.js diff --git a/ui/app/components/ui/balance/balance.component.js b/ui/app/components/ui/balance/balance.component.js new file mode 100644 index 000000000..9a6f71ce5 --- /dev/null +++ b/ui/app/components/ui/balance/balance.component.js @@ -0,0 +1,92 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import TokenBalance from '../token-balance' +import Identicon from '../identicon' +import UserPreferencedCurrencyDisplay from '../../app/user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' +import { formatBalance } from '../../../helpers/utils/util' + +export default class Balance extends PureComponent { + static propTypes = { + account: PropTypes.object, + assetImages: PropTypes.object, + nativeCurrency: PropTypes.string, + needsParse: PropTypes.bool, + network: PropTypes.string, + showFiat: PropTypes.bool, + token: PropTypes.object, + } + + static defaultProps = { + needsParse: true, + showFiat: true, + } + + renderBalance () { + const { account, nativeCurrency, needsParse, showFiat } = this.props + const balanceValue = account && account.balance + const formattedBalance = balanceValue + ? formatBalance(balanceValue, 6, needsParse, nativeCurrency) + : '...' + + if (formattedBalance === 'None' || formattedBalance === '...') { + return ( + <div className="flex-column balance-display"> + <div className="token-amount"> + { formattedBalance } + </div> + </div> + ) + } + + return ( + <div className="flex-column balance-display"> + <UserPreferencedCurrencyDisplay + className="token-amount" + value={balanceValue} + type={PRIMARY} + ethNumberOfDecimals={4} + /> + { + showFiat && ( + <UserPreferencedCurrencyDisplay + value={balanceValue} + type={SECONDARY} + ethNumberOfDecimals={4} + /> + ) + } + </div> + ) + } + + renderTokenBalance () { + const { token } = this.props + + return ( + <div className="flex-column balance-display"> + <div className="token-amount"> + <TokenBalance token={token} /> + </div> + </div> + ) + } + + render () { + const { token, network, assetImages } = this.props + const address = token && token.address + const image = assetImages && address ? assetImages[token.address] : undefined + + return ( + <div className="balance-container"> + <Identicon + diameter={50} + address={address} + network={network} + image={image} + /> + { token ? this.renderTokenBalance() : this.renderBalance() } + </div> + ) + } +} diff --git a/ui/app/components/ui/balance/balance.container.js b/ui/app/components/ui/balance/balance.container.js new file mode 100644 index 000000000..2ad5c5ad8 --- /dev/null +++ b/ui/app/components/ui/balance/balance.container.js @@ -0,0 +1,32 @@ +import { connect } from 'react-redux' +import Balance from './balance.component' +import { + getNativeCurrency, + getAssetImages, + conversionRateSelector, + getCurrentCurrency, + getMetaMaskAccounts, + getIsMainnet, + preferencesSelector, +} from '../../../selectors/selectors' + +const mapStateToProps = state => { + const { showFiatInTestnets } = preferencesSelector(state) + const isMainnet = getIsMainnet(state) + const accounts = getMetaMaskAccounts(state) + const network = state.metamask.network + const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + const account = accounts[selectedAddress] + + return { + account, + network, + nativeCurrency: getNativeCurrency(state), + conversionRate: conversionRateSelector(state), + currentCurrency: getCurrentCurrency(state), + assetImages: getAssetImages(state), + showFiat: (isMainnet || !!showFiatInTestnets), + } +} + +export default connect(mapStateToProps)(Balance) diff --git a/ui/app/components/ui/balance/index.js b/ui/app/components/ui/balance/index.js new file mode 100644 index 000000000..f8fb9ea19 --- /dev/null +++ b/ui/app/components/ui/balance/index.js @@ -0,0 +1 @@ +export { default } from './balance.container' diff --git a/ui/app/components/ui/breadcrumbs/breadcrumbs.component.js b/ui/app/components/ui/breadcrumbs/breadcrumbs.component.js new file mode 100644 index 000000000..6644836db --- /dev/null +++ b/ui/app/components/ui/breadcrumbs/breadcrumbs.component.js @@ -0,0 +1,29 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +export default class Breadcrumbs extends PureComponent { + static propTypes = { + className: PropTypes.string, + currentIndex: PropTypes.number, + total: PropTypes.number, + } + + render () { + const { className, currentIndex, total } = this.props + + return ( + <div className={classnames('breadcrumbs', className)}> + { + Array(total).fill().map((_, i) => ( + <div + key={i} + className="breadcrumb" + style={{backgroundColor: i === currentIndex ? '#D8D8D8' : '#FFFFFF'}} + /> + )) + } + </div> + ) + } +} diff --git a/ui/app/components/ui/breadcrumbs/index.js b/ui/app/components/ui/breadcrumbs/index.js new file mode 100644 index 000000000..07a11574f --- /dev/null +++ b/ui/app/components/ui/breadcrumbs/index.js @@ -0,0 +1 @@ +export { default } from './breadcrumbs.component' diff --git a/ui/app/components/ui/breadcrumbs/index.scss b/ui/app/components/ui/breadcrumbs/index.scss new file mode 100644 index 000000000..e23aa7970 --- /dev/null +++ b/ui/app/components/ui/breadcrumbs/index.scss @@ -0,0 +1,15 @@ +.breadcrumbs { + display: flex; + flex-flow: row nowrap; +} + +.breadcrumb { + height: 10px; + width: 10px; + border: 1px solid #979797; + border-radius: 50%; +} + +.breadcrumb + .breadcrumb { + margin-left: 10px; +} diff --git a/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js b/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js new file mode 100644 index 000000000..5013c5b60 --- /dev/null +++ b/ui/app/components/ui/breadcrumbs/tests/breadcrumbs.component.test.js @@ -0,0 +1,22 @@ +import React from 'react' +import assert from 'assert' +import { shallow } from 'enzyme' +import Breadcrumbs from '../breadcrumbs.component' + +describe('Breadcrumbs Component', () => { + it('should render with the correct colors', () => { + const wrapper = shallow( + <Breadcrumbs + currentIndex={1} + total={3} + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.breadcrumbs').length, 1) + assert.equal(wrapper.find('.breadcrumb').length, 3) + assert.equal(wrapper.find('.breadcrumb').at(0).props().style['backgroundColor'], '#FFFFFF') + assert.equal(wrapper.find('.breadcrumb').at(1).props().style['backgroundColor'], '#D8D8D8') + assert.equal(wrapper.find('.breadcrumb').at(2).props().style['backgroundColor'], '#FFFFFF') + }) +}) diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/ui/button-group/button-group.component.js index f99f710ce..17a281030 100644 --- a/ui/app/components/button-group/button-group.component.js +++ b/ui/app/components/ui/button-group/button-group.component.js @@ -5,18 +5,30 @@ import classnames from 'classnames' export default class ButtonGroup extends PureComponent { static propTypes = { defaultActiveButtonIndex: PropTypes.number, + noButtonActiveByDefault: PropTypes.bool, disabled: PropTypes.bool, children: PropTypes.array, className: PropTypes.string, style: PropTypes.object, + newActiveButtonIndex: PropTypes.number, } static defaultProps = { className: 'button-group', + defaultActiveButtonIndex: 0, } state = { - activeButtonIndex: this.props.defaultActiveButtonIndex || 0, + activeButtonIndex: this.props.noButtonActiveByDefault + ? null + : this.props.defaultActiveButtonIndex, + } + + componentDidUpdate (_, prevState) { + // Provides an API for dynamically updating the activeButtonIndex + if (typeof this.props.newActiveButtonIndex === 'number' && prevState.activeButtonIndex !== this.props.newActiveButtonIndex) { + this.setState({ activeButtonIndex: this.props.newActiveButtonIndex }) + } } handleButtonClick (activeButtonIndex) { diff --git a/ui/app/components/button-group/button-group.stories.js b/ui/app/components/ui/button-group/button-group.stories.js index 14e1a7e49..c58c628b3 100644 --- a/ui/app/components/button-group/button-group.stories.js +++ b/ui/app/components/ui/button-group/button-group.stories.js @@ -1,7 +1,7 @@ import React from 'react' import { storiesOf } from '@storybook/react' import { action } from '@storybook/addon-actions' -import ButtonGroup from './' +import ButtonGroup from '.' import Button from '../button' import { text, boolean } from '@storybook/addon-knobs/react' diff --git a/ui/app/components/button-group/index.js b/ui/app/components/ui/button-group/index.js index df470bd57..df470bd57 100644 --- a/ui/app/components/button-group/index.js +++ b/ui/app/components/ui/button-group/index.js diff --git a/ui/app/components/button-group/index.scss b/ui/app/components/ui/button-group/index.scss index 29713c75b..29713c75b 100644 --- a/ui/app/components/button-group/index.scss +++ b/ui/app/components/ui/button-group/index.scss diff --git a/ui/app/components/button-group/tests/button-group-component.test.js b/ui/app/components/ui/button-group/tests/button-group-component.test.js index f07bb97c8..0bece90d6 100644 --- a/ui/app/components/button-group/tests/button-group-component.test.js +++ b/ui/app/components/ui/button-group/tests/button-group-component.test.js @@ -35,6 +35,20 @@ describe('ButtonGroup Component', function () { ButtonGroup.prototype.renderButtons.resetHistory() }) + describe('componentDidUpdate', () => { + it('should set the activeButtonIndex to the updated newActiveButtonIndex', () => { + assert.equal(wrapper.state('activeButtonIndex'), 1) + wrapper.setProps({ newActiveButtonIndex: 2 }) + assert.equal(wrapper.state('activeButtonIndex'), 2) + }) + + it('should not set the activeButtonIndex to an updated newActiveButtonIndex that is not a number', () => { + assert.equal(wrapper.state('activeButtonIndex'), 1) + wrapper.setProps({ newActiveButtonIndex: null }) + assert.equal(wrapper.state('activeButtonIndex'), 1) + }) + }) + describe('handleButtonClick', () => { it('should set the activeButtonIndex', () => { assert.equal(wrapper.state('activeButtonIndex'), 1) diff --git a/ui/app/components/button/button.component.js b/ui/app/components/ui/button/button.component.js index 4a06333e7..5d19219b4 100644 --- a/ui/app/components/button/button.component.js +++ b/ui/app/components/ui/button/button.component.js @@ -8,6 +8,7 @@ const CLASSNAME_SECONDARY = 'btn-secondary' const CLASSNAME_CONFIRM = 'btn-confirm' const CLASSNAME_RAISED = 'btn-raised' const CLASSNAME_LARGE = 'btn--large' +const CLASSNAME_FIRST_TIME = 'btn--first-time' const typeHash = { default: CLASSNAME_DEFAULT, @@ -15,6 +16,7 @@ const typeHash = { secondary: CLASSNAME_SECONDARY, confirm: CLASSNAME_CONFIRM, raised: CLASSNAME_RAISED, + 'first-time': CLASSNAME_FIRST_TIME, } export default class Button extends Component { @@ -22,7 +24,11 @@ export default class Button extends Component { type: PropTypes.string, large: PropTypes.bool, className: PropTypes.string, - children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.array, + PropTypes.element, + ]), } render () { diff --git a/ui/app/components/button/button.stories.js b/ui/app/components/ui/button/button.stories.js index dec084a25..667824a47 100644 --- a/ui/app/components/button/button.stories.js +++ b/ui/app/components/ui/button/button.stories.js @@ -1,7 +1,7 @@ import React from 'react' import { storiesOf } from '@storybook/react' import { action } from '@storybook/addon-actions' -import Button from './' +import Button from '.' import { text } from '@storybook/addon-knobs/react' storiesOf('Button', module) diff --git a/ui/app/components/button/index.js b/ui/app/components/ui/button/index.js index 33ae95ae2..33ae95ae2 100644 --- a/ui/app/components/button/index.js +++ b/ui/app/components/ui/button/index.js diff --git a/ui/app/components/card/card.component.js b/ui/app/components/ui/card/card.component.js index bb7241da1..bb7241da1 100644 --- a/ui/app/components/card/card.component.js +++ b/ui/app/components/ui/card/card.component.js diff --git a/ui/app/components/card/index.js b/ui/app/components/ui/card/index.js index c3ca6e3f4..c3ca6e3f4 100644 --- a/ui/app/components/card/index.js +++ b/ui/app/components/ui/card/index.js diff --git a/ui/app/components/card/index.scss b/ui/app/components/ui/card/index.scss index bde54a15e..bde54a15e 100644 --- a/ui/app/components/card/index.scss +++ b/ui/app/components/ui/card/index.scss diff --git a/ui/app/components/card/tests/card.component.test.js b/ui/app/components/ui/card/tests/card.component.test.js index cea05033f..cea05033f 100644 --- a/ui/app/components/card/tests/card.component.test.js +++ b/ui/app/components/ui/card/tests/card.component.test.js diff --git a/ui/app/components/copyButton.js b/ui/app/components/ui/copyButton.js index a60d33523..a60d33523 100644 --- a/ui/app/components/copyButton.js +++ b/ui/app/components/ui/copyButton.js diff --git a/ui/app/components/currency-display/currency-display.component.js b/ui/app/components/ui/currency-display/currency-display.component.js index 2d7413b57..04dd89892 100644 --- a/ui/app/components/currency-display/currency-display.component.js +++ b/ui/app/components/ui/currency-display/currency-display.component.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { GWEI } from '../../constants/common' +import { GWEI } from '../../../helpers/constants/common' export default class CurrencyDisplay extends PureComponent { static propTypes = { @@ -17,10 +17,11 @@ export default class CurrencyDisplay extends PureComponent { value: PropTypes.string, numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), hideLabel: PropTypes.bool, + hideTitle: PropTypes.bool, } render () { - const { className, displayValue, prefix, prefixComponent, style, suffix } = this.props + const { className, displayValue, prefix, prefixComponent, style, suffix, hideTitle } = this.props const text = `${prefix || ''}${displayValue}` const title = `${text} ${suffix}` @@ -28,9 +29,9 @@ export default class CurrencyDisplay extends PureComponent { <div className={classnames('currency-display-component', className)} style={style} - title={title} + title={!hideTitle && title || null} > - { prefixComponent} + { prefixComponent } <span className="currency-display-component__text">{ text }</span> { suffix && ( diff --git a/ui/app/components/currency-display/currency-display.container.js b/ui/app/components/ui/currency-display/currency-display.container.js index 6ddf07172..093d99c8e 100644 --- a/ui/app/components/currency-display/currency-display.container.js +++ b/ui/app/components/ui/currency-display/currency-display.container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux' import CurrencyDisplay from './currency-display.component' -import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util' +import { getValueFromWeiHex, formatCurrency } from '../../../helpers/utils/confirm-tx.util' const mapStateToProps = state => { const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state @@ -20,15 +20,24 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { currency, denomination, hideLabel, + displayValue: propsDisplayValue, + suffix: propsSuffix, ...restOwnProps } = ownProps const toCurrency = currency || currentCurrency - const convertedValue = getValueFromWeiHex({ - value, fromCurrency: nativeCurrency, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination, - }) - const displayValue = formatCurrency(convertedValue, toCurrency) - const suffix = hideLabel ? undefined : toCurrency.toUpperCase() + + const displayValue = propsDisplayValue || formatCurrency( + getValueFromWeiHex({ + value, + fromCurrency: nativeCurrency, + toCurrency, conversionRate, + numberOfDecimals, + toDenomination: denomination, + }), + toCurrency + ) + const suffix = propsSuffix || (hideLabel ? undefined : toCurrency.toUpperCase()) return { ...restStateProps, diff --git a/ui/app/components/currency-display/index.js b/ui/app/components/ui/currency-display/index.js index 38f08765f..38f08765f 100644 --- a/ui/app/components/currency-display/index.js +++ b/ui/app/components/ui/currency-display/index.js diff --git a/ui/app/components/currency-display/index.scss b/ui/app/components/ui/currency-display/index.scss index 313c932b8..313c932b8 100644 --- a/ui/app/components/currency-display/index.scss +++ b/ui/app/components/ui/currency-display/index.scss diff --git a/ui/app/components/currency-display/tests/currency-display.component.test.js b/ui/app/components/ui/currency-display/tests/currency-display.component.test.js index d9ef052f1..d9ef052f1 100644 --- a/ui/app/components/currency-display/tests/currency-display.component.test.js +++ b/ui/app/components/ui/currency-display/tests/currency-display.component.test.js diff --git a/ui/app/components/currency-display/tests/currency-display.container.test.js b/ui/app/components/ui/currency-display/tests/currency-display.container.test.js index 0c886af50..9888c366e 100644 --- a/ui/app/components/currency-display/tests/currency-display.container.test.js +++ b/ui/app/components/ui/currency-display/tests/currency-display.container.test.js @@ -131,7 +131,7 @@ describe('CurrencyDisplay container', () => { }, result: { nativeCurrency: 'ETH', - displayValue: '1e-9', + displayValue: '0.000000001', suffix: undefined, }, }, diff --git a/ui/app/components/currency-input/currency-input.component.js b/ui/app/components/ui/currency-input/currency-input.component.js index 0761a75c5..b5be0972b 100644 --- a/ui/app/components/currency-input/currency-input.component.js +++ b/ui/app/components/ui/currency-input/currency-input.component.js @@ -2,8 +2,8 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import UnitInput from '../unit-input' import CurrencyDisplay from '../currency-display' -import { getValueFromWeiHex, getWeiHexFromDecimalValue } from '../../helpers/conversions.util' -import { ETH } from '../../constants/common' +import { getValueFromWeiHex, getWeiHexFromDecimalValue } from '../../../helpers/utils/conversions.util' +import { ETH } from '../../../helpers/constants/common' /** * Component that allows user to enter currency values as a number, and props receive a converted @@ -11,15 +11,21 @@ import { ETH } from '../../constants/common' * gets converted into a decimal value depending on the currency (ETH or Fiat). */ export default class CurrencyInput extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + static propTypes = { conversionRate: PropTypes.number, currentCurrency: PropTypes.string, nativeCurrency: PropTypes.string, onChange: PropTypes.func, onBlur: PropTypes.func, - suffix: PropTypes.string, useFiat: PropTypes.bool, + hideFiat: PropTypes.bool, value: PropTypes.string, + fiatSuffix: PropTypes.string, + nativeSuffix: PropTypes.string, } constructor (props) { @@ -31,6 +37,7 @@ export default class CurrencyInput extends PureComponent { this.state = { decimalValue, hexValue, + isSwapped: false, } } @@ -46,8 +53,8 @@ export default class CurrencyInput extends PureComponent { } getDecimalValue (props) { - const { value: hexValue, useFiat, currentCurrency, conversionRate } = props - const decimalValueString = useFiat + const { value: hexValue, currentCurrency, conversionRate } = props + const decimalValueString = this.shouldUseFiat() ? getValueFromWeiHex({ value: hexValue, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2, }) @@ -58,10 +65,28 @@ export default class CurrencyInput extends PureComponent { return Number(decimalValueString) || 0 } + shouldUseFiat = () => { + const { useFiat, hideFiat } = this.props + const { isSwapped } = this.state || {} + + if (hideFiat) { + return false + } + + return isSwapped ? !useFiat : useFiat + } + + swap = () => { + const { isSwapped, decimalValue } = this.state + this.setState({isSwapped: !isSwapped}, () => { + this.handleChange(decimalValue) + }) + } + handleChange = decimalValue => { - const { useFiat, currentCurrency: fromCurrency, conversionRate, onChange } = this.props + const { currentCurrency: fromCurrency, conversionRate, onChange } = this.props - const hexValue = useFiat + const hexValue = this.shouldUseFiat() ? getWeiHexFromDecimalValue({ value: decimalValue, fromCurrency, conversionRate, invertConversionRate: true, }) @@ -78,11 +103,19 @@ export default class CurrencyInput extends PureComponent { } renderConversionComponent () { - const { useFiat, currentCurrency, nativeCurrency } = this.props + const { currentCurrency, nativeCurrency, hideFiat } = this.props const { hexValue } = this.state let currency, numberOfDecimals - if (useFiat) { + if (hideFiat) { + return ( + <div className="currency-input__conversion-component"> + { this.context.t('noConversionRateAvailable') } + </div> + ) + } + + if (this.shouldUseFiat()) { // Display ETH currency = nativeCurrency || ETH numberOfDecimals = 6 @@ -103,19 +136,25 @@ export default class CurrencyInput extends PureComponent { } render () { - const { suffix, ...restProps } = this.props + const { fiatSuffix, nativeSuffix, ...restProps } = this.props const { decimalValue } = this.state return ( - <UnitInput - {...restProps} - suffix={suffix} - onChange={this.handleChange} - onBlur={this.handleBlur} - value={decimalValue} - > - { this.renderConversionComponent() } - </UnitInput> + <UnitInput + {...restProps} + suffix={this.shouldUseFiat() ? fiatSuffix : nativeSuffix} + onChange={this.handleChange} + onBlur={this.handleBlur} + value={decimalValue} + actionComponent={( + <div + className="currency-input__swap-component" + onClick={this.swap} + /> + )} + > + { this.renderConversionComponent() } + </UnitInput> ) } } diff --git a/ui/app/components/currency-input/currency-input.container.js b/ui/app/components/ui/currency-input/currency-input.container.js index 1d1ed7b41..b5d7dfe6d 100644 --- a/ui/app/components/currency-input/currency-input.container.js +++ b/ui/app/components/ui/currency-input/currency-input.container.js @@ -1,27 +1,30 @@ import { connect } from 'react-redux' import CurrencyInput from './currency-input.component' -import { ETH } from '../../constants/common' +import { ETH } from '../../../helpers/constants/common' +import {getIsMainnet, preferencesSelector} from '../../../selectors/selectors' const mapStateToProps = state => { const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state + const { showFiatInTestnets } = preferencesSelector(state) + const isMainnet = getIsMainnet(state) return { nativeCurrency, currentCurrency, conversionRate, + hideFiat: (!isMainnet && !showFiatInTestnets), } } const mergeProps = (stateProps, dispatchProps, ownProps) => { const { nativeCurrency, currentCurrency } = stateProps - const { useFiat } = ownProps - const suffix = useFiat ? currentCurrency.toUpperCase() : nativeCurrency || ETH return { ...stateProps, ...dispatchProps, ...ownProps, - suffix, + nativeSuffix: nativeCurrency || ETH, + fiatSuffix: currentCurrency.toUpperCase(), } } diff --git a/ui/app/components/currency-input/index.js b/ui/app/components/ui/currency-input/index.js index d8069fb67..d8069fb67 100644 --- a/ui/app/components/currency-input/index.js +++ b/ui/app/components/ui/currency-input/index.js diff --git a/ui/app/components/ui/currency-input/index.scss b/ui/app/components/ui/currency-input/index.scss new file mode 100644 index 000000000..f659f5b35 --- /dev/null +++ b/ui/app/components/ui/currency-input/index.scss @@ -0,0 +1,26 @@ +.currency-input { + &__conversion-component { + font-size: 12px; + line-height: 12px; + padding-left: 1px; + } + + &__swap-component { + flex: 0 0 auto; + height: 24px; + width: 24px; + background-image: url("images/icons/swap.svg"); + background-size: contain; + background-repeat: no-repeat; + cursor: pointer; + opacity: .4; + + &:hover { + opacity: .3; + } + + &:active { + opacity: .5; + } + } +} diff --git a/ui/app/components/currency-input/tests/currency-input.component.test.js b/ui/app/components/ui/currency-input/tests/currency-input.component.test.js index a33889f94..6d4612e3c 100644 --- a/ui/app/components/currency-input/tests/currency-input.component.test.js +++ b/ui/app/components/ui/currency-input/tests/currency-input.component.test.js @@ -1,4 +1,5 @@ import React from 'react' +import PropTypes from 'prop-types' import assert from 'assert' import { shallow, mount } from 'enzyme' import sinon from 'sinon' @@ -32,7 +33,8 @@ describe('CurrencyInput Component', () => { const wrapper = mount( <Provider store={store}> <CurrencyInput - suffix="ETH" + nativeSuffix="ETH" + fiatSuffix="USD" nativeCurrency="ETH" /> </Provider> @@ -58,7 +60,8 @@ describe('CurrencyInput Component', () => { <Provider store={store}> <CurrencyInput value="de0b6b3a7640000" - suffix="ETH" + fiatSuffix="USD" + nativeSuffix="ETH" nativeCurrency="ETH" currentCurrency="usd" conversionRate={231.06} @@ -90,7 +93,8 @@ describe('CurrencyInput Component', () => { <Provider store={store}> <CurrencyInput value="f602f2234d0ea" - suffix="USD" + fiatSuffix="USD" + nativeSuffix="ETH" useFiat nativeCurrency="ETH" currentCurrency="usd" @@ -108,6 +112,45 @@ describe('CurrencyInput Component', () => { assert.equal(wrapper.find('.unit-input__input').props().value, '1') assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH') }) + + it('should render properly with a native value when hideFiat is true', () => { + const mockStore = { + metamask: { + nativeCurrency: 'ETH', + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <CurrencyInput + value="f602f2234d0ea" + fiatSuffix="USD" + nativeSuffix="ETH" + useFiat + hideFiat={true} + nativeCurrency="ETH" + currentCurrency="usd" + conversionRate={231.06} + /> + </Provider>, + { + context: { t: str => str + '_t' }, + childContextTypes: { t: PropTypes.func }, + } + ) + + assert.ok(wrapper) + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 0.004328) + assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea') + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') + assert.equal(wrapper.find('.unit-input__input').props().value, '0.004328') + assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'noConversionRateAvailable_t') + }) }) describe('handling actions', () => { @@ -247,5 +290,56 @@ describe('CurrencyInput Component', () => { assert.equal(currencyInputInstance.state('hexValue'), '1ec05e43e72400') assert.equal(currencyInputInstance.find(UnitInput).props().value, 2) }) + + it('should swap selected currency when swap icon is clicked', () => { + const mockStore = { + metamask: { + nativeCurrency: 'ETH', + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = mount( + <Provider store={store}> + <CurrencyInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + nativeSuffix="ETH" + fiatSuffix="USD" + nativeCurrency="ETH" + currentCurrency="usd" + conversionRate={231.06} + /> + </Provider> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 0) + assert.equal(currencyInputInstance.state.hexValue, undefined) + assert.equal(wrapper.find('.currency-display-component').text(), '$0.00USD') + const input = wrapper.find('input') + assert.equal(input.props().value, 0) + + input.simulate('change', { target: { value: 1 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000')) + assert.equal(wrapper.find('.currency-display-component').text(), '$231.06USD') + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('de0b6b3a7640000')) + + const swap = wrapper.find('.currency-input__swap-component') + swap.simulate('click') + assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH') + }) }) }) diff --git a/ui/app/components/ui/currency-input/tests/currency-input.container.test.js b/ui/app/components/ui/currency-input/tests/currency-input.container.test.js new file mode 100644 index 000000000..6109d29b6 --- /dev/null +++ b/ui/app/components/ui/currency-input/tests/currency-input.container.test.js @@ -0,0 +1,170 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps, mergeProps + +proxyquire('../currency-input.container.js', { + 'react-redux': { + connect: (ms, md, mp) => { + mapStateToProps = ms + mergeProps = mp + return () => ({}) + }, + }, +}) + +describe('CurrencyInput container', () => { + describe('mapStateToProps()', () => { + const tests = [ + // Test # 1 + { + comment: 'should return correct props in mainnet', + mockState: { + metamask: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + preferences: { + showFiatInTestnets: false, + }, + provider: { + type: 'mainnet', + }, + }, + }, + expected: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + hideFiat: false, + }, + }, + // Test # 2 + { + comment: 'should return correct props when not in mainnet and showFiatInTestnets is false', + mockState: { + metamask: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + preferences: { + showFiatInTestnets: false, + }, + provider: { + type: 'rinkeby', + }, + }, + }, + expected: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + hideFiat: true, + }, + }, + // Test # 3 + { + comment: 'should return correct props when not in mainnet and showFiatInTestnets is true', + mockState: { + metamask: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + preferences: { + showFiatInTestnets: true, + }, + provider: { + type: 'rinkeby', + }, + }, + }, + expected: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + hideFiat: false, + }, + }, + // Test # 4 + { + comment: 'should return correct props when in mainnet and showFiatInTestnets is true', + mockState: { + metamask: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + preferences: { + showFiatInTestnets: true, + }, + provider: { + type: 'mainnet', + }, + }, + }, + expected: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + hideFiat: false, + }, + }, + ] + + tests.forEach(({ mockState, expected, comment }) => { + it(comment, () => assert.deepEqual(mapStateToProps(mockState), expected)) + }) + }) + + describe('mergeProps()', () => { + const tests = [ + // Test # 1 + { + comment: 'should return the correct props', + mock: { + stateProps: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + }, + dispatchProps: {}, + ownProps: {}, + }, + expected: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + // useFiat: true, + nativeSuffix: 'ETH', + fiatSuffix: 'USD', + }, + }, + // Test # 1 + { + comment: 'should return the correct props when useFiat is true', + mock: { + stateProps: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + }, + dispatchProps: {}, + ownProps: { useFiat: true }, + }, + expected: { + conversionRate: 280.45, + currentCurrency: 'usd', + nativeCurrency: 'ETH', + useFiat: true, + nativeSuffix: 'ETH', + fiatSuffix: 'USD', + }, + }, + ] + + tests.forEach(({ mock: { stateProps, dispatchProps, ownProps }, expected, comment }) => { + it(comment, () => { + assert.deepEqual(mergeProps(stateProps, dispatchProps, ownProps), expected) + }) + }) + }) +}) diff --git a/ui/app/components/editable-label.js b/ui/app/components/ui/editable-label.js index eb41ec50c..eb41ec50c 100644 --- a/ui/app/components/editable-label.js +++ b/ui/app/components/ui/editable-label.js diff --git a/ui/app/components/error-message/error-message.component.js b/ui/app/components/ui/error-message/error-message.component.js index b4464c33b..b4464c33b 100644 --- a/ui/app/components/error-message/error-message.component.js +++ b/ui/app/components/ui/error-message/error-message.component.js diff --git a/ui/app/components/error-message/index.js b/ui/app/components/ui/error-message/index.js index 1c97a9955..1c97a9955 100644 --- a/ui/app/components/error-message/index.js +++ b/ui/app/components/ui/error-message/index.js diff --git a/ui/app/components/error-message/index.scss b/ui/app/components/ui/error-message/index.scss index 5915e21cf..5915e21cf 100644 --- a/ui/app/components/error-message/index.scss +++ b/ui/app/components/ui/error-message/index.scss diff --git a/ui/app/components/error-message/tests/error-message.component.test.js b/ui/app/components/ui/error-message/tests/error-message.component.test.js index 8c5347173..8c5347173 100644 --- a/ui/app/components/error-message/tests/error-message.component.test.js +++ b/ui/app/components/ui/error-message/tests/error-message.component.test.js diff --git a/ui/app/components/eth-balance.js b/ui/app/components/ui/eth-balance.js index 2f6395a2d..7d577b716 100644 --- a/ui/app/components/eth-balance.js +++ b/ui/app/components/ui/eth-balance.js @@ -5,7 +5,7 @@ const { inherits } = require('util') const { formatBalance, generateBalanceObject, -} = require('../util') +} = require('../../helpers/utils/util') const Tooltip = require('./tooltip.js') const FiatValue = require('./fiat-value.js') diff --git a/ui/app/components/export-text-container/export-text-container.component.js b/ui/app/components/ui/export-text-container/export-text-container.component.js index c2546fa9b..c632e8f26 100644 --- a/ui/app/components/export-text-container/export-text-container.component.js +++ b/ui/app/components/ui/export-text-container/export-text-container.component.js @@ -2,7 +2,7 @@ const { Component } = require('react') const PropTypes = require('prop-types') const h = require('react-hyperscript') const copyToClipboard = require('copy-to-clipboard') -const { exportAsFile } = require('../../util') +const { exportAsFile } = require('../../../helpers/utils/util') class ExportTextContainer extends Component { render () { diff --git a/ui/app/components/export-text-container/index.js b/ui/app/components/ui/export-text-container/index.js index b2864a717..b2864a717 100644 --- a/ui/app/components/export-text-container/index.js +++ b/ui/app/components/ui/export-text-container/index.js diff --git a/ui/app/components/export-text-container/index.scss b/ui/app/components/ui/export-text-container/index.scss index 975d62f70..975d62f70 100644 --- a/ui/app/components/export-text-container/index.scss +++ b/ui/app/components/ui/export-text-container/index.scss diff --git a/ui/app/components/fiat-value.js b/ui/app/components/ui/fiat-value.js index 56465fc9d..02111ba49 100644 --- a/ui/app/components/fiat-value.js +++ b/ui/app/components/ui/fiat-value.js @@ -1,7 +1,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits -const formatBalance = require('../util').formatBalance +const formatBalance = require('../../helpers/utils/util').formatBalance module.exports = FiatValue diff --git a/ui/app/components/hex-to-decimal/hex-to-decimal.component.js b/ui/app/components/ui/hex-to-decimal/hex-to-decimal.component.js index 6847a6919..f03aaf255 100644 --- a/ui/app/components/hex-to-decimal/hex-to-decimal.component.js +++ b/ui/app/components/ui/hex-to-decimal/hex-to-decimal.component.js @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import { hexToDecimal } from '../../helpers/conversions.util' +import { hexToDecimal } from '../../../helpers/utils/conversions.util' export default class HexToDecimal extends PureComponent { static propTypes = { diff --git a/ui/app/components/hex-to-decimal/index.js b/ui/app/components/ui/hex-to-decimal/index.js index 6e8567ca9..6e8567ca9 100644 --- a/ui/app/components/hex-to-decimal/index.js +++ b/ui/app/components/ui/hex-to-decimal/index.js diff --git a/ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js b/ui/app/components/ui/hex-to-decimal/tests/hex-to-decimal.component.test.js index c98da9ad4..c98da9ad4 100644 --- a/ui/app/components/hex-to-decimal/tests/hex-to-decimal.component.test.js +++ b/ui/app/components/ui/hex-to-decimal/tests/hex-to-decimal.component.test.js diff --git a/ui/app/components/identicon/identicon.component.js b/ui/app/components/ui/identicon/identicon.component.js index b892e5ae5..88521247c 100644 --- a/ui/app/components/identicon/identicon.component.js +++ b/ui/app/components/ui/identicon/identicon.component.js @@ -1,9 +1,9 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { toDataUrl } from '../../../lib/blockies' +import { toDataUrl } from '../../../../lib/blockies' import contractMap from 'eth-contract-metadata' -import { checksumAddress } from '../../../app/util' +import { checksumAddress } from '../../../helpers/utils/util' import Jazzicon from '../jazzicon' const getStyles = diameter => ( diff --git a/ui/app/components/identicon/identicon.container.js b/ui/app/components/ui/identicon/identicon.container.js index bc49bc18e..bc49bc18e 100644 --- a/ui/app/components/identicon/identicon.container.js +++ b/ui/app/components/ui/identicon/identicon.container.js diff --git a/ui/app/components/identicon/index.js b/ui/app/components/ui/identicon/index.js index 799c886f2..799c886f2 100644 --- a/ui/app/components/identicon/index.js +++ b/ui/app/components/ui/identicon/index.js diff --git a/ui/app/components/identicon/index.scss b/ui/app/components/ui/identicon/index.scss index 657afc48f..657afc48f 100644 --- a/ui/app/components/identicon/index.scss +++ b/ui/app/components/ui/identicon/index.scss diff --git a/ui/app/components/identicon/tests/identicon.component.test.js b/ui/app/components/ui/identicon/tests/identicon.component.test.js index 2944818f5..2944818f5 100644 --- a/ui/app/components/identicon/tests/identicon.component.test.js +++ b/ui/app/components/ui/identicon/tests/identicon.component.test.js diff --git a/ui/app/components/jazzicon/index.js b/ui/app/components/ui/jazzicon/index.js index bea900ab9..bea900ab9 100644 --- a/ui/app/components/jazzicon/index.js +++ b/ui/app/components/ui/jazzicon/index.js diff --git a/ui/app/components/jazzicon/jazzicon.component.js b/ui/app/components/ui/jazzicon/jazzicon.component.js index fcb1c59b1..3a17e446f 100644 --- a/ui/app/components/jazzicon/jazzicon.component.js +++ b/ui/app/components/ui/jazzicon/jazzicon.component.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import isNode from 'detect-node' import { findDOMNode } from 'react-dom' import jazzicon from 'jazzicon' -import iconFactoryGenerator from '../../../lib/icon-factory' +import iconFactoryGenerator from '../../../../lib/icon-factory' const iconFactory = iconFactoryGenerator(jazzicon) /** diff --git a/ui/app/components/loading-screen/index.js b/ui/app/components/ui/loading-screen/index.js index 191d953f7..191d953f7 100644 --- a/ui/app/components/loading-screen/index.js +++ b/ui/app/components/ui/loading-screen/index.js diff --git a/ui/app/components/loading-screen/loading-screen.component.js b/ui/app/components/ui/loading-screen/loading-screen.component.js index 6b843cfee..6b843cfee 100644 --- a/ui/app/components/loading-screen/loading-screen.component.js +++ b/ui/app/components/ui/loading-screen/loading-screen.component.js diff --git a/ui/app/components/ui/lock-icon/index.js b/ui/app/components/ui/lock-icon/index.js new file mode 100644 index 000000000..6b4df0e58 --- /dev/null +++ b/ui/app/components/ui/lock-icon/index.js @@ -0,0 +1 @@ +export { default } from './lock-icon.component' diff --git a/ui/app/components/ui/lock-icon/lock-icon.component.js b/ui/app/components/ui/lock-icon/lock-icon.component.js new file mode 100644 index 000000000..d010cb6b2 --- /dev/null +++ b/ui/app/components/ui/lock-icon/lock-icon.component.js @@ -0,0 +1,32 @@ +import React from 'react' + +export default function LockIcon (props) { + return ( + <svg + version="1.1" + id="Capa_1" + xmlns="http://www.w3.org/2000/svg" + xmlnsXlink="http://www.w3.org/1999/xlink" + x="0px" + y="0px" + width="401.998px" + height="401.998px" + viewBox="0 0 401.998 401.998" + style={{enableBackground: 'new 0 0 401.998 401.998'}} + xmlSpace="preserve" + {...props} + > + <g> + <path + d="M357.45,190.721c-5.331-5.33-11.8-7.993-19.417-7.993h-9.131v-54.821c0-35.022-12.559-65.093-37.685-90.218 + C266.093,12.563,236.025,0,200.998,0c-35.026,0-65.1,12.563-90.222,37.688C85.65,62.814,73.091,92.884,73.091,127.907v54.821 + h-9.135c-7.611,0-14.084,2.663-19.414,7.993c-5.33,5.326-7.994,11.799-7.994,19.417V374.59c0,7.611,2.665,14.086,7.994,19.417 + c5.33,5.325,11.803,7.991,19.414,7.991H338.04c7.617,0,14.085-2.663,19.417-7.991c5.325-5.331,7.994-11.806,7.994-19.417V210.135 + C365.455,202.523,362.782,196.051,357.45,190.721z M274.087,182.728H127.909v-54.821c0-20.175,7.139-37.402,21.414-51.675 + c14.277-14.275,31.501-21.411,51.678-21.411c20.179,0,37.399,7.135,51.677,21.411c14.271,14.272,21.409,31.5,21.409,51.675V182.728 + z" + /> + </g> + </svg> + ) +} diff --git a/ui/app/components/mascot.js b/ui/app/components/ui/mascot.js index 3b0d3e31b..3b0d3e31b 100644 --- a/ui/app/components/mascot.js +++ b/ui/app/components/ui/mascot.js diff --git a/ui/app/components/page-container/index.js b/ui/app/components/ui/page-container/index.js index 913b8c9c6..913b8c9c6 100644 --- a/ui/app/components/page-container/index.js +++ b/ui/app/components/ui/page-container/index.js diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/ui/page-container/index.scss index ba1215e84..b71a3cb9d 100644 --- a/ui/app/components/page-container/index.scss +++ b/ui/app/components/ui/page-container/index.scss @@ -6,6 +6,7 @@ display: flex; flex-flow: column; border-radius: 8px; + overflow-y: auto; &__header { display: flex; @@ -41,6 +42,12 @@ justify-content: space-between; } + &__bottom { + flex: 1; + display: flex; + flex-direction: column; + } + &__footer { display: flex; flex-flow: column; @@ -194,10 +201,10 @@ .page-container { height: 100%; width: 100%; - overflow-y: auto; background-color: $white; border-radius: 0; flex: 1; + overflow-y: auto; } } diff --git a/ui/app/components/page-container/page-container-content.component.js b/ui/app/components/ui/page-container/page-container-content.component.js index a1d6988cc..a1d6988cc 100644 --- a/ui/app/components/page-container/page-container-content.component.js +++ b/ui/app/components/ui/page-container/page-container-content.component.js diff --git a/ui/app/components/page-container/page-container-footer/index.js b/ui/app/components/ui/page-container/page-container-footer/index.js index 7825c4520..7825c4520 100644 --- a/ui/app/components/page-container/page-container-footer/index.js +++ b/ui/app/components/ui/page-container/page-container-footer/index.js diff --git a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js index 773fe1f56..85b16cefe 100644 --- a/ui/app/components/page-container/page-container-footer/page-container-footer.component.js +++ b/ui/app/components/ui/page-container/page-container-footer/page-container-footer.component.js @@ -12,6 +12,7 @@ export default class PageContainerFooter extends Component { submitText: PropTypes.string, disabled: PropTypes.bool, submitButtonType: PropTypes.string, + hideCancel: PropTypes.bool, } static contextTypes = { @@ -27,20 +28,21 @@ export default class PageContainerFooter extends Component { submitText, disabled, submitButtonType, + hideCancel, } = this.props return ( <div className="page-container__footer"> <header> - <Button + {!hideCancel && <Button type="default" large className="page-container__footer-button" onClick={e => onCancel(e)} > { cancelText || this.context.t('cancel') } - </Button> + </Button>} <Button type={submitButtonType || 'primary'} diff --git a/ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js b/ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js index 64efabab0..64efabab0 100644 --- a/ui/app/components/page-container/page-container-footer/tests/page-container-footer.component.test.js +++ b/ui/app/components/ui/page-container/page-container-footer/tests/page-container-footer.component.test.js diff --git a/ui/app/components/page-container/page-container-header/index.js b/ui/app/components/ui/page-container/page-container-header/index.js index b194af057..b194af057 100644 --- a/ui/app/components/page-container/page-container-header/index.js +++ b/ui/app/components/ui/page-container/page-container-header/index.js diff --git a/ui/app/components/page-container/page-container-header/page-container-header.component.js b/ui/app/components/ui/page-container/page-container-header/page-container-header.component.js index a8458604e..08f9c7544 100644 --- a/ui/app/components/page-container/page-container-header/page-container-header.component.js +++ b/ui/app/components/ui/page-container/page-container-header/page-container-header.component.js @@ -12,6 +12,7 @@ export default class PageContainerHeader extends Component { backButtonStyles: PropTypes.object, backButtonString: PropTypes.string, tabs: PropTypes.node, + headerCloseText: PropTypes.string, } renderTabs () { @@ -41,7 +42,7 @@ export default class PageContainerHeader extends Component { } render () { - const { title, subtitle, onClose, tabs } = this.props + const { title, subtitle, onClose, tabs, headerCloseText } = this.props return ( <div className={ @@ -66,10 +67,12 @@ export default class PageContainerHeader extends Component { } { - onClose && <div - className="page-container__header-close" - onClick={() => onClose()} - /> + onClose && headerCloseText + ? <div className="page-container__header-close-text" onClick={() => onClose()}>{ headerCloseText }</div> + : onClose && <div + className="page-container__header-close" + onClick={() => onClose()} + /> } { this.renderTabs() } diff --git a/ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js b/ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js index 59304b2bd..59304b2bd 100644 --- a/ui/app/components/page-container/page-container-header/tests/page-container-header.component.test.js +++ b/ui/app/components/ui/page-container/page-container-header/tests/page-container-header.component.test.js diff --git a/ui/app/components/page-container/page-container.component.js b/ui/app/components/ui/page-container/page-container.component.js index 3a2274a29..45dfff517 100644 --- a/ui/app/components/page-container/page-container.component.js +++ b/ui/app/components/ui/page-container/page-container.component.js @@ -9,6 +9,7 @@ export default class PageContainer extends PureComponent { // PageContainerHeader props backButtonString: PropTypes.string, backButtonStyles: PropTypes.object, + headerCloseText: PropTypes.string, onBackButtonClick: PropTypes.func, onClose: PropTypes.func, showBackButton: PropTypes.bool, @@ -22,6 +23,7 @@ export default class PageContainer extends PureComponent { // PageContainerFooter props cancelText: PropTypes.string, disabled: PropTypes.bool, + hideCancel: PropTypes.bool, onCancel: PropTypes.func, onSubmit: PropTypes.func, submitText: PropTypes.string, @@ -58,7 +60,8 @@ export default class PageContainer extends PureComponent { renderActiveTabContent () { const { tabsComponent } = this.props - const { children } = tabsComponent.props + let { children } = tabsComponent.props + children = children.filter(child => child) const { activeTabIndex } = this.state return children[activeTabIndex] @@ -92,6 +95,8 @@ export default class PageContainer extends PureComponent { onSubmit, submitText, disabled, + headerCloseText, + hideCancel, } = this.props return ( @@ -105,17 +110,21 @@ export default class PageContainer extends PureComponent { backButtonStyles={backButtonStyles} backButtonString={backButtonString} tabs={this.renderTabs()} + headerCloseText={headerCloseText} /> - <div className="page-container__content"> - { this.renderContent() } + <div className="page-container__bottom"> + <div className="page-container__content"> + { this.renderContent() } + </div> + <PageContainerFooter + onCancel={onCancel} + cancelText={cancelText} + hideCancel={hideCancel} + onSubmit={onSubmit} + submitText={submitText} + disabled={disabled} + /> </div> - <PageContainerFooter - onCancel={onCancel} - cancelText={cancelText} - onSubmit={onSubmit} - submitText={submitText} - disabled={disabled} - /> </div> ) } diff --git a/ui/app/components/send/account-list-item/account-list-item.scss b/ui/app/components/ui/page-container/tests/page-container.component.test.js index e69de29bb..e69de29bb 100644 --- a/ui/app/components/send/account-list-item/account-list-item.scss +++ b/ui/app/components/ui/page-container/tests/page-container.component.test.js diff --git a/ui/app/components/qr-code.js b/ui/app/components/ui/qr-code.js index d3242ddf5..351e072e2 100644 --- a/ui/app/components/qr-code.js +++ b/ui/app/components/ui/qr-code.js @@ -1,11 +1,11 @@ const Component = require('react').Component const h = require('react-hyperscript') -const qrCode = require('qrcode-npm').qrcode +const qrCode = require('qrcode-generator') const inherits = require('util').inherits const connect = require('react-redux').connect const { isHexPrefixed } = require('ethereumjs-util') const ReadOnlyInput = require('./readonly-input') -const { checksumAddress } = require('../util') +const { checksumAddress } = require('../../helpers/utils/util') module.exports = connect(mapStateToProps)(QrCodeView) @@ -25,8 +25,8 @@ function QrCodeView () { QrCodeView.prototype.render = function () { const props = this.props - const { message, data } = props.Qr - const address = `${isHexPrefixed(data) ? 'ethereum:' : ''}${checksumAddress(data)}` + const { message, data, network } = props.Qr + const address = `${isHexPrefixed(data) ? 'ethereum:' : ''}${checksumAddress(data, network)}` const qrImage = qrCode(4, 'M') qrImage.addData(address) qrImage.make() @@ -51,7 +51,7 @@ QrCodeView.prototype.render = function () { h(ReadOnlyInput, { wrapperClass: 'ellip-address-wrapper', inputClass: 'qr-ellip-address', - value: checksumAddress(data), + value: checksumAddress(data, network), }), ]) } diff --git a/ui/app/components/readonly-input.js b/ui/app/components/ui/readonly-input.js index fcf05fb9e..fcf05fb9e 100644 --- a/ui/app/components/readonly-input.js +++ b/ui/app/components/ui/readonly-input.js diff --git a/ui/app/components/sender-to-recipient/index.js b/ui/app/components/ui/sender-to-recipient/index.js index f515c4ac4..f515c4ac4 100644 --- a/ui/app/components/sender-to-recipient/index.js +++ b/ui/app/components/ui/sender-to-recipient/index.js diff --git a/ui/app/components/sender-to-recipient/index.scss b/ui/app/components/ui/sender-to-recipient/index.scss index 0ab0413be..b21e4e1bb 100644 --- a/ui/app/components/sender-to-recipient/index.scss +++ b/ui/app/components/ui/sender-to-recipient/index.scss @@ -1,12 +1,13 @@ .sender-to-recipient { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + position: relative; + flex: 0 0 auto; + &--default { - width: 100%; - display: flex; - flex-direction: row; - justify-content: center; border-bottom: 1px solid $geyser; - position: relative; - flex: 0 0 auto; height: 42px; .sender-to-recipient { @@ -74,13 +75,6 @@ } &--cards { - width: 100%; - display: flex; - flex-direction: row; - justify-content: center; - position: relative; - flex: 0 0 auto; - .sender-to-recipient { &__party { display: flex; @@ -117,4 +111,39 @@ } } } + + &--flat { + .sender-to-recipient { + &__party { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + flex: 1; + padding: 6px; + cursor: pointer; + min-width: 0; + color: $dusty-gray; + } + + &__tooltip-wrapper { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: .6875rem; + } + + &__arrow-container { + display: flex; + justify-content: center; + align-items: center; + } + } + } } diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js index e71bd7406..57b595d48 100644 --- a/ui/app/components/sender-to-recipient/sender-to-recipient.component.js +++ b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js @@ -4,12 +4,13 @@ import classnames from 'classnames' import Identicon from '../identicon' import Tooltip from '../tooltip-v2' import copyToClipboard from 'copy-to-clipboard' -import { DEFAULT_VARIANT, CARDS_VARIANT } from './sender-to-recipient.constants' -import { checksumAddress } from '../../util' +import { DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT } from './sender-to-recipient.constants' +import { checksumAddress } from '../../../helpers/utils/util' const variantHash = { [DEFAULT_VARIANT]: 'sender-to-recipient--default', [CARDS_VARIANT]: 'sender-to-recipient--cards', + [FLAT_VARIANT]: 'sender-to-recipient--flat', } export default class SenderToRecipient extends PureComponent { @@ -19,9 +20,11 @@ export default class SenderToRecipient extends PureComponent { recipientName: PropTypes.string, recipientAddress: PropTypes.string, t: PropTypes.func, - variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT]), + variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT]), addressOnly: PropTypes.bool, assetImage: PropTypes.string, + onRecipientClick: PropTypes.func, + onSenderClick: PropTypes.func, } static defaultProps = { @@ -85,7 +88,7 @@ export default class SenderToRecipient extends PureComponent { renderRecipientWithAddress () { const { t } = this.context - const { recipientName, recipientAddress, addressOnly } = this.props + const { recipientName, recipientAddress, addressOnly, onRecipientClick } = this.props const checksummedRecipientAddress = checksumAddress(recipientAddress) return ( @@ -94,6 +97,9 @@ export default class SenderToRecipient extends PureComponent { onClick={() => { this.setState({ recipientAddressCopied: true }) copyToClipboard(checksummedRecipientAddress) + if (onRecipientClick) { + onRecipientClick() + } }} > { this.renderRecipientIdenticon() } @@ -128,16 +134,9 @@ export default class SenderToRecipient extends PureComponent { } renderArrow () { - return this.props.variant === CARDS_VARIANT + return this.props.variant === DEFAULT_VARIANT ? ( <div className="sender-to-recipient__arrow-container"> - <img - height={20} - src="./images/caret-right.svg" - /> - </div> - ) : ( - <div className="sender-to-recipient__arrow-container"> <div className="sender-to-recipient__arrow-circle"> <img height={15} @@ -146,20 +145,30 @@ export default class SenderToRecipient extends PureComponent { /> </div> </div> + ) : ( + <div className="sender-to-recipient__arrow-container"> + <img + height={20} + src="./images/caret-right.svg" + /> + </div> ) } render () { - const { senderAddress, recipientAddress, variant } = this.props + const { senderAddress, recipientAddress, variant, onSenderClick } = this.props const checksummedSenderAddress = checksumAddress(senderAddress) return ( - <div className={classnames(variantHash[variant])}> + <div className={classnames('sender-to-recipient', variantHash[variant])}> <div className={classnames('sender-to-recipient__party sender-to-recipient__party--sender')} onClick={() => { this.setState({ senderAddressCopied: true }) copyToClipboard(checksummedSenderAddress) + if (onSenderClick) { + onSenderClick() + } }} > { this.renderSenderIdenticon() } diff --git a/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.constants.js index 166228932..f53a5115d 100644 --- a/ui/app/components/sender-to-recipient/sender-to-recipient.constants.js +++ b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.constants.js @@ -1,3 +1,4 @@ // Component design variants export const DEFAULT_VARIANT = 'DEFAULT_VARIANT' export const CARDS_VARIANT = 'CARDS_VARIANT' +export const FLAT_VARIANT = 'FLAT_VARIANT' diff --git a/ui/app/components/spinner/index.js b/ui/app/components/ui/spinner/index.js index 9589efcf0..9589efcf0 100644 --- a/ui/app/components/spinner/index.js +++ b/ui/app/components/ui/spinner/index.js diff --git a/ui/app/components/spinner/spinner.component.js b/ui/app/components/ui/spinner/spinner.component.js index b9a2eb52a..b9a2eb52a 100644 --- a/ui/app/components/spinner/spinner.component.js +++ b/ui/app/components/ui/spinner/spinner.component.js diff --git a/ui/app/components/tabs/index.js b/ui/app/components/ui/tabs/index.js index 3a8d18248..3a8d18248 100644 --- a/ui/app/components/tabs/index.js +++ b/ui/app/components/ui/tabs/index.js diff --git a/ui/app/components/tabs/index.scss b/ui/app/components/ui/tabs/index.scss index a3b42f8e3..25143ff35 100644 --- a/ui/app/components/tabs/index.scss +++ b/ui/app/components/ui/tabs/index.scss @@ -1,4 +1,4 @@ -@import './tab/index'; +@import 'tab/index'; .tabs { &__list { diff --git a/ui/app/components/tabs/tab/index.js b/ui/app/components/ui/tabs/tab/index.js index fbc309e8e..fbc309e8e 100644 --- a/ui/app/components/tabs/tab/index.js +++ b/ui/app/components/ui/tabs/tab/index.js diff --git a/ui/app/components/tabs/tab/index.scss b/ui/app/components/ui/tabs/tab/index.scss index 1de6ffa0e..1de6ffa0e 100644 --- a/ui/app/components/tabs/tab/index.scss +++ b/ui/app/components/ui/tabs/tab/index.scss diff --git a/ui/app/components/tabs/tab/tab.component.js b/ui/app/components/ui/tabs/tab/tab.component.js index 9e590391c..9e590391c 100644 --- a/ui/app/components/tabs/tab/tab.component.js +++ b/ui/app/components/ui/tabs/tab/tab.component.js diff --git a/ui/app/components/tabs/tabs.component.js b/ui/app/components/ui/tabs/tabs.component.js index d26dcff2f..d26dcff2f 100644 --- a/ui/app/components/tabs/tabs.component.js +++ b/ui/app/components/ui/tabs/tabs.component.js diff --git a/ui/app/components/text-field/index.js b/ui/app/components/ui/text-field/index.js index 171caf7a4..171caf7a4 100644 --- a/ui/app/components/text-field/index.js +++ b/ui/app/components/ui/text-field/index.js diff --git a/ui/app/components/text-field/text-field.component.js b/ui/app/components/ui/text-field/text-field.component.js index 2c72d8124..2c72d8124 100644 --- a/ui/app/components/text-field/text-field.component.js +++ b/ui/app/components/ui/text-field/text-field.component.js diff --git a/ui/app/components/text-field/text-field.stories.js b/ui/app/components/ui/text-field/text-field.stories.js index c00873b8a..337f78ecf 100644 --- a/ui/app/components/text-field/text-field.stories.js +++ b/ui/app/components/ui/text-field/text-field.stories.js @@ -1,6 +1,6 @@ import React from 'react' import { storiesOf } from '@storybook/react' -import TextField from './' +import TextField from '.' storiesOf('TextField', module) .add('text', () => diff --git a/ui/app/components/token-balance/index.js b/ui/app/components/ui/token-balance/index.js index f7da15cf8..f7da15cf8 100644 --- a/ui/app/components/token-balance/index.js +++ b/ui/app/components/ui/token-balance/index.js diff --git a/ui/app/components/ui/token-balance/index.scss b/ui/app/components/ui/token-balance/index.scss new file mode 100644 index 000000000..2ff6dfbc8 --- /dev/null +++ b/ui/app/components/ui/token-balance/index.scss @@ -0,0 +1,14 @@ +.token-balance-component { + display: flex; + align-items: center; + + &__text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__suffix { + padding-left: 4px; + } +} diff --git a/ui/app/components/token-balance/token-balance.component.js b/ui/app/components/ui/token-balance/token-balance.component.js index 2b4f73980..af1a32578 100644 --- a/ui/app/components/token-balance/token-balance.component.js +++ b/ui/app/components/ui/token-balance/token-balance.component.js @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import classnames from 'classnames' +import CurrencyDisplay from '../currency-display' export default class TokenBalance extends PureComponent { static propTypes = { @@ -12,12 +12,14 @@ export default class TokenBalance extends PureComponent { } render () { - const { className, string, withSymbol, symbol } = this.props + const { className, string, symbol } = this.props return ( - <div className={classnames('hide-text-overflow', className)}> - { string + (withSymbol ? ` ${symbol}` : '') } - </div> + <CurrencyDisplay + className={className} + displayValue={string} + suffix={symbol} + /> ) } } diff --git a/ui/app/components/token-balance/token-balance.container.js b/ui/app/components/ui/token-balance/token-balance.container.js index adc001f83..a0f1efc20 100644 --- a/ui/app/components/token-balance/token-balance.container.js +++ b/ui/app/components/ui/token-balance/token-balance.container.js @@ -1,8 +1,8 @@ import { connect } from 'react-redux' import { compose } from 'recompose' -import withTokenTracker from '../../higher-order-components/with-token-tracker' +import withTokenTracker from '../../../helpers/higher-order-components/with-token-tracker' import TokenBalance from './token-balance.component' -import selectors from '../../selectors' +import selectors from '../../../selectors/selectors' const mapStateToProps = state => { return { diff --git a/ui/app/components/token-currency-display/index.js b/ui/app/components/ui/token-currency-display/index.js index 6065cae1f..6065cae1f 100644 --- a/ui/app/components/token-currency-display/index.js +++ b/ui/app/components/ui/token-currency-display/index.js diff --git a/ui/app/components/token-currency-display/token-currency-display.component.js b/ui/app/components/ui/token-currency-display/token-currency-display.component.js index 4bb09a4b6..3c2722b36 100644 --- a/ui/app/components/token-currency-display/token-currency-display.component.js +++ b/ui/app/components/ui/token-currency-display/token-currency-display.component.js @@ -1,8 +1,8 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import CurrencyDisplay from '../currency-display/currency-display.component' -import { getTokenData } from '../../helpers/transactions.util' -import { getTokenValue, calcTokenAmount } from '../../token-util' +import CurrencyDisplay from '../currency-display' +import { getTokenData } from '../../../helpers/utils/transactions.util' +import { getTokenValue, calcTokenAmount } from '../../../helpers/utils/token-util' export default class TokenCurrencyDisplay extends PureComponent { static propTypes = { @@ -12,6 +12,7 @@ export default class TokenCurrencyDisplay extends PureComponent { state = { displayValue: '', + suffix: '', } componentDidMount () { @@ -29,25 +30,27 @@ export default class TokenCurrencyDisplay extends PureComponent { setDisplayValue () { const { transactionData: data, token } = this.props - const { decimals = '', symbol = '' } = token + const { decimals = '', symbol: suffix = '' } = token const tokenData = getTokenData(data) let displayValue - if (tokenData.params && tokenData.params.length) { + if (tokenData && tokenData.params && tokenData.params.length) { const tokenValue = getTokenValue(tokenData.params) - const tokenAmount = calcTokenAmount(tokenValue, decimals) - displayValue = `${tokenAmount} ${symbol}` + displayValue = calcTokenAmount(tokenValue, decimals).toString() } - this.setState({ displayValue }) + this.setState({ displayValue, suffix }) } render () { + const { displayValue, suffix } = this.state + return ( <CurrencyDisplay {...this.props} - displayValue={this.state.displayValue} + displayValue={displayValue} + suffix={suffix} /> ) } diff --git a/ui/app/components/token-input/index.js b/ui/app/components/ui/token-input/index.js index 22c06111e..22c06111e 100644 --- a/ui/app/components/token-input/index.js +++ b/ui/app/components/ui/token-input/index.js diff --git a/ui/app/components/token-input/tests/token-input.component.test.js b/ui/app/components/ui/token-input/tests/token-input.component.test.js index 2dacb9bc4..881101880 100644 --- a/ui/app/components/token-input/tests/token-input.component.test.js +++ b/ui/app/components/ui/token-input/tests/token-input.component.test.js @@ -159,6 +159,48 @@ describe('TokenInput Component', () => { assert.equal(wrapper.find('.unit-input__input').props().value, '1') assert.equal(wrapper.find('.currency-display-component').text(), '$462.12USD') }) + + it('should render properly with a token value for fiat, but hideConversion is true', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <TokenInput + value="2710" + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + selectedTokenExchangeRate={2} + showFiat + hideConversion + /> + </Provider>, + { + context: { t }, + childContextTypes: { + t: PropTypes.func, + }, + }, + ) + + assert.ok(wrapper) + const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() + assert.equal(tokenInputInstance.state.decimalValue, 1) + assert.equal(tokenInputInstance.state.hexValue, '2710') + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') + assert.equal(wrapper.find('.unit-input__input').props().value, '1') + assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'translate noConversionRateAvailable') + }) }) describe('handling actions', () => { diff --git a/ui/app/components/ui/token-input/tests/token-input.container.test.js b/ui/app/components/ui/token-input/tests/token-input.container.test.js new file mode 100644 index 000000000..2b1c102c8 --- /dev/null +++ b/ui/app/components/ui/token-input/tests/token-input.container.test.js @@ -0,0 +1,255 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps, mergeProps + +proxyquire('../token-input.container.js', { + 'react-redux': { + connect: (ms, md, mp) => { + mapStateToProps = ms + mergeProps = mp + return () => ({}) + }, + }, +}) + +describe('TokenInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props when send is empty', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x1', + contractExchangeRates: {}, + send: {}, + preferences: { + showFiatInTestnets: false, + }, + provider: { + type: 'mainnet', + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 0, + hideConversion: false, + }) + }) + + it('should return the correct props when selectedTokenAddress is not found and send is populated', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x2', + contractExchangeRates: {}, + send: { + token: { address: 'test' }, + }, + preferences: { + showFiatInTestnets: false, + }, + provider: { + type: 'mainnet', + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: 'test', + }, + selectedTokenExchangeRate: 0, + hideConversion: false, + }) + }) + + it('should return the correct props when contractExchangeRates is populated', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x1', + contractExchangeRates: { + '0x1': 5, + }, + send: {}, + preferences: { + showFiatInTestnets: false, + }, + provider: { + type: 'mainnet', + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 5, + hideConversion: false, + }) + }) + + it('should return the correct props when not in mainnet and showFiatInTestnets is false', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x1', + contractExchangeRates: {}, + send: {}, + preferences: { + showFiatInTestnets: false, + }, + provider: { + type: 'rinkeby', + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 0, + hideConversion: true, + }) + }) + + it('should return the correct props when not in mainnet and showFiatInTestnets is true', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x1', + contractExchangeRates: {}, + send: {}, + preferences: { + showFiatInTestnets: true, + }, + provider: { + type: 'rinkeby', + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 0, + hideConversion: false, + }) + }) + + it('should return the correct props when in mainnet and showFiatInTestnets is true', () => { + const mockState = { + metamask: { + currentCurrency: 'usd', + tokens: [ + { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + ], + selectedTokenAddress: '0x1', + contractExchangeRates: {}, + send: {}, + preferences: { + showFiatInTestnets: true, + }, + provider: { + type: 'mainnet', + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 0, + hideConversion: false, + }) + }) + }) + + describe('mergeProps()', () => { + it('should return the correct props', () => { + const mockStateProps = { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 5, + } + + assert.deepEqual(mergeProps(mockStateProps, {}, {}), { + currentCurrency: 'usd', + selectedToken: { + address: '0x1', + decimals: '4', + symbol: 'ABC', + }, + selectedTokenExchangeRate: 5, + suffix: 'ABC', + }) + }) + }) +}) diff --git a/ui/app/components/token-input/token-input.component.js b/ui/app/components/ui/token-input/token-input.component.js index d1388945b..c28af5fde 100644 --- a/ui/app/components/token-input/token-input.component.js +++ b/ui/app/components/ui/token-input/token-input.component.js @@ -2,10 +2,10 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import UnitInput from '../unit-input' import CurrencyDisplay from '../currency-display' -import { getWeiHexFromDecimalValue } from '../../helpers/conversions.util' +import { getWeiHexFromDecimalValue } from '../../../helpers/utils/conversions.util' import ethUtil from 'ethereumjs-util' -import { conversionUtil, multiplyCurrencies } from '../../conversion-util' -import { ETH } from '../../constants/common' +import { conversionUtil, multiplyCurrencies } from '../../../helpers/utils/conversion-util' +import { ETH } from '../../../helpers/constants/common' /** * Component that allows user to enter token values as a number, and props receive a converted @@ -24,6 +24,7 @@ export default class TokenInput extends PureComponent { value: PropTypes.string, suffix: PropTypes.string, showFiat: PropTypes.bool, + hideConversion: PropTypes.bool, selectedToken: PropTypes.object, selectedTokenExchangeRate: PropTypes.number, } @@ -32,7 +33,7 @@ export default class TokenInput extends PureComponent { super(props) const { value: hexValue } = props - const decimalValue = hexValue ? this.getDecimalValue(props) : 0 + const decimalValue = hexValue ? this.getValue(props) : 0 this.state = { decimalValue, @@ -46,12 +47,12 @@ export default class TokenInput extends PureComponent { const { hexValue: stateHexValue } = this.state if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) { - const decimalValue = this.getDecimalValue(this.props) + const decimalValue = this.getValue(this.props) this.setState({ hexValue: propsHexValue, decimalValue }) } } - getDecimalValue (props) { + getValue (props) { const { value: hexValue, selectedToken: { decimals, symbol } = {} } = props const multiplier = Math.pow(10, Number(decimals || 0)) @@ -63,7 +64,7 @@ export default class TokenInput extends PureComponent { invertConversionRate: true, }) - return Number(decimalValueString) || 0 + return Number(decimalValueString) ? decimalValueString : '' } handleChange = decimalValue => { @@ -81,10 +82,18 @@ export default class TokenInput extends PureComponent { } renderConversionComponent () { - const { selectedTokenExchangeRate, showFiat, currentCurrency } = this.props + const { selectedTokenExchangeRate, showFiat, currentCurrency, hideConversion } = this.props const { decimalValue } = this.state let currency, numberOfDecimals + if (hideConversion) { + return ( + <div className="currency-input__conversion-component"> + { this.context.t('noConversionRateAvailable') } + </div> + ) + } + if (showFiat) { // Display Fiat currency = currentCurrency diff --git a/ui/app/components/token-input/token-input.container.js b/ui/app/components/ui/token-input/token-input.container.js index ec233b1b8..981cb3598 100644 --- a/ui/app/components/token-input/token-input.container.js +++ b/ui/app/components/ui/token-input/token-input.container.js @@ -1,14 +1,17 @@ import { connect } from 'react-redux' import TokenInput from './token-input.component' -import { getSelectedToken, getSelectedTokenExchangeRate } from '../../selectors' +import {getIsMainnet, getSelectedToken, getSelectedTokenExchangeRate, preferencesSelector} from '../../../selectors/selectors' const mapStateToProps = state => { const { metamask: { currentCurrency } } = state + const { showFiatInTestnets } = preferencesSelector(state) + const isMainnet = getIsMainnet(state) return { currentCurrency, selectedToken: getSelectedToken(state), selectedTokenExchangeRate: getSelectedTokenExchangeRate(state), + hideConversion: (!isMainnet && !showFiatInTestnets), } } diff --git a/ui/app/components/tooltip-v2.js b/ui/app/components/ui/tooltip-v2.js index 054782203..b54026794 100644 --- a/ui/app/components/tooltip-v2.js +++ b/ui/app/components/ui/tooltip-v2.js @@ -20,6 +20,7 @@ export default class Tooltip extends PureComponent { arrow: PropTypes.bool, children: PropTypes.node, containerClassName: PropTypes.string, + disabled: PropTypes.bool, onHidden: PropTypes.func, position: PropTypes.oneOf([ 'top', @@ -33,10 +34,11 @@ export default class Tooltip extends PureComponent { title: PropTypes.string, trigger: PropTypes.any, wrapperClassName: PropTypes.string, + style: PropTypes.object, } render () { - const {arrow, children, containerClassName, position, size, title, trigger, onHidden, wrapperClassName } = this.props + const {arrow, children, containerClassName, disabled, position, size, title, trigger, onHidden, wrapperClassName, style } = this.props if (!title) { return ( @@ -50,6 +52,7 @@ export default class Tooltip extends PureComponent { <div className={wrapperClassName}> <ReactTippy className={containerClassName} + disabled={disabled} title={title} position={position} trigger={trigger} @@ -57,6 +60,7 @@ export default class Tooltip extends PureComponent { size={size} arrow={arrow} onHidden={onHidden} + style={style} > {children} </ReactTippy> diff --git a/ui/app/components/tooltip.js b/ui/app/components/ui/tooltip.js index efab2c497..efab2c497 100644 --- a/ui/app/components/tooltip.js +++ b/ui/app/components/ui/tooltip.js diff --git a/ui/app/components/unit-input/index.js b/ui/app/components/ui/unit-input/index.js index 7c33c9e5c..7c33c9e5c 100644 --- a/ui/app/components/unit-input/index.js +++ b/ui/app/components/ui/unit-input/index.js diff --git a/ui/app/components/unit-input/index.scss b/ui/app/components/ui/unit-input/index.scss index 28c5bf6f0..e4075d225 100644 --- a/ui/app/components/unit-input/index.scss +++ b/ui/app/components/ui/unit-input/index.scss @@ -1,4 +1,7 @@ .unit-input { + display: flex; + flex-flow: row nowrap; + align-items: center; min-height: 54px; border: 1px solid #dedede; border-radius: 4px; @@ -24,6 +27,10 @@ display: none; } + &__inputs { + flex: 1 0 auto; + } + &__input { color: #4d4d4d; font-size: 1rem; @@ -38,6 +45,10 @@ align-items: center; } + &__suffix { + margin-left: 3px; + } + &--error { border-color: $red; } diff --git a/ui/app/components/unit-input/tests/unit-input.component.test.js b/ui/app/components/ui/unit-input/tests/unit-input.component.test.js index 97d987bc7..97d987bc7 100644 --- a/ui/app/components/unit-input/tests/unit-input.component.test.js +++ b/ui/app/components/ui/unit-input/tests/unit-input.component.test.js diff --git a/ui/app/components/unit-input/unit-input.component.js b/ui/app/components/ui/unit-input/unit-input.component.js index f1ebf4d77..7b414f177 100644 --- a/ui/app/components/unit-input/unit-input.component.js +++ b/ui/app/components/ui/unit-input/unit-input.component.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' -import { removeLeadingZeroes } from '../send/send.utils' +import { removeLeadingZeroes } from '../../app/send/send.utils' /** * Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also @@ -11,6 +11,7 @@ import { removeLeadingZeroes } from '../send/send.utils' export default class UnitInput extends PureComponent { static propTypes = { children: PropTypes.node, + actionComponent: PropTypes.node, error: PropTypes.bool, onBlur: PropTypes.func, onChange: PropTypes.func, @@ -66,11 +67,11 @@ export default class UnitInput extends PureComponent { const valueString = String(value) const valueLength = valueString.length || 1 const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 - return (valueLength + decimalPointDeficit + 0.75) + 'ch' + return (valueLength + decimalPointDeficit + 0.5) + 'ch' } render () { - const { error, placeholder, suffix, children } = this.props + const { error, placeholder, suffix, actionComponent, children } = this.props const { value } = this.state return ( @@ -78,26 +79,29 @@ export default class UnitInput extends PureComponent { className={classnames('unit-input', { 'unit-input--error': error })} onClick={this.handleFocus} > - <div className="unit-input__input-container"> - <input - type="number" - className="unit-input__input" - value={value} - placeholder={placeholder} - onChange={this.handleChange} - onBlur={this.handleBlur} - style={{ width: this.getInputWidth(value) }} - ref={ref => { this.unitInput = ref }} - /> - { - suffix && ( - <div className="unit-input__suffix"> - { suffix } - </div> - ) - } + <div className="unit-input__inputs"> + <div className="unit-input__input-container"> + <input + type="number" + className="unit-input__input" + value={value} + placeholder={placeholder} + onChange={this.handleChange} + onBlur={this.handleBlur} + style={{ width: this.getInputWidth(value) }} + ref={ref => { this.unitInput = ref }} + /> + { + suffix && ( + <div className="unit-input__suffix"> + { suffix } + </div> + ) + } + </div> + { children } </div> - { children } + {actionComponent} </div> ) } |