diff options
merging upstream branch
Diffstat (limited to 'ui')
156 files changed, 3722 insertions, 1009 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js index eea581d33..501fef76d 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -305,6 +305,12 @@ var actions = { updateFeatureFlags, UPDATE_FEATURE_FLAGS: 'UPDATE_FEATURE_FLAGS', + // Preferences + setPreference, + updatePreferences, + UPDATE_PREFERENCES: 'UPDATE_PREFERENCES', + setUseNativeCurrencyAsPrimaryCurrencyPreference, + setMouseUserState, SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE', @@ -319,6 +325,9 @@ var actions = { clearPendingTokens, createCancelTransaction, + approveProviderRequest, + rejectProviderRequest, + clearApprovedOrigins, } module.exports = actions @@ -1868,10 +1877,10 @@ function updateProviderType (type) { } } -function setRpcTarget (newRpc) { +function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname = '') { return (dispatch) => { - log.debug(`background.setRpcTarget: ${newRpc}`) - background.setCustomRpc(newRpc, (err, result) => { + log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`) + background.setCustomRpc(newRpc, chainId, ticker, nickname, (err, result) => { if (err) { log.error(err) return dispatch(actions.displayWarning('Had a problem changing networks!')) @@ -2298,6 +2307,36 @@ function updateFeatureFlags (updatedFeatureFlags) { } } +function setPreference (preference, value) { + return dispatch => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.setPreference(preference, value, (err, updatedPreferences) => { + dispatch(actions.hideLoadingIndication()) + + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.updatePreferences(updatedPreferences)) + resolve(updatedPreferences) + }) + }) + } +} + +function updatePreferences (value) { + return { + type: actions.UPDATE_PREFERENCES, + value, + } +} + +function setUseNativeCurrencyAsPrimaryCurrencyPreference (value) { + return setPreference('useNativeCurrencyAsPrimaryCurrency', value) +} + function setNetworkNonce (networkNonce) { return { type: actions.SET_NETWORK_NONCE, @@ -2448,3 +2487,21 @@ function setPendingTokens (pendingTokens) { payload: tokens, } } + +function approveProviderRequest (origin) { + return (dispatch) => { + background.approveProviderRequest(origin) + } +} + +function rejectProviderRequest (origin) { + return (dispatch) => { + background.rejectProviderRequest(origin) + } +} + +function clearApprovedOrigins () { + return (dispatch) => { + background.clearApprovedOrigins() + } +} diff --git a/ui/app/app.js b/ui/app/app.js index aeb3d05ee..b3aff1f39 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -101,7 +101,7 @@ class App extends Component { network, isMouseUser, provider, - frequentRpcList, + frequentRpcListDetail, currentView, setMouseUserState, sidebar, @@ -147,7 +147,7 @@ class App extends Component { // network dropdown h(NetworkDropdown, { provider, - frequentRpcList, + frequentRpcListDetail, }, []), h(AccountMenu), @@ -230,7 +230,7 @@ App.propTypes = { alertMessage: PropTypes.string, network: PropTypes.string, provider: PropTypes.object, - frequentRpcList: PropTypes.array, + frequentRpcListDetail: PropTypes.array, currentView: PropTypes.object, sidebar: PropTypes.object, alertOpen: PropTypes.bool, @@ -322,7 +322,7 @@ function mapStateToProps (state) { forgottenPassword: state.appState.forgottenPassword, nextUnreadNotice, lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], currentCurrency: state.metamask.currentCurrency, isMouseUser: state.appState.isMouseUser, betaUI: state.metamask.featureFlags.betaUI, diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 043008a36..06376e48b 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -6,10 +6,11 @@ const genAccountLink = require('etherscan-link').createAccountLink const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem -const Identicon = require('./identicon') const copyToClipboard = require('copy-to-clipboard') const { checksumAddress } = require('../util') +import Identicon from './identicon' + class AccountDropdowns extends Component { constructor (props) { super(props) diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index bcada41e3..94eae8d07 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -7,12 +7,12 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const actions = require('../../actions') const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu') -const Identicon = require('../identicon') -const { formatBalance } = require('../../util') 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, @@ -163,7 +163,6 @@ AccountMenu.prototype.renderAccounts = function () { const isSelected = identity.address === selectedAddress const balanceValue = accounts[address] ? accounts[address].balance : '' - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() const keyring = keyrings.find((kr) => { @@ -189,7 +188,11 @@ AccountMenu.prototype.renderAccounts = function () { h('div.account-menu__account-info', [ h('div.account-menu__name', identity.name || ''), - h('div.account-menu__balance', formattedBalance), + h(UserPreferencedCurrencyDisplay, { + className: 'account-menu__balance', + value: balanceValue, + type: PRIMARY, + }), ]), this.renderKeyringType(keyring), diff --git a/ui/app/components/account-panel.js b/ui/app/components/account-panel.js index abaaf8163..a379ed3ac 100644 --- a/ui/app/components/account-panel.js +++ b/ui/app/components/account-panel.js @@ -1,7 +1,7 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') -const Identicon = require('./identicon') +import Identicon from './identicon' const formatBalance = require('../util').formatBalance const addressSummary = require('../util').addressSummary diff --git a/ui/app/components/add-token-button/add-token-button.component.js b/ui/app/components/add-token-button/add-token-button.component.js new file mode 100644 index 000000000..10887aed8 --- /dev/null +++ b/ui/app/components/add-token-button/add-token-button.component.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types' +import React, {PureComponent} from 'react' + +export default class AddTokenButton extends PureComponent { + static contextTypes = { + t: PropTypes.func.isRequired, + } + + static defaultProps = { + onClick: () => {}, + } + + static propTypes = { + onClick: PropTypes.func, + } + + render () { + const { t } = this.context + const { onClick } = this.props + + return ( + <div className="add-token-button"> + <h1 className="add-token-button__help-header">{t('missingYourTokens')}</h1> + <p className="add-token-button__help-desc">{t('clickToAdd', [t('addToken')])}</p> + <div + className="add-token-button__button" + onClick={onClick} + > + {t('addToken')} + </div> + </div> + ) + } +} diff --git a/ui/app/components/add-token-button/index.js b/ui/app/components/add-token-button/index.js new file mode 100644 index 000000000..15c4fe6ca --- /dev/null +++ b/ui/app/components/add-token-button/index.js @@ -0,0 +1 @@ +export { default } from './add-token-button.component' diff --git a/ui/app/components/add-token-button/index.scss b/ui/app/components/add-token-button/index.scss new file mode 100644 index 000000000..39f404716 --- /dev/null +++ b/ui/app/components/add-token-button/index.scss @@ -0,0 +1,26 @@ +.add-token-button { + display: flex; + flex-direction: column; + color: lighten($scorpion, 25%); + width: 185px; + margin: 36px auto; + text-align: center; + + &__help-header { + font-weight: bold; + font-size: 1rem; + } + + &__help-desc { + font-size: 0.75rem; + margin-top: 1rem; + } + + &__button { + font-size: 0.75rem; + margin: 1rem; + text-transform: uppercase; + color: $curious-blue; + cursor: pointer; + } +} diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js index b8b002dcc..c82dc1de9 100644 --- a/ui/app/components/app-header/app-header.component.js +++ b/ui/app/components/app-header/app-header.component.js @@ -2,13 +2,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') -const Identicon = require('../identicon') const NetworkIndicator = require('../network') export default class AppHeader extends PureComponent { diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js index d63d78c9f..4e2769ee8 100644 --- a/ui/app/components/balance-component.js +++ b/ui/app/components/balance-component.js @@ -2,12 +2,13 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') const inherits = require('util').inherits -const TokenBalance = require('./token-balance') -const Identicon = require('./identicon') -import CurrencyDisplay from './currency-display' -const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors') +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, generateBalanceObject } = require('../util') +const { formatBalance } = require('../util') module.exports = connect(mapStateToProps)(BalanceComponent) @@ -20,6 +21,7 @@ function mapStateToProps (state) { return { account, network, + nativeCurrency: getNativeCurrency(state), conversionRate: conversionRateSelector(state), currentCurrency: getCurrentCurrency(state), assetImages: getAssetImages(state), @@ -65,10 +67,10 @@ BalanceComponent.prototype.renderTokenBalance = function () { BalanceComponent.prototype.renderBalance = function () { const props = this.props - const { shorten, account } = 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) : '...' + const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse, nativeCurrency) : '...' const showFiat = 'showFiat' in props ? props.showFiat : true if (formattedBalance === 'None' || formattedBalance === '...') { @@ -80,25 +82,21 @@ BalanceComponent.prototype.renderBalance = function () { } return h('div.flex-column.balance-display', {}, [ - h('div.token-amount', { - style: {}, - }, this.getTokenBalance(formattedBalance, shorten)), + h(UserPreferencedCurrencyDisplay, { + className: 'token-amount', + value: balanceValue, + type: PRIMARY, + ethNumberOfDecimals: 4, + }), - showFiat && h(CurrencyDisplay, { + showFiat && h(UserPreferencedCurrencyDisplay, { value: balanceValue, + type: SECONDARY, + ethNumberOfDecimals: 4, }), ]) } -BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) { - const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3) - - const balanceValue = shorten ? balanceObj.shortBalance : balanceObj.balance - const label = balanceObj.label - - return `${balanceValue} ${label}` -} - BalanceComponent.prototype.getFiatDisplayNumber = function (formattedBalance, conversionRate) { if (formattedBalance === 'None') return formattedBalance if (conversionRate === 0) return 'N/A' diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js index f0703dde2..c7262d2a9 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js +++ b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js @@ -1,16 +1,19 @@ 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' const ConfirmDetailRow = props => { const { label, - fiatText, - ethText, + primaryText, + secondaryText, onHeaderClick, - fiatTextColor, + primaryValueTextColor, headerText, headerTextClassName, + value, } = props return ( @@ -25,28 +28,57 @@ const ConfirmDetailRow = props => { > { headerText } </div> - <div - className="confirm-detail-row__fiat" - style={{ color: fiatTextColor }} - > - { fiatText } - </div> - <div className="confirm-detail-row__eth"> - { ethText } - </div> + { + primaryText + ? ( + <div + className="confirm-detail-row__primary" + style={{ color: primaryValueTextColor }} + > + { primaryText } + </div> + ) : ( + <UserPreferencedCurrencyDisplay + className="confirm-detail-row__primary" + type={PRIMARY} + value={value} + showEthLogo + ethLogoHeight="18" + style={{ color: primaryValueTextColor }} + hideLabel + /> + ) + } + { + secondaryText + ? ( + <div className="confirm-detail-row__secondary"> + { secondaryText } + </div> + ) : ( + <UserPreferencedCurrencyDisplay + className="confirm-detail-row__secondary" + type={SECONDARY} + value={value} + showEthLogo + hideLabel + /> + ) + } </div> </div> ) } ConfirmDetailRow.propTypes = { - label: PropTypes.string, - fiatText: PropTypes.string, - ethText: PropTypes.string, - fiatTextColor: PropTypes.string, - onHeaderClick: PropTypes.func, headerText: PropTypes.string, headerTextClassName: PropTypes.string, + label: PropTypes.string, + onHeaderClick: PropTypes.func, + primaryValueTextColor: PropTypes.string, + primaryText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + secondaryText: PropTypes.string, + value: PropTypes.string, } export default ConfirmDetailRow diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss index dd6f87c17..580a41fde 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/index.scss +++ b/ui/app/components/confirm-page-container/confirm-detail-row/index.scss @@ -18,18 +18,14 @@ min-width: 0; } - &__fiat { + &__primary { font-size: 1.5rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + justify-content: flex-end; } - &__eth { + &__secondary { color: $oslo-gray; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + justify-content: flex-end; } &__header-text { diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js b/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js index 6f2489071..c8507985d 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js +++ b/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js @@ -12,17 +12,19 @@ describe('Confirm Detail Row Component', function () { let wrapper beforeEach(() => { - wrapper = shallow(<ConfirmDetailRow - errorType={'mockErrorType'} - label={'mockLabel'} - showError={false} - fiatText = {'mockFiatText'} - ethText = {'mockEthText'} - fiatTextColor= {'mockColor'} - onHeaderClick= {propsMethodSpies.onHeaderClick} - headerText = {'mockHeaderText'} - headerTextClassName = {'mockHeaderClass'} - />) + wrapper = shallow( + <ConfirmDetailRow + errorType={'mockErrorType'} + label={'mockLabel'} + showError={false} + primaryText = {'mockFiatText'} + secondaryText = {'mockEthText'} + primaryValueTextColor= {'mockColor'} + onHeaderClick= {propsMethodSpies.onHeaderClick} + headerText = {'mockHeaderText'} + headerTextClassName = {'mockHeaderClass'} + /> + ) }) describe('render', () => { @@ -38,16 +40,16 @@ describe('Confirm Detail Row Component', function () { assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__header-text').childAt(0).text(), 'mockHeaderText') }) - it('should render the fiatText as a child of the confirm-detail-row__fiat', () => { - assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__fiat').childAt(0).text(), 'mockFiatText') + it('should render the primaryText as a child of the confirm-detail-row__primary', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__primary').childAt(0).text(), 'mockFiatText') }) - it('should render the ethText as a child of the confirm-detail-row__eth', () => { - assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__eth').childAt(0).text(), 'mockEthText') + it('should render the ethText as a child of the confirm-detail-row__secondary', () => { + assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__secondary').childAt(0).text(), 'mockEthText') }) - it('should set the fiatTextColor on confirm-detail-row__fiat', () => { - assert.equal(wrapper.find('.confirm-detail-row__fiat').props().style.color, 'mockColor') + it('should set the fiatTextColor on confirm-detail-row__primary', () => { + assert.equal(wrapper.find('.confirm-detail-row__primary').props().style.color, 'mockColor') }) it('should assure the confirm-detail-row__header-text classname is correct', () => { @@ -58,7 +60,5 @@ describe('Confirm Detail Row Component', function () { wrapper.find('.confirm-detail-row__header-text').props().onClick() assert.equal(assert.equal(propsMethodSpies.onHeaderClick.callCount, 1)) }) - - }) }) diff --git a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index 74e95ece6..1dca81560 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/app/components/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -17,9 +17,10 @@ export default class ConfirmPageContainerContent extends Component { nonce: PropTypes.string, assetImage: PropTypes.string, subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + subtitleComponent: PropTypes.node, summaryComponent: PropTypes.node, title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - titleComponent: PropTypes.func, + titleComponent: PropTypes.node, warning: PropTypes.string, } @@ -54,7 +55,9 @@ export default class ConfirmPageContainerContent extends Component { errorKey, errorMessage, title, + titleComponent, subtitle, + subtitleComponent, hideSubtitle, identiconAddress, nonce, @@ -80,7 +83,9 @@ export default class ConfirmPageContainerContent extends Component { })} action={action} title={title} + titleComponent={titleComponent} subtitle={subtitle} + subtitleComponent={subtitleComponent} hideSubtitle={hideSubtitle} identiconAddress={identiconAddress} nonce={nonce} 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/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js index 38b158fd3..89ceb015f 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/confirm-page-container/confirm-page-container-content/confirm-page-container-summary/confirm-page-container-summary.component.js @@ -4,7 +4,18 @@ import classnames from 'classnames' import Identicon from '../../../identicon' const ConfirmPageContainerSummary = props => { - const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce, assetImage } = props + const { + action, + title, + titleComponent, + subtitle, + subtitleComponent, + hideSubtitle, + className, + identiconAddress, + nonce, + assetImage, + } = props return ( <div className={classnames('confirm-page-container-summary', className)}> @@ -32,12 +43,12 @@ const ConfirmPageContainerSummary = props => { ) } <div className="confirm-page-container-summary__title-text"> - { title } + { titleComponent || title } </div> </div> { hideSubtitle || <div className="confirm-page-container-summary__subtitle"> - { subtitle } + { subtitleComponent || subtitle } </div> } </div> @@ -47,7 +58,9 @@ const ConfirmPageContainerSummary = props => { ConfirmPageContainerSummary.propTypes = { action: PropTypes.string, title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + titleComponent: PropTypes.node, subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + subtitleComponent: PropTypes.node, hideSubtitle: PropTypes.bool, className: PropTypes.string, identiconAddress: PropTypes.string, diff --git a/ui/app/components/confirm-page-container/confirm-page-container.component.js b/ui/app/components/confirm-page-container/confirm-page-container.component.js index 36d5a1f58..8b2e47cbb 100644 --- a/ui/app/components/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/confirm-page-container/confirm-page-container.component.js @@ -16,8 +16,9 @@ export default class ConfirmPageContainer extends Component { onEdit: PropTypes.func, showEdit: PropTypes.bool, subtitle: PropTypes.string, + subtitleComponent: PropTypes.node, title: PropTypes.string, - titleComponent: PropTypes.func, + titleComponent: PropTypes.node, // Sender to Recipient fromAddress: PropTypes.string, fromName: PropTypes.string, @@ -65,6 +66,7 @@ export default class ConfirmPageContainer extends Component { title, titleComponent, subtitle, + subtitleComponent, hideSubtitle, summaryComponent, detailsComponent, @@ -101,6 +103,7 @@ export default class ConfirmPageContainer extends Component { title={title} titleComponent={titleComponent} subtitle={subtitle} + subtitleComponent={subtitleComponent} hideSubtitle={hideSubtitle} summaryComponent={summaryComponent} detailsComponent={detailsComponent} diff --git a/ui/app/components/currency-display/currency-display.component.js b/ui/app/components/currency-display/currency-display.component.js index e4eb58a2a..2d7413b57 100644 --- a/ui/app/components/currency-display/currency-display.component.js +++ b/ui/app/components/currency-display/currency-display.component.js @@ -1,14 +1,18 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' -import { ETH, GWEI } from '../../constants/common' +import classnames from 'classnames' +import { GWEI } from '../../constants/common' export default class CurrencyDisplay extends PureComponent { static propTypes = { className: PropTypes.string, displayValue: PropTypes.string, prefix: PropTypes.string, + prefixComponent: PropTypes.node, + style: PropTypes.object, + suffix: PropTypes.string, // Used in container - currency: PropTypes.oneOf([ETH]), + currency: PropTypes.string, denomination: PropTypes.oneOf([GWEI]), value: PropTypes.string, numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), @@ -16,15 +20,25 @@ export default class CurrencyDisplay extends PureComponent { } render () { - const { className, displayValue, prefix } = this.props + const { className, displayValue, prefix, prefixComponent, style, suffix } = this.props const text = `${prefix || ''}${displayValue}` + const title = `${text} ${suffix}` return ( <div - className={className} - title={text} + className={classnames('currency-display-component', className)} + style={style} + title={title} > - { text } + { prefixComponent} + <span className="currency-display-component__text">{ text }</span> + { + suffix && ( + <span className="currency-display-component__suffix"> + { suffix } + </span> + ) + } </div> ) } diff --git a/ui/app/components/currency-display/currency-display.container.js b/ui/app/components/currency-display/currency-display.container.js index 6644a1099..6ddf07172 100644 --- a/ui/app/components/currency-display/currency-display.container.js +++ b/ui/app/components/currency-display/currency-display.container.js @@ -2,20 +2,41 @@ import { connect } from 'react-redux' import CurrencyDisplay from './currency-display.component' import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util' -const mapStateToProps = (state, ownProps) => { - const { value, numberOfDecimals = 2, currency, denomination, hideLabel } = ownProps - const { metamask: { currentCurrency, conversionRate } } = state +const mapStateToProps = state => { + const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state + + return { + currentCurrency, + conversionRate, + nativeCurrency, + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { nativeCurrency, currentCurrency, conversionRate, ...restStateProps } = stateProps + const { + value, + numberOfDecimals = 2, + currency, + denomination, + hideLabel, + ...restOwnProps + } = ownProps const toCurrency = currency || currentCurrency const convertedValue = getValueFromWeiHex({ - value, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination, + value, fromCurrency: nativeCurrency, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination, }) - const formattedValue = formatCurrency(convertedValue, toCurrency) - const displayValue = hideLabel ? formattedValue : `${formattedValue} ${toCurrency.toUpperCase()}` + const displayValue = formatCurrency(convertedValue, toCurrency) + const suffix = hideLabel ? undefined : toCurrency.toUpperCase() return { + ...restStateProps, + ...dispatchProps, + ...restOwnProps, displayValue, + suffix, } } -export default connect(mapStateToProps)(CurrencyDisplay) +export default connect(mapStateToProps, null, mergeProps)(CurrencyDisplay) diff --git a/ui/app/components/currency-display/index.scss b/ui/app/components/currency-display/index.scss new file mode 100644 index 000000000..313c932b8 --- /dev/null +++ b/ui/app/components/currency-display/index.scss @@ -0,0 +1,14 @@ +.currency-display-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/currency-display/tests/currency-display.container.test.js b/ui/app/components/currency-display/tests/currency-display.container.test.js index 5265bbb04..0c886af50 100644 --- a/ui/app/components/currency-display/tests/currency-display.container.test.js +++ b/ui/app/components/currency-display/tests/currency-display.container.test.js @@ -1,12 +1,13 @@ import assert from 'assert' import proxyquire from 'proxyquire' -let mapStateToProps +let mapStateToProps, mergeProps proxyquire('../currency-display.container.js', { 'react-redux': { - connect: ms => { + connect: (ms, md, mp) => { mapStateToProps = ms + mergeProps = mp return () => ({}) }, }, @@ -19,86 +20,125 @@ describe('CurrencyDisplay container', () => { 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 tests = [ { props: { value: '0x2386f26fc10000', numberOfDecimals: 2, currency: 'usd', + nativeCurrency: 'ETH', }, result: { - displayValue: '$2.80 USD', + displayValue: '$2.80', + suffix: 'USD', + nativeCurrency: 'ETH', }, }, { props: { value: '0x2386f26fc10000', + currency: 'usd', + nativeCurrency: 'ETH', }, result: { - displayValue: '$2.80 USD', + displayValue: '$2.80', + suffix: 'USD', + nativeCurrency: 'ETH', }, }, { props: { value: '0x1193461d01595930', currency: 'ETH', + nativeCurrency: 'ETH', numberOfDecimals: 3, }, result: { - displayValue: '1.266 ETH', + displayValue: '1.266', + suffix: 'ETH', + nativeCurrency: 'ETH', }, }, { props: { value: '0x1193461d01595930', currency: 'ETH', + nativeCurrency: 'ETH', numberOfDecimals: 3, hideLabel: true, }, result: { + nativeCurrency: 'ETH', displayValue: '1.266', + suffix: undefined, }, }, { props: { value: '0x3b9aca00', currency: 'ETH', + nativeCurrency: 'ETH', denomination: 'GWEI', hideLabel: true, }, result: { + nativeCurrency: 'ETH', displayValue: '1', + suffix: undefined, }, }, { props: { value: '0x3b9aca00', currency: 'ETH', + nativeCurrency: 'ETH', denomination: 'WEI', hideLabel: true, }, result: { + nativeCurrency: 'ETH', displayValue: '1000000000', + suffix: undefined, }, }, { props: { value: '0x3b9aca00', currency: 'ETH', + nativeCurrency: 'ETH', numberOfDecimals: 100, hideLabel: true, }, result: { + nativeCurrency: 'ETH', displayValue: '1e-9', + suffix: undefined, }, }, ] tests.forEach(({ props, result }) => { - assert.deepEqual(mapStateToProps(mockState, props), result) + assert.deepEqual(mergeProps(mockStateProps, {}, { ...props }), result) }) }) }) diff --git a/ui/app/components/currency-input/currency-input.component.js b/ui/app/components/currency-input/currency-input.component.js new file mode 100644 index 000000000..0761a75c5 --- /dev/null +++ b/ui/app/components/currency-input/currency-input.component.js @@ -0,0 +1,121 @@ +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' + +/** + * Component that allows user to enter currency values as a number, and props receive a converted + * hex value in WEI. props.value, used as a default or forced value, should be a hex value, which + * gets converted into a decimal value depending on the currency (ETH or Fiat). + */ +export default class CurrencyInput extends PureComponent { + static propTypes = { + conversionRate: PropTypes.number, + currentCurrency: PropTypes.string, + nativeCurrency: PropTypes.string, + onChange: PropTypes.func, + onBlur: PropTypes.func, + suffix: PropTypes.string, + useFiat: PropTypes.bool, + value: PropTypes.string, + } + + constructor (props) { + super(props) + + const { value: hexValue } = props + const decimalValue = hexValue ? this.getDecimalValue(props) : 0 + + this.state = { + decimalValue, + hexValue, + } + } + + componentDidUpdate (prevProps) { + const { value: prevPropsHexValue } = prevProps + const { value: propsHexValue } = this.props + const { hexValue: stateHexValue } = this.state + + if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) { + const decimalValue = this.getDecimalValue(this.props) + this.setState({ hexValue: propsHexValue, decimalValue }) + } + } + + getDecimalValue (props) { + const { value: hexValue, useFiat, currentCurrency, conversionRate } = props + const decimalValueString = useFiat + ? getValueFromWeiHex({ + value: hexValue, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2, + }) + : getValueFromWeiHex({ + value: hexValue, toCurrency: ETH, numberOfDecimals: 6, + }) + + return Number(decimalValueString) || 0 + } + + handleChange = decimalValue => { + const { useFiat, currentCurrency: fromCurrency, conversionRate, onChange } = this.props + + const hexValue = useFiat + ? getWeiHexFromDecimalValue({ + value: decimalValue, fromCurrency, conversionRate, invertConversionRate: true, + }) + : getWeiHexFromDecimalValue({ + value: decimalValue, fromCurrency: ETH, fromDenomination: ETH, conversionRate, + }) + + this.setState({ hexValue, decimalValue }) + onChange(hexValue) + } + + handleBlur = () => { + this.props.onBlur(this.state.hexValue) + } + + renderConversionComponent () { + const { useFiat, currentCurrency, nativeCurrency } = this.props + const { hexValue } = this.state + let currency, numberOfDecimals + + if (useFiat) { + // Display ETH + currency = nativeCurrency || ETH + numberOfDecimals = 6 + } else { + // Display Fiat + currency = currentCurrency + numberOfDecimals = 2 + } + + return ( + <CurrencyDisplay + className="currency-input__conversion-component" + currency={currency} + value={hexValue} + numberOfDecimals={numberOfDecimals} + /> + ) + } + + render () { + const { suffix, ...restProps } = this.props + const { decimalValue } = this.state + + return ( + <UnitInput + {...restProps} + suffix={suffix} + onChange={this.handleChange} + onBlur={this.handleBlur} + value={decimalValue} + > + { this.renderConversionComponent() } + </UnitInput> + ) + } +} diff --git a/ui/app/components/currency-input/currency-input.container.js b/ui/app/components/currency-input/currency-input.container.js new file mode 100644 index 000000000..1d1ed7b41 --- /dev/null +++ b/ui/app/components/currency-input/currency-input.container.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux' +import CurrencyInput from './currency-input.component' +import { ETH } from '../../constants/common' + +const mapStateToProps = state => { + const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state + + return { + nativeCurrency, + currentCurrency, + conversionRate, + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { nativeCurrency, currentCurrency } = stateProps + const { useFiat } = ownProps + const suffix = useFiat ? currentCurrency.toUpperCase() : nativeCurrency || ETH + + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + suffix, + } +} + +export default connect(mapStateToProps, null, mergeProps)(CurrencyInput) diff --git a/ui/app/components/currency-input/index.js b/ui/app/components/currency-input/index.js new file mode 100644 index 000000000..d8069fb67 --- /dev/null +++ b/ui/app/components/currency-input/index.js @@ -0,0 +1 @@ +export { default } from './currency-input.container' diff --git a/ui/app/components/currency-input/index.scss b/ui/app/components/currency-input/index.scss new file mode 100644 index 000000000..fcb2db461 --- /dev/null +++ b/ui/app/components/currency-input/index.scss @@ -0,0 +1,7 @@ +.currency-input { + &__conversion-component { + font-size: 12px; + line-height: 12px; + padding-left: 1px; + } +} diff --git a/ui/app/components/currency-input/tests/currency-input.component.test.js b/ui/app/components/currency-input/tests/currency-input.component.test.js new file mode 100644 index 000000000..a33889f94 --- /dev/null +++ b/ui/app/components/currency-input/tests/currency-input.component.test.js @@ -0,0 +1,251 @@ +import React from 'react' +import assert from 'assert' +import { shallow, mount } from 'enzyme' +import sinon from 'sinon' +import { Provider } from 'react-redux' +import configureMockStore from 'redux-mock-store' +import CurrencyInput from '../currency-input.component' +import UnitInput from '../../unit-input' +import CurrencyDisplay from '../../currency-display' + +describe('CurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly without a suffix', () => { + const wrapper = shallow( + <CurrencyInput /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(UnitInput).length, 1) + }) + + it('should render properly with a suffix', () => { + const mockStore = { + metamask: { + nativeCurrency: 'ETH', + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <CurrencyInput + suffix="ETH" + nativeCurrency="ETH" + /> + </Provider> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + }) + + it('should render properly with an ETH value', () => { + const mockStore = { + metamask: { + nativeCurrency: 'ETH', + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <CurrencyInput + value="de0b6b3a7640000" + suffix="ETH" + nativeCurrency="ETH" + currentCurrency="usd" + conversionRate={231.06} + /> + </Provider> + ) + + assert.ok(wrapper) + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000') + 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, '1') + assert.equal(wrapper.find('.currency-display-component').text(), '$231.06USD') + }) + + it('should render properly with a fiat value', () => { + const mockStore = { + metamask: { + nativeCurrency: 'ETH', + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <CurrencyInput + value="f602f2234d0ea" + suffix="USD" + useFiat + nativeCurrency="ETH" + currentCurrency="usd" + conversionRate={231.06} + /> + </Provider> + ) + + assert.ok(wrapper) + const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance() + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea') + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'USD') + assert.equal(wrapper.find('.unit-input__input').props().value, '1') + assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH') + }) + }) + + describe('handling actions', () => { + const handleChangeSpy = sinon.spy() + const handleBlurSpy = sinon.spy() + + afterEach(() => { + handleChangeSpy.resetHistory() + handleBlurSpy.resetHistory() + }) + + it('should call onChange and onBlur on input changes with the hex value for ETH', () => { + 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} + suffix="ETH" + 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')) + }) + + it('should call onChange and onBlur on input changes with the hex value for fiat', () => { + 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} + suffix="USD" + nativeCurrency="ETH" + currentCurrency="usd" + conversionRate={231.06} + useFiat + /> + </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(), '0ETH') + 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('f602f2234d0ea')) + assert.equal(wrapper.find('.currency-display-component').text(), '0.004328ETH') + assert.equal(currencyInputInstance.state.decimalValue, 1) + assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('f602f2234d0ea')) + }) + + it('should change the state and pass in a new decimalValue when props.value changes', () => { + const mockStore = { + metamask: { + nativeCurrency: 'ETH', + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = shallow( + <Provider store={store}> + <CurrencyInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + suffix="USD" + nativeCurrency="ETH" + currentCurrency="usd" + conversionRate={231.06} + useFiat + /> + </Provider> + ) + + assert.ok(wrapper) + const currencyInputInstance = wrapper.find(CurrencyInput).dive() + assert.equal(currencyInputInstance.state('decimalValue'), 0) + assert.equal(currencyInputInstance.state('hexValue'), undefined) + assert.equal(currencyInputInstance.find(UnitInput).props().value, 0) + + currencyInputInstance.setProps({ value: '1ec05e43e72400' }) + currencyInputInstance.update() + assert.equal(currencyInputInstance.state('decimalValue'), 2) + assert.equal(currencyInputInstance.state('hexValue'), '1ec05e43e72400') + assert.equal(currencyInputInstance.find(UnitInput).props().value, 2) + }) + }) +}) 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 new file mode 100644 index 000000000..5d72958e6 --- /dev/null +++ b/ui/app/components/currency-input/tests/currency-input.container.test.js @@ -0,0 +1,60 @@ +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/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index b497f5c09..e6b3e0c0c 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -6,7 +6,7 @@ const genAccountLink = require('../../../../lib/account-link.js') const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem -const Identicon = require('../../identicon') +import Identicon from '../../identicon' const { checksumAddress } = require('../../../util') const copyToClipboard = require('copy-to-clipboard') const { formatBalance } = require('../../../util') @@ -26,14 +26,14 @@ class AccountDropdowns extends Component { } renderAccounts () { - const { identities, accounts, selected, menuItemStyles, actions, keyrings } = this.props + const { identities, accounts, selected, menuItemStyles, actions, keyrings, ticker } = this.props return Object.keys(identities).map((key, index) => { const identity = identities[key] const isSelected = identity.address === selected const balanceValue = accounts[key].balance - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' + const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, true, ticker) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() const keyring = keyrings.find((kr) => { @@ -421,6 +421,7 @@ AccountDropdowns.propTypes = { network: PropTypes.number, // actions.showExportPrivateKeyModal: , style: PropTypes.object, + ticker: PropTypes.string, enableAccountsSelector: PropTypes.bool, enableAccountOption: PropTypes.bool, enableAccountOptions: PropTypes.bool, @@ -458,6 +459,7 @@ const mapDispatchToProps = (dispatch) => { function mapStateToProps (state) { return { + ticker: state.metamask.ticker, keyrings: state.metamask.keyrings, sidebarOpen: state.appState.sidebar.isOpen, } diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js index b252b25d9..d4cc695a6 100644 --- a/ui/app/components/dropdowns/network-dropdown.js +++ b/ui/app/components/dropdowns/network-dropdown.js @@ -24,8 +24,9 @@ const notToggleElementClassnames = [ function mapStateToProps (state) { return { provider: state.metamask.provider, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], networkDropdownOpen: state.appState.networkDropdownOpen, + network: state.metamask.network, } } @@ -40,8 +41,8 @@ function mapDispatchToProps (dispatch) { setDefaultRpcTarget: type => { dispatch(actions.setDefaultRpcTarget(type)) }, - setRpcTarget: (target) => { - dispatch(actions.setRpcTarget(target)) + setRpcTarget: (target, network, ticker, nickname) => { + dispatch(actions.setRpcTarget(target, network, ticker, nickname)) }, delRpcTarget: (target) => { dispatch(actions.delRpcTarget(target)) @@ -71,7 +72,7 @@ module.exports = compose( NetworkDropdown.prototype.render = function () { const props = this.props const { provider: { type: providerType, rpcTarget: activeNetwork } } = props - const rpcList = props.frequentRpcList + const rpcListDetail = props.frequentRpcListDetail const isOpen = this.props.networkDropdownOpen const dropdownMenuItemStyle = { fontSize: '16px', @@ -225,7 +226,7 @@ NetworkDropdown.prototype.render = function () { ), this.renderCustomOption(props.provider), - this.renderCommonRpc(rpcList, props.provider), + this.renderCommonRpc(rpcListDetail, props.provider), h( DropdownMenuItem, @@ -267,28 +268,33 @@ NetworkDropdown.prototype.getNetworkName = function () { } else if (providerName === 'rinkeby') { name = this.context.t('rinkeby') } else { - name = this.context.t('unknownNetwork') + name = provider.nickname || this.context.t('unknownNetwork') } return name } -NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { +NetworkDropdown.prototype.renderCommonRpc = function (rpcListDetail, provider) { const props = this.props - const reversedRpcList = rpcList.slice().reverse() + const reversedRpcListDetail = rpcListDetail.slice().reverse() + const network = props.network - return reversedRpcList.map((rpc) => { + return reversedRpcListDetail.map((entry) => { + const rpc = entry.rpcUrl + const ticker = entry.ticker || 'ETH' + const nickname = entry.nickname || '' const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget if ((rpc === 'http://localhost:8545') || currentRpcTarget) { return null } else { + const chainId = entry.chainId || network return h( DropdownMenuItem, { key: `common${rpc}`, closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => props.setRpcTarget(rpc), + onClick: () => props.setRpcTarget(rpc, chainId, ticker, nickname), style: { fontSize: '16px', lineHeight: '20px', @@ -302,7 +308,7 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { style: { color: currentRpcTarget ? '#ffffff' : '#9b9b9b', }, - }, rpc), + }, nickname || rpc), h('i.fa.fa-times.delete', { onClick: (e) => { @@ -317,8 +323,9 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { } NetworkDropdown.prototype.renderCustomOption = function (provider) { - const { rpcTarget, type } = provider + const { rpcTarget, type, ticker, nickname } = provider const props = this.props + const network = props.network if (type !== 'rpc') return null @@ -332,7 +339,7 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) { DropdownMenuItem, { key: rpcTarget, - onClick: () => props.setRpcTarget(rpcTarget), + onClick: () => props.setRpcTarget(rpcTarget, network, ticker, nickname), closeMenu: () => this.props.hideNetworkDropdown(), style: { fontSize: '16px', @@ -347,7 +354,7 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) { style: { color: '#ffffff', }, - }, rpcTarget), + }, nickname || rpcTarget), ] ) } diff --git a/ui/app/components/dropdowns/tests/network-dropdown.test.js b/ui/app/components/dropdowns/tests/network-dropdown.test.js index 699b54605..88ad56851 100644 --- a/ui/app/components/dropdowns/tests/network-dropdown.test.js +++ b/ui/app/components/dropdowns/tests/network-dropdown.test.js @@ -45,8 +45,8 @@ describe('Network Dropdown', () => { provider: { 'type': 'test', }, - frequentRpcList: [ - 'http://localhost:7545', + frequentRpcListDetail: [ + { rpcUrl: 'http://localhost:7545' }, ], }, appState: { diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js index c3d084bdc..2f6395a2d 100644 --- a/ui/app/components/eth-balance.js +++ b/ui/app/components/eth-balance.js @@ -1,5 +1,6 @@ const { Component } = require('react') const h = require('react-hyperscript') +const connect = require('react-redux').connect const { inherits } = require('util') const { formatBalance, @@ -8,7 +9,12 @@ const { const Tooltip = require('./tooltip.js') const FiatValue = require('./fiat-value.js') -module.exports = EthBalanceComponent +module.exports = connect(mapStateToProps)(EthBalanceComponent) +function mapStateToProps (state) { + return { + ticker: state.metamask.ticker, + } +} inherits(EthBalanceComponent, Component) function EthBalanceComponent () { @@ -17,9 +23,9 @@ function EthBalanceComponent () { EthBalanceComponent.prototype.render = function () { const props = this.props - const { value, style, width, needsParse = true } = props + const { ticker, value, style, width, needsParse = true } = props - const formattedValue = value ? formatBalance(value, 6, needsParse) : '...' + const formattedValue = value ? formatBalance(value, 6, needsParse, ticker) : '...' return ( diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js deleted file mode 100644 index 7bd921892..000000000 --- a/ui/app/components/identicon.js +++ /dev/null @@ -1,124 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const connect = require('react-redux').connect -const isNode = require('detect-node') -const findDOMNode = require('react-dom').findDOMNode -const jazzicon = require('jazzicon') -const iconFactoryGen = require('../../lib/icon-factory') -const iconFactory = iconFactoryGen(jazzicon) -const { toDataUrl } = require('../../lib/blockies') - -module.exports = connect(mapStateToProps)(IdenticonComponent) - -inherits(IdenticonComponent, Component) -function IdenticonComponent () { - Component.call(this) - - this.defaultDiameter = 46 -} - -function mapStateToProps (state) { - return { - useBlockie: state.metamask.useBlockie, - } -} - -IdenticonComponent.prototype.render = function () { - var props = this.props - const { className = '', address, image } = props - var diameter = props.diameter || this.defaultDiameter - const style = { - height: diameter, - width: diameter, - borderRadius: diameter / 2, - } - if (image) { - return h('img', { - className: `${className} identicon`, - src: image, - style: { - ...style, - }, - }) - } else if (address) { - return h('div', { - className: `${className} identicon`, - key: 'identicon-' + address, - style: { - display: 'flex', - flexShrink: 0, - alignItems: 'center', - justifyContent: 'center', - ...style, - overflow: 'hidden', - }, - }) - } else { - return h('img.balance-icon', { - className, - src: './images/eth_logo.svg', - style: { - ...style, - }, - }) - } -} - -IdenticonComponent.prototype.componentDidMount = function () { - var props = this.props - const { address, useBlockie } = props - - if (!address) return - - if (!isNode) { - // eslint-disable-next-line react/no-find-dom-node - var container = findDOMNode(this) - - const diameter = props.diameter || this.defaultDiameter - - if (useBlockie) { - _generateBlockie(container, address, diameter) - } else { - _generateJazzicon(container, address, diameter) - } - } -} - -IdenticonComponent.prototype.componentDidUpdate = function () { - var props = this.props - const { address, useBlockie } = props - - if (!address) return - - if (!isNode) { - // eslint-disable-next-line react/no-find-dom-node - var container = findDOMNode(this) - - var children = container.children - for (var i = 0; i < children.length; i++) { - container.removeChild(children[i]) - } - - const diameter = props.diameter || this.defaultDiameter - - if (useBlockie) { - _generateBlockie(container, address, diameter) - } else { - _generateJazzicon(container, address, diameter) - } - } -} - -function _generateBlockie (container, address, diameter) { - const img = new Image() - img.src = toDataUrl(address) - img.height = diameter - img.width = diameter - container.appendChild(img) -} - -function _generateJazzicon (container, address, diameter) { - const img = iconFactory.iconForAddress(address, diameter) - container.appendChild(img) -} diff --git a/ui/app/components/identicon/identicon.component.js b/ui/app/components/identicon/identicon.component.js new file mode 100644 index 000000000..b892e5ae5 --- /dev/null +++ b/ui/app/components/identicon/identicon.component.js @@ -0,0 +1,99 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { toDataUrl } from '../../../lib/blockies' +import contractMap from 'eth-contract-metadata' +import { checksumAddress } from '../../../app/util' +import Jazzicon from '../jazzicon' + +const getStyles = diameter => ( + { + height: diameter, + width: diameter, + borderRadius: diameter / 2, + } +) + +export default class Identicon extends PureComponent { + static propTypes = { + address: PropTypes.string, + className: PropTypes.string, + diameter: PropTypes.number, + image: PropTypes.string, + useBlockie: PropTypes.bool, + } + + static defaultProps = { + diameter: 46, + } + + renderImage () { + const { className, diameter, image } = this.props + + return ( + <img + className={classnames('identicon', className)} + src={image} + style={getStyles(diameter)} + /> + ) + } + + renderJazzicon () { + const { address, className, diameter } = this.props + + return ( + <Jazzicon + address={address} + diameter={diameter} + className={classnames('identicon', className)} + style={getStyles(diameter)} + /> + ) + } + + renderBlockie () { + const { address, className, diameter } = this.props + + return ( + <div + className={classnames('identicon', className)} + style={getStyles(diameter)} + > + <img + src={toDataUrl(address)} + height={diameter} + width={diameter} + /> + </div> + ) + } + + render () { + const { className, address, image, diameter, useBlockie } = this.props + + if (image) { + return this.renderImage() + } + + if (address) { + const checksummedAddress = checksumAddress(address) + + if (contractMap[checksummedAddress] && contractMap[checksummedAddress].logo) { + return this.renderJazzicon() + } + + return useBlockie + ? this.renderBlockie() + : this.renderJazzicon() + } + + return ( + <img + className={classnames('balance-icon', className)} + src="./images/eth_logo.svg" + style={getStyles(diameter)} + /> + ) + } +} diff --git a/ui/app/components/identicon/identicon.container.js b/ui/app/components/identicon/identicon.container.js new file mode 100644 index 000000000..bc49bc18e --- /dev/null +++ b/ui/app/components/identicon/identicon.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import Identicon from './identicon.component' + +const mapStateToProps = state => { + const { metamask: { useBlockie } } = state + + return { + useBlockie, + } +} + +export default connect(mapStateToProps)(Identicon) diff --git a/ui/app/components/identicon/index.js b/ui/app/components/identicon/index.js new file mode 100644 index 000000000..799c886f2 --- /dev/null +++ b/ui/app/components/identicon/index.js @@ -0,0 +1 @@ +export { default } from './identicon.container' diff --git a/ui/app/components/identicon/index.scss b/ui/app/components/identicon/index.scss new file mode 100644 index 000000000..657afc48f --- /dev/null +++ b/ui/app/components/identicon/index.scss @@ -0,0 +1,7 @@ +.identicon { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + overflow: hidden; +} diff --git a/ui/app/components/identicon/tests/identicon.component.test.js b/ui/app/components/identicon/tests/identicon.component.test.js new file mode 100644 index 000000000..2944818f5 --- /dev/null +++ b/ui/app/components/identicon/tests/identicon.component.test.js @@ -0,0 +1,51 @@ +import React from 'react' +import assert from 'assert' +import thunk from 'redux-thunk' +import configureMockStore from 'redux-mock-store' +import { mount } from 'enzyme' +import Identicon from '../identicon.component' + +describe('Identicon', () => { + const state = { + metamask: { + useBlockie: false, + }, + } + + const middlewares = [thunk] + const mockStore = configureMockStore(middlewares) + const store = mockStore(state) + + it('renders default eth_logo identicon with no props', () => { + const wrapper = mount( + <Identicon store={store}/> + ) + + assert.equal(wrapper.find('img.balance-icon').prop('src'), './images/eth_logo.svg') + }) + + it('renders custom image and add className props', () => { + const wrapper = mount( + <Identicon + store={store} + className="test-image" + image="test-image" + /> + ) + + assert.equal(wrapper.find('img.test-image').prop('className'), 'identicon test-image') + assert.equal(wrapper.find('img.test-image').prop('src'), 'test-image') + }) + + it('renders div with address prop', () => { + const wrapper = mount( + <Identicon + store={store} + className="test-address" + address="0xTest" + /> + ) + + assert.equal(wrapper.find('div.test-address').prop('className'), 'identicon test-address') + }) +}) diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss index 21b65bf55..e27b0f182 100644 --- a/ui/app/components/index.scss +++ b/ui/app/components/index.scss @@ -1,15 +1,23 @@ @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'; @@ -24,6 +32,8 @@ @import './pages/index'; +@import './provider-page-container/index'; + @import './selected-account/index'; @import './sender-to-recipient/index'; @@ -49,3 +59,5 @@ @import './app-header/index'; @import './sidebars/index'; + +@import './unit-input/index'; diff --git a/ui/app/components/jazzicon/index.js b/ui/app/components/jazzicon/index.js new file mode 100644 index 000000000..bea900ab9 --- /dev/null +++ b/ui/app/components/jazzicon/index.js @@ -0,0 +1 @@ +export { default } from './jazzicon.component' diff --git a/ui/app/components/jazzicon/jazzicon.component.js b/ui/app/components/jazzicon/jazzicon.component.js new file mode 100644 index 000000000..fcb1c59b1 --- /dev/null +++ b/ui/app/components/jazzicon/jazzicon.component.js @@ -0,0 +1,69 @@ +import React, { PureComponent } from 'react' +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' +const iconFactory = iconFactoryGenerator(jazzicon) + +/** + * Wrapper around the jazzicon library to return a React component, as the library returns an + * HTMLDivElement which needs to be appended. + */ +export default class Jazzicon extends PureComponent { + static propTypes = { + address: PropTypes.string.isRequired, + className: PropTypes.string, + diameter: PropTypes.number, + style: PropTypes.object, + } + + static defaultProps = { + diameter: 46, + } + + componentDidMount () { + if (!isNode) { + this.appendJazzicon() + } + } + + componentDidUpdate (prevProps) { + const { address: prevAddress } = prevProps + const { address } = this.props + + if (!isNode && address !== prevAddress) { + this.removeExistingChildren() + this.appendJazzicon() + } + } + + removeExistingChildren () { + // eslint-disable-next-line react/no-find-dom-node + const container = findDOMNode(this) + const { children } = container + + for (let i = 0; i < children.length; i++) { + container.removeChild(children[i]) + } + } + + appendJazzicon () { + // eslint-disable-next-line react/no-find-dom-node + const container = findDOMNode(this) + const { address, diameter } = this.props + const image = iconFactory.iconForAddress(address, diameter) + container.appendChild(image) + } + + render () { + const { className, style } = this.props + + return ( + <div + className={className} + style={style} + /> + ) + } +} diff --git a/ui/app/components/modals/account-modal-container.js b/ui/app/components/modals/account-modal-container.js index aa0593df8..2a6c655e1 100644 --- a/ui/app/components/modals/account-modal-container.js +++ b/ui/app/components/modals/account-modal-container.js @@ -5,7 +5,7 @@ const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../actions') const { getSelectedIdentity } = require('../../selectors') -const Identicon = require('../identicon') +import Identicon from '../identicon' function mapStateToProps (state, ownProps) { return { diff --git a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js b/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js index b082db1d0..b973f221c 100644 --- a/ui/app/components/modals/cancel-transaction/cancel-transaction-gas-fee/cancel-transaction-gas-fee.component.js +++ b/ui/app/components/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 CurrencyDisplay from '../../../currency-display' -import { ETH } from '../../../../constants/common' +import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../../constants/common' export default class CancelTransaction extends PureComponent { static propTypes = { @@ -13,15 +13,15 @@ export default class CancelTransaction extends PureComponent { return ( <div className="cancel-transaction-gas-fee"> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="cancel-transaction-gas-fee__eth" - currency={ETH} value={value} - numberOfDecimals={6} + type={PRIMARY} /> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="cancel-transaction-gas-fee__fiat" value={value} + type={SECONDARY} /> </div> ) 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/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js index 994c2a577..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/modals/cancel-transaction/cancel-transaction-gas-fee/tests/cancel-transaction-gas-fee.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import CancelTransactionGasFee from '../cancel-transaction-gas-fee.component' -import CurrencyDisplay from '../../../../currency-display' +import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display' describe('CancelTransactionGasFee Component', () => { it('should render', () => { @@ -13,12 +13,11 @@ describe('CancelTransactionGasFee Component', () => { ) assert.ok(wrapper) - assert.equal(wrapper.find(CurrencyDisplay).length, 2) - const ethDisplay = wrapper.find(CurrencyDisplay).at(0) - const fiatDisplay = wrapper.find(CurrencyDisplay).at(1) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) + const ethDisplay = wrapper.find(UserPreferencedCurrencyDisplay).at(0) + const fiatDisplay = wrapper.find(UserPreferencedCurrencyDisplay).at(1) assert.equal(ethDisplay.props().value, '0x3b9aca00') - assert.equal(ethDisplay.props().currency, 'ETH') assert.equal(ethDisplay.props().className, 'cancel-transaction-gas-fee__eth') assert.equal(fiatDisplay.props().value, '0x3b9aca00') diff --git a/ui/app/components/modals/clear-approved-origins/clear-approved-origins.component.js b/ui/app/components/modals/clear-approved-origins/clear-approved-origins.component.js new file mode 100644 index 000000000..ceaa20a95 --- /dev/null +++ b/ui/app/components/modals/clear-approved-origins/clear-approved-origins.component.js @@ -0,0 +1,39 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Modal, { ModalContent } from '../../modal' + +export default class ClearApprovedOrigins extends PureComponent { + static propTypes = { + hideModal: PropTypes.func.isRequired, + clearApprovedOrigins: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + handleClear = () => { + const { clearApprovedOrigins, hideModal } = this.props + clearApprovedOrigins() + hideModal() + } + + render () { + const { t } = this.context + + return ( + <Modal + onSubmit={this.handleClear} + onCancel={() => this.props.hideModal()} + submitText={t('ok')} + cancelText={t('nevermind')} + submitType="secondary" + > + <ModalContent + title={t('clearApprovalData')} + description={t('confirmClear')} + /> + </Modal> + ) + } +} diff --git a/ui/app/components/modals/clear-approved-origins/clear-approved-origins.container.js b/ui/app/components/modals/clear-approved-origins/clear-approved-origins.container.js new file mode 100644 index 000000000..3a801a062 --- /dev/null +++ b/ui/app/components/modals/clear-approved-origins/clear-approved-origins.container.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import withModalProps from '../../../higher-order-components/with-modal-props' +import ClearApprovedOriginsComponent from './clear-approved-origins.component' +import { clearApprovedOrigins } from '../../../actions' + +const mapDispatchToProps = dispatch => { + return { + clearApprovedOrigins: () => dispatch(clearApprovedOrigins()), + } +} + +export default compose( + withModalProps, + connect(null, mapDispatchToProps) +)(ClearApprovedOriginsComponent) diff --git a/ui/app/components/modals/clear-approved-origins/index.js b/ui/app/components/modals/clear-approved-origins/index.js new file mode 100644 index 000000000..b3e321995 --- /dev/null +++ b/ui/app/components/modals/clear-approved-origins/index.js @@ -0,0 +1 @@ +export { default } from './clear-approved-origins.container' diff --git a/ui/app/components/modals/hide-token-confirmation-modal.js b/ui/app/components/modals/hide-token-confirmation-modal.js index fb38516d3..43f3009a5 100644 --- a/ui/app/components/modals/hide-token-confirmation-modal.js +++ b/ui/app/components/modals/hide-token-confirmation-modal.js @@ -4,7 +4,7 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../actions') -const Identicon = require('../identicon') +import Identicon from '../identicon' function mapStateToProps (state) { return { diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js index 15ca9deaa..5aff4f5e1 100644 --- a/ui/app/components/modals/modal.js +++ b/ui/app/components/modals/modal.js @@ -27,8 +27,8 @@ import TransactionConfirmed from './transaction-confirmed' import ConfirmCustomizeGasModal from './customize-gas' import CancelTransaction from './cancel-transaction' import WelcomeBeta from './welcome-beta' -import TransactionDetails from './transaction-details' import RejectTransactions from './reject-transactions' +import ClearApprovedOrigins from './clear-approved-origins' const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -213,6 +213,19 @@ const MODALS = { }, }, + CLEAR_APPROVED_ORIGINS: { + contents: h(ClearApprovedOrigins), + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, + OLD_UI_NOTIFICATION_MODAL: { contents: [ h(NotifcationModal, { @@ -366,19 +379,6 @@ const MODALS = { }, }, - TRANSACTION_DETAILS: { - contents: h(TransactionDetails), - mobileModalStyle: { - ...modalContainerMobileStyle, - }, - laptopModalStyle: { - ...modalContainerLaptopStyle, - }, - contentStyle: { - borderRadius: '8px', - }, - }, - REJECT_TRANSACTIONS: { contents: h(RejectTransactions), mobileModalStyle: { diff --git a/ui/app/components/modals/transaction-details/index.js b/ui/app/components/modals/transaction-details/index.js deleted file mode 100644 index 1fc42c662..000000000 --- a/ui/app/components/modals/transaction-details/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './transaction-details.container' diff --git a/ui/app/components/modals/transaction-details/transaction-details.component.js b/ui/app/components/modals/transaction-details/transaction-details.component.js deleted file mode 100644 index f2fec3409..000000000 --- a/ui/app/components/modals/transaction-details/transaction-details.component.js +++ /dev/null @@ -1,54 +0,0 @@ -import React, { PureComponent } from 'react' -import PropTypes from 'prop-types' -import Modal from '../../modal' -import TransactionListItemDetails from '../../transaction-list-item-details' -import { hexToDecimal } from '../../../helpers/conversions.util' - -export default class TransactionConfirmed extends PureComponent { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - hideModal: PropTypes.func, - transaction: PropTypes.object, - onRetry: PropTypes.func, - showRetry: PropTypes.bool, - onCancel: PropTypes.func, - showCancel: PropTypes.bool, - } - - handleSubmit = () => { - this.props.hideModal() - } - - handleRetry = () => { - const { onRetry, hideModal } = this.props - - Promise.resolve(onRetry()).then(() => hideModal()) - } - - render () { - const { t } = this.context - const { transaction, showRetry, onCancel, showCancel } = this.props - const { txParams: { nonce } = {} } = transaction - const decimalNonce = nonce && hexToDecimal(nonce) - - return ( - <Modal - onSubmit={this.handleSubmit} - onClose={this.handleSubmit} - submitText={t('ok')} - headerText={t('transactionWithNonce', [`#${decimalNonce}`])} - > - <TransactionListItemDetails - transaction={transaction} - onRetry={this.handleRetry} - showRetry={showRetry} - onCancel={() => onCancel()} - showCancel={showCancel} - /> - </Modal> - ) - } -} diff --git a/ui/app/components/modals/transaction-details/transaction-details.container.js b/ui/app/components/modals/transaction-details/transaction-details.container.js deleted file mode 100644 index f212920bb..000000000 --- a/ui/app/components/modals/transaction-details/transaction-details.container.js +++ /dev/null @@ -1,4 +0,0 @@ -import TransactionDetails from './transaction-details.component' -import withModalProps from '../../../higher-order-components/with-modal-props' - -export default withModalProps(TransactionDetails) diff --git a/ui/app/components/network-display/index.scss b/ui/app/components/network-display/index.scss index 2085cff67..e9f2f2057 100644 --- a/ui/app/components/network-display/index.scss +++ b/ui/app/components/network-display/index.scss @@ -3,11 +3,14 @@ display: flex; align-items: center; justify-content: flex-start; - background-color: lighten(rgb(125, 128, 130), 45%); padding: 0 10px; border-radius: 4px; height: 25px; + &--colored { + background-color: lighten(rgb(125, 128, 130), 45%); + } + &--mainnet { background-color: lighten($blue-lagoon, 68%); } diff --git a/ui/app/components/network-display/network-display.component.js b/ui/app/components/network-display/network-display.component.js index 38626af20..22d617099 100644 --- a/ui/app/components/network-display/network-display.component.js +++ b/ui/app/components/network-display/network-display.component.js @@ -16,7 +16,12 @@ const networkToClassHash = { } export default class NetworkDisplay extends Component { + static defaultProps = { + colored: true, + } + static propTypes = { + colored: PropTypes.bool, network: PropTypes.string, provider: PropTypes.object, } @@ -41,14 +46,16 @@ export default class NetworkDisplay extends Component { } render () { - const { network, provider: { type } } = this.props + const { colored, network, provider: { type, nickname } } = this.props const networkClass = networkToClassHash[network] return ( - <div className={classnames( - 'network-display__container', - networkClass && ('network-display__container--' + networkClass) - )}> + <div + className={classnames('network-display__container', { + 'network-display__container--colored': colored, + ['network-display__container--' + networkClass]: colored && networkClass, + })} + > { networkClass ? <div className={`network-display__icon network-display__icon--${networkClass}`} /> @@ -61,7 +68,7 @@ export default class NetworkDisplay extends Component { /> } <div className="network-display__name"> - { this.context.t(type) } + { type === 'rpc' && nickname ? nickname : this.context.t(type) } </div> </div> ) diff --git a/ui/app/components/network.js b/ui/app/components/network.js index 83297c4f2..611aadb7b 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -23,9 +23,10 @@ Network.prototype.render = function () { const props = this.props const context = this.context const networkNumber = props.network - let providerName + let providerName, providerNick try { providerName = props.provider.type + providerNick = props.provider.nickname || '' } catch (e) { providerName = null } @@ -131,7 +132,7 @@ Network.prototype.render = function () { }, }), - h('.network-name', context.t('privateNetwork')), + h('.network-name', providerNick || context.t('privateNetwork')), h('i.fa.fa-chevron-down.fa-lg.network-caret'), ]) } diff --git a/ui/app/components/page-container/index.scss b/ui/app/components/page-container/index.scss index 6742e3082..ba1215e84 100644 --- a/ui/app/components/page-container/index.scss +++ b/ui/app/components/page-container/index.scss @@ -101,6 +101,7 @@ font-size: 2rem; font-weight: 500; line-height: 2rem; + margin-right: 1.5rem; } &__subtitle { 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 index acaed383a..7f1fb4e49 100644 --- 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 @@ -1,12 +1,15 @@ 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 = { @@ -36,19 +39,48 @@ export default class ConfirmTokenTransactionBase extends Component { }) } - getSubtitle () { - const { currentCurrency, contractExchangeRate } = this.props + renderSubtitleComponent () { + const { contractExchangeRate, tokenAmount } = this.props - if (typeof contractExchangeRate === 'undefined') { - return this.context.t('noConversionRateAvailable') - } else { - const fiatTransactionAmount = this.getFiatTransactionAmount() - const roundedFiatTransactionAmount = roundExponential(fiatTransactionAmount) - return formatCurrency(roundedFiatTransactionAmount, currentCurrency) - } + 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> + ) } - getFiatTotalTextOverride () { + getSecondaryTotalTextOverride () { const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props if (typeof contractExchangeRate === 'undefined') { @@ -67,7 +99,6 @@ export default class ConfirmTokenTransactionBase extends Component { tokenAddress, tokenSymbol, tokenAmount, - ethTransactionTotal, ...restProps } = this.props @@ -78,9 +109,9 @@ export default class ConfirmTokenTransactionBase extends Component { toAddress={toAddress} identiconAddress={tokenAddress} title={tokensText} - subtitle={this.getSubtitle()} - ethTotalTextOverride={`${tokensText} + \u2666 ${ethTransactionTotal}`} - fiatTotalTextOverride={this.getFiatTotalTextOverride()} + subtitleComponent={this.renderSubtitleComponent()} + primaryTotalTextOverride={this.renderPrimaryTotalTextOverride()} + secondaryTotalTextOverride={this.getSecondaryTotalTextOverride()} {...restProps} /> ) 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 index 707dad62d..7d01aaffb 100644 --- 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 @@ -1,7 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container' -import { formatCurrency } from '../../../helpers/confirm-transaction/util' import { isBalanceSufficient } from '../../send/send.utils' import { DEFAULT_ROUTE } from '../../../routes' import { @@ -9,6 +8,8 @@ import { 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 = { @@ -36,7 +37,9 @@ export default class ConfirmTransactionBase extends Component { fiatTransactionTotal: PropTypes.string, fromAddress: PropTypes.string, fromName: PropTypes.string, - hexGasTotal: PropTypes.string, + hexTransactionAmount: PropTypes.string, + hexTransactionFee: PropTypes.string, + hexTransactionTotal: PropTypes.string, isTxReprice: PropTypes.bool, methodData: PropTypes.object, nonce: PropTypes.string, @@ -59,8 +62,8 @@ export default class ConfirmTransactionBase extends Component { detailsComponent: PropTypes.node, errorKey: PropTypes.string, errorMessage: PropTypes.string, - ethTotalTextOverride: PropTypes.string, - fiatTotalTextOverride: PropTypes.string, + primaryTotalTextOverride: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + secondaryTotalTextOverride: PropTypes.string, hideData: PropTypes.bool, hideDetails: PropTypes.bool, hideSubtitle: PropTypes.bool, @@ -70,8 +73,10 @@ export default class ConfirmTransactionBase extends Component { 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, } @@ -105,7 +110,7 @@ export default class ConfirmTransactionBase extends Component { const { balance, conversionRate, - hexGasTotal, + hexTransactionFee, txData: { simulationFails, txParams: { @@ -116,7 +121,7 @@ export default class ConfirmTransactionBase extends Component { const insufficientBalance = balance && !isBalanceSufficient({ amount, - gasTotal: hexGasTotal || '0x0', + gasTotal: hexTransactionFee || '0x0', balance, conversionRate, }) @@ -131,7 +136,7 @@ export default class ConfirmTransactionBase extends Component { if (simulationFails) { return { valid: true, - errorKey: TRANSACTION_ERROR_KEY, + errorKey: simulationFails.errorKey ? simulationFails.errorKey : TRANSACTION_ERROR_KEY, } } @@ -153,13 +158,10 @@ export default class ConfirmTransactionBase extends Component { renderDetails () { const { detailsComponent, - fiatTransactionFee, - ethTransactionFee, - currentCurrency, - fiatTransactionTotal, - ethTransactionTotal, - fiatTotalTextOverride, - ethTotalTextOverride, + primaryTotalTextOverride, + secondaryTotalTextOverride, + hexTransactionFee, + hexTransactionTotal, hideDetails, } = this.props @@ -167,16 +169,13 @@ export default class ConfirmTransactionBase extends Component { return null } - const formattedCurrency = formatCurrency(fiatTransactionTotal, currentCurrency) - return ( detailsComponent || ( <div className="confirm-page-container-content__details"> <div className="confirm-page-container-content__gas-fee"> <ConfirmDetailRow label="Gas Fee" - fiatText={formatCurrency(fiatTransactionFee, currentCurrency)} - ethText={`\u2666 ${ethTransactionFee}`} + value={hexTransactionFee} headerText="Edit" headerTextClassName="confirm-detail-row__header-text--edit" onHeaderClick={() => this.handleEditGas()} @@ -185,11 +184,12 @@ export default class ConfirmTransactionBase extends Component { <div> <ConfirmDetailRow label="Total" - fiatText={fiatTotalTextOverride || formattedCurrency} - ethText={ethTotalTextOverride || `\u2666 ${ethTransactionTotal}`} + value={hexTransactionTotal} + primaryText={primaryTotalTextOverride} + secondaryText={secondaryTotalTextOverride} headerText="Amount + Gas Fee" headerTextClassName="confirm-detail-row__header-text--total" - fiatTextColor="#2f9ae0" + primaryValueTextColor="#2f9ae0" /> </div> </div> @@ -311,6 +311,43 @@ export default class ConfirmTransactionBase extends Component { } } + 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, @@ -319,12 +356,9 @@ export default class ConfirmTransactionBase extends Component { toName, toAddress, methodData, - ethTransactionAmount, - fiatTransactionAmount, valid: propsValid = true, errorMessage, errorKey: propsErrorKey, - currentCurrency, action, title, subtitle, @@ -341,7 +375,6 @@ export default class ConfirmTransactionBase extends Component { const { submitting, submitError } = this.state const { name } = methodData - const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency) const { valid, errorKey } = this.getErrorKey() return ( @@ -352,8 +385,10 @@ export default class ConfirmTransactionBase extends Component { toAddress={toAddress} showEdit={onEdit && !isTxReprice} action={action || name || this.context.t('unknownFunction')} - title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`} - subtitle={subtitle || `\u2666 ${ethTransactionAmount}`} + title={title} + titleComponent={this.renderTitleComponent()} + subtitle={subtitle} + subtitleComponent={this.renderSubtitleComponent()} hideSubtitle={hideSubtitle} summaryComponent={summaryComponent} detailsComponent={this.renderDetails()} 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 index b34067686..c366d5137 100644 --- 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 @@ -36,7 +36,9 @@ const mapStateToProps = (state, props) => { fiatTransactionAmount, fiatTransactionFee, fiatTransactionTotal, - hexGasTotal, + hexTransactionAmount, + hexTransactionFee, + hexTransactionTotal, tokenData, methodData, txData, @@ -87,7 +89,9 @@ const mapStateToProps = (state, props) => { fiatTransactionAmount, fiatTransactionFee, fiatTransactionTotal, - hexGasTotal, + hexTransactionAmount, + hexTransactionFee, + hexTransactionTotal, txData, tokenData, methodData, diff --git a/ui/app/components/pages/home/home.component.js b/ui/app/components/pages/home/home.component.js index d3c71c4f6..b9ec3c258 100644 --- a/ui/app/components/pages/home/home.component.js +++ b/ui/app/components/pages/home/home.component.js @@ -4,6 +4,8 @@ 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, @@ -21,6 +23,7 @@ export default class Home extends PureComponent { seedWords: PropTypes.string, suggestedTokens: PropTypes.object, unconfirmedTransactionsCount: PropTypes.number, + providerRequests: PropTypes.array, } componentDidMount () { @@ -46,6 +49,7 @@ export default class Home extends PureComponent { lostAccounts, forgottenPassword, seedWords, + providerRequests, } = this.props // notices @@ -62,6 +66,12 @@ export default class Home extends PureComponent { 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"> diff --git a/ui/app/components/pages/home/home.container.js b/ui/app/components/pages/home/home.container.js index 58001df6b..bb8cf5e81 100644 --- a/ui/app/components/pages/home/home.container.js +++ b/ui/app/components/pages/home/home.container.js @@ -11,6 +11,7 @@ const mapStateToProps = state => { lostAccounts, seedWords, suggestedTokens, + providerRequests, } = metamask const { forgottenPassword } = appState @@ -21,6 +22,7 @@ const mapStateToProps = state => { seedWords, suggestedTokens, unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state), + providerRequests, } } diff --git a/ui/app/components/pages/provider-approval/index.js b/ui/app/components/pages/provider-approval/index.js new file mode 100644 index 000000000..4162f3155 --- /dev/null +++ b/ui/app/components/pages/provider-approval/index.js @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000..da98bc3fc --- /dev/null +++ b/ui/app/components/pages/provider-approval/provider-approval.component.js @@ -0,0 +1,28 @@ +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 new file mode 100644 index 000000000..b223244a1 --- /dev/null +++ b/ui/app/components/pages/provider-approval/provider-approval.container.js @@ -0,0 +1,12 @@ +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/settings-tab/index.scss b/ui/app/components/pages/settings/settings-tab/index.scss index 76a0cec6f..ef32b0e4c 100644 --- a/ui/app/components/pages/settings/settings-tab/index.scss +++ b/ui/app/components/pages/settings/settings-tab/index.scss @@ -5,12 +5,9 @@ color: $crimson; } - &__rpc-save-button { - align-self: flex-end; - padding: 5px; - text-transform: uppercase; - color: $dusty-gray; - cursor: pointer; + &__advanced-link { + color: $curious-blue; + padding-left: 5px; } &__rpc-save-button { @@ -19,6 +16,9 @@ text-transform: uppercase; color: $dusty-gray; cursor: pointer; + width: 25%; + min-width: 80px; + height: 33px; } &__button--red { @@ -48,4 +48,22 @@ 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 index 9da624f56..a0a8ed47e 100644 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.component.js +++ b/ui/app/components/pages/settings/settings-tab/settings-tab.component.js @@ -39,12 +39,15 @@ export default class SettingsTab extends PureComponent { 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, @@ -55,10 +58,17 @@ export default class SettingsTab extends PureComponent { 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 () { @@ -119,37 +129,98 @@ export default class SettingsTab extends PureComponent { renderNewRpcUrl () { const { t } = this.context - const { newRpc } = this.state + const { newRpc, chainId, ticker, nickname } = this.state return ( <div className="settings-page__content-row"> <div className="settings-page__content-item"> - <span>{ t('newRPC') }</span> + <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('newRPC')} + placeholder={t('rpcURL')} value={newRpc} onChange={e => this.setState({ newRpc: e.target.value })} onKeyPress={e => { if (e.key === 'Enter') { - this.validateRpc(newRpc) + this.validateRpc(newRpc, chainId, ticker, nickname) } }} fullWidth - margin="none" + margin="dense" /> - <div - className="settings-tab__rpc-save-button" - onClick={e => { - e.preventDefault() - this.validateRpc(newRpc) + <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) + } }} - > - { t('save') } + 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> @@ -157,11 +228,11 @@ export default class SettingsTab extends PureComponent { ) } - validateRpc (newRpc) { + validateRpc (newRpc, chainId, ticker = 'ETH', nickname) { const { setRpcTarget, displayWarning } = this.props if (validUrl.isWebUri(newRpc)) { - setRpcTarget(newRpc) + setRpcTarget(newRpc, chainId, ticker, nickname) } else { const appendedRpc = `http://${newRpc}` @@ -208,6 +279,36 @@ export default class SettingsTab extends PureComponent { ) } + 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 @@ -339,6 +440,86 @@ export default class SettingsTab extends PureComponent { ) } + 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 @@ -346,14 +527,17 @@ export default class SettingsTab extends PureComponent { <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.renderBlockieOptIn() } + { 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 index 665b56f5c..b6c33a5b2 100644 --- a/ui/app/components/pages/settings/settings-tab/settings-tab.container.js +++ b/ui/app/components/pages/settings/settings-tab/settings-tab.container.js @@ -11,19 +11,26 @@ import { 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 } = {}, + featureFlags: { + sendHexData, + privacyMode, + } = {}, provider = {}, isMascara, currentLocale, } = metamask + const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state) return { warning, @@ -31,16 +38,19 @@ const mapStateToProps = state => { currentLocale, currentCurrency, conversionDate, + nativeCurrency, useBlockie, sendHexData, + privacyMode, provider, + useNativeCurrencyAsPrimaryCurrency, } } const mapDispatchToProps = dispatch => { return { setCurrentCurrency: currency => dispatch(setCurrentCurrency(currency)), - setRpcTarget: newRpc => dispatch(setRpcTarget(newRpc)), + setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(setRpcTarget(newRpc, chainId, ticker, nickname)), displayWarning: warning => dispatch(displayWarning(warning)), revealSeedConfirmation: () => dispatch(revealSeedConfirmation()), setUseBlockie: value => dispatch(setUseBlockie(value)), @@ -49,7 +59,12 @@ const mapDispatchToProps = dispatch => { 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' })), } } diff --git a/ui/app/components/provider-page-container/index.js b/ui/app/components/provider-page-container/index.js new file mode 100644 index 000000000..927c35940 --- /dev/null +++ b/ui/app/components/provider-page-container/index.js @@ -0,0 +1,3 @@ +export {default} from './provider-page-container.component' +export {default as ProviderPageContainerContent} from './provider-page-container-content' +export {default as ProviderPageContainerHeader} from './provider-page-container-header' diff --git a/ui/app/components/provider-page-container/index.scss b/ui/app/components/provider-page-container/index.scss new file mode 100644 index 000000000..8d35ac179 --- /dev/null +++ b/ui/app/components/provider-page-container/index.scss @@ -0,0 +1,121 @@ +.provider-approval-container { + display: flex; + + &__header { + display: flex; + flex-direction: column; + align-items: flex-end; + border-bottom: 1px solid $geyser; + padding: 9px; + } + + &__content { + display: flex; + overflow-y: auto; + flex: 1; + flex-direction: column; + justify-content: space-between; + color: #7C808E; + + h1, h2 { + color: #4A4A4A; + display: flex; + justify-content: center; + text-align: center; + } + + h2 { + font-size: 16px; + line-height: 18px; + padding: 20px; + } + + h1 { + font-size: 22px; + line-height: 26px; + padding: 20px; + } + + p { + padding: 0 40px; + text-align: center; + font-size: 12px; + line-height: 18px; + } + + a, a:hover { + color: $dodger-blue; + } + + .provider-approval-visual { + display: flex; + flex-direction: row; + justify-content: space-evenly; + position: relative; + margin: 0 32px; + + section { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + } + + h1 { + font-size: 14px; + line-height: 18px; + padding: 8px 0 0; + } + + h2 { + font-size: 10px; + line-height: 14px; + padding: 0; + color: #A2A4AC; + } + + &__check { + width: 40px; + height: 40px; + background: white url("/images/provider-approval-check.svg") no-repeat; + margin-top: 14px; + } + + &__identicon { + width: 64px; + height: 64px; + + &--default { + background-color: #777A87; + color: white; + width: 64px; + height: 64px; + border-radius: 32px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + } + } + + &:before { + border-top: 2px dashed #CDD1E4; + content: ""; + margin: 0 auto; + position: absolute; + top: 32px; + left: 0; + bottom: 0; + right: 0; + width: 65%; + z-index: -1; + } + } + + .secure-badge { + display: flex; + justify-content: center; + padding: 25px; + } + } +} diff --git a/ui/app/components/provider-page-container/provider-page-container-content/index.js b/ui/app/components/provider-page-container/provider-page-container-content/index.js new file mode 100644 index 000000000..73e491adc --- /dev/null +++ b/ui/app/components/provider-page-container/provider-page-container-content/index.js @@ -0,0 +1 @@ +export {default} from './provider-page-container-content.container' diff --git a/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.component.js b/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.component.js new file mode 100644 index 000000000..268db613f --- /dev/null +++ b/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.component.js @@ -0,0 +1,77 @@ +import PropTypes from 'prop-types' +import React, {PureComponent} from 'react' +import Identicon from '../../identicon' + +export default class ProviderPageContainerContent extends PureComponent { + static propTypes = { + origin: PropTypes.string.isRequired, + selectedIdentity: PropTypes.string.isRequired, + siteImage: PropTypes.string, + siteTitle: PropTypes.string.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + }; + + renderConnectVisual = () => { + const { origin, selectedIdentity, siteImage, siteTitle } = this.props + + return ( + <div className="provider-approval-visual"> + <section> + {siteImage ? ( + <img + className="provider-approval-visual__identicon" + src={siteImage} + /> + ) : ( + <i className="provider-approval-visual__identicon--default"> + {siteTitle.charAt(0).toUpperCase()} + </i> + )} + <h1>{siteTitle}</h1> + <h2>{origin}</h2> + </section> + <span className="provider-approval-visual__check" /> + <section> + <Identicon + className="provider-approval-visual__identicon" + address={selectedIdentity.address} + diameter={64} + /> + <h1>{selectedIdentity.name}</h1> + </section> + </div> + ) + } + + render () { + const { siteTitle } = this.props + const { t } = this.context + + return ( + <div className="provider-approval-container__content"> + <section> + <h2>{t('connectRequest')}</h2> + {this.renderConnectVisual()} + <h1>{t('providerRequest', [siteTitle])}</h1> + <p> + {t('providerRequestInfo')} + <br/> + <a + href="https://medium.com/metamask/introducing-privacy-mode-42549d4870fa" + target="_blank" + rel="noopener noreferrer" + > + {t('learnMore')}. + </a> + </p> + </section> + <section className="secure-badge"> + <img src="/images/mm-secure.svg" /> + </section> + </div> + ) + } +} diff --git a/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.container.js b/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.container.js new file mode 100644 index 000000000..3ea1ce20e --- /dev/null +++ b/ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.container.js @@ -0,0 +1,11 @@ +import { connect } from 'react-redux' +import ProviderPageContainerContent from './provider-page-container-content.component' +import { getSelectedIdentity } from '../../../selectors' + +const mapStateToProps = (state) => { + return { + selectedIdentity: getSelectedIdentity(state), + } +} + +export default connect(mapStateToProps)(ProviderPageContainerContent) diff --git a/ui/app/components/provider-page-container/provider-page-container-header/index.js b/ui/app/components/provider-page-container/provider-page-container-header/index.js new file mode 100644 index 000000000..430627d3a --- /dev/null +++ b/ui/app/components/provider-page-container/provider-page-container-header/index.js @@ -0,0 +1 @@ +export {default} from './provider-page-container-header.component' diff --git a/ui/app/components/provider-page-container/provider-page-container-header/provider-page-container-header.component.js b/ui/app/components/provider-page-container/provider-page-container-header/provider-page-container-header.component.js new file mode 100644 index 000000000..41bf6c3dd --- /dev/null +++ b/ui/app/components/provider-page-container/provider-page-container-header/provider-page-container-header.component.js @@ -0,0 +1,12 @@ +import React, {PureComponent} from 'react' +import NetworkDisplay from '../../network-display' + +export default class ProviderPageContainerHeader extends PureComponent { + render () { + return ( + <div className="provider-approval-container__header"> + <NetworkDisplay colored={false} /> + </div> + ) + } +} diff --git a/ui/app/components/provider-page-container/provider-page-container.component.js b/ui/app/components/provider-page-container/provider-page-container.component.js new file mode 100644 index 000000000..902733616 --- /dev/null +++ b/ui/app/components/provider-page-container/provider-page-container.component.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types' +import React, {PureComponent} from 'react' +import { ProviderPageContainerContent, ProviderPageContainerHeader } from './' +import { PageContainerFooter } from '../page-container' + +export default class ProviderPageContainer extends PureComponent { + static propTypes = { + approveProviderRequest: PropTypes.func.isRequired, + origin: PropTypes.string.isRequired, + rejectProviderRequest: PropTypes.func.isRequired, + siteImage: PropTypes.string, + siteTitle: PropTypes.string.isRequired, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + onCancel = () => { + const { origin, rejectProviderRequest } = this.props + rejectProviderRequest(origin) + } + + onSubmit = () => { + const { approveProviderRequest, origin } = this.props + approveProviderRequest(origin) + } + + render () { + const {origin, siteImage, siteTitle} = this.props + + return ( + <div className="page-container provider-approval-container"> + <ProviderPageContainerHeader /> + <ProviderPageContainerContent + origin={origin} + siteImage={siteImage} + siteTitle={siteTitle} + /> + <PageContainerFooter + onCancel={() => this.onCancel()} + cancelText={this.context.t('cancel')} + onSubmit={() => this.onSubmit()} + submitText={this.context.t('connect')} + submitButtonType="confirm" + /> + </div> + ) + } +} 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 index 9f4a96e61..14bb7471f 100644 --- 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 @@ -2,7 +2,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import { checksumAddress } from '../../../util' import Identicon from '../../identicon' -import CurrencyDisplay from '../currency-display' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../constants/common' export default class AccountListItem extends Component { @@ -25,8 +26,6 @@ export default class AccountListItem extends Component { const { account, className, - conversionRate, - currentCurrency, displayAddress = false, displayBalance = true, handleClick, @@ -57,16 +56,20 @@ export default class AccountListItem extends Component { { checksumAddress(address) } </div>} - {displayBalance && <CurrencyDisplay - className="account-list-item__account-balances" - conversionRate={conversionRate} - convertedBalanceClassName="account-list-item__account-secondary-balance" - convertedCurrency={currentCurrency} - primaryBalanceClassName="account-list-item__account-primary-balance" - primaryCurrency="ETH" - readOnly={true} - value={balance} - />} + { + 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/account-list-item.container.js b/ui/app/components/send/account-list-item/account-list-item.container.js index 4b4519288..f8e73d923 100644 --- a/ui/app/components/send/account-list-item/account-list-item.container.js +++ b/ui/app/components/send/account-list-item/account-list-item.container.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux' import { getConversionRate, getCurrentCurrency, + getNativeCurrency, } from '../send.selectors.js' import AccountListItem from './account-list-item.component' @@ -11,5 +12,6 @@ function mapStateToProps (state) { return { conversionRate: getConversionRate(state), currentCurrency: getCurrentCurrency(state), + nativeCurrency: getNativeCurrency(state), } } diff --git a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js index ef152d2e7..6ffc0b1c6 100644 --- a/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js +++ b/ui/app/components/send/account-list-item/tests/account-list-item-component.test.js @@ -4,7 +4,7 @@ import { shallow } from 'enzyme' import sinon from 'sinon' import proxyquire from 'proxyquire' import Identicon from '../../../identicon' -import CurrencyDisplay from '../../currency-display' +import UserPreferencedCurrencyDisplay from '../../../user-preferenced-currency-display' const utilsMethodStubs = { checksumAddress: sinon.stub().returns('mockCheckSumAddress'), @@ -28,6 +28,7 @@ describe('AccountListItem Component', function () { className={'mockClassName'} conversionRate={4} currentCurrency={'mockCurrentyCurrency'} + nativeCurrency={'ETH'} displayAddress={false} displayBalance={false} handleClick={propsMethodSpies.handleClick} @@ -114,17 +115,11 @@ describe('AccountListItem Component', function () { it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => { wrapper.setProps({ displayBalance: true }) - assert.equal(wrapper.find(CurrencyDisplay).length, 1) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) assert.deepEqual( - wrapper.find(CurrencyDisplay).props(), + wrapper.find(UserPreferencedCurrencyDisplay).at(0).props(), { - className: 'account-list-item__account-balances', - conversionRate: 4, - convertedBalanceClassName: 'account-list-item__account-secondary-balance', - convertedCurrency: 'mockCurrentyCurrency', - primaryBalanceClassName: 'account-list-item__account-primary-balance', - primaryCurrency: 'ETH', - readOnly: true, + type: 'PRIMARY', value: 'mockBalance', } ) @@ -132,7 +127,7 @@ describe('AccountListItem Component', function () { it('should not render a CurrencyDisplay if displayBalance is false', () => { wrapper.setProps({ displayBalance: false }) - assert.equal(wrapper.find(CurrencyDisplay).length, 0) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 0) }) }) }) 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 index af0859117..7c2f5fcb2 100644 --- 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 @@ -13,6 +13,7 @@ proxyquire('../account-list-item.container.js', { '../send.selectors.js': { getConversionRate: (s) => `mockConversionRate:${s}`, getCurrentCurrency: (s) => `mockCurrentCurrency:${s}`, + getNativeCurrency: (s) => `mockNativeCurrency:${s}`, }, }) @@ -24,6 +25,7 @@ describe('account-list-item container', () => { assert.deepEqual(mapStateToProps('mockState'), { conversionRate: 'mockConversionRate:mockState', currentCurrency: 'mockCurrentCurrency:mockState', + nativeCurrency: 'mockNativeCurrency:mockState', }) }) diff --git a/ui/app/components/send/currency-display/currency-display.js b/ui/app/components/send/currency-display/currency-display.js deleted file mode 100644 index 2b8eaa41f..000000000 --- a/ui/app/components/send/currency-display/currency-display.js +++ /dev/null @@ -1,186 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const { conversionUtil, multiplyCurrencies } = require('../../../conversion-util') -const { removeLeadingZeroes } = require('../send.utils') -const currencyFormatter = require('currency-formatter') -const currencies = require('currency-formatter/currencies') -const ethUtil = require('ethereumjs-util') -const PropTypes = require('prop-types') - -CurrencyDisplay.contextTypes = { - t: PropTypes.func, -} - -module.exports = CurrencyDisplay - -inherits(CurrencyDisplay, Component) -function CurrencyDisplay () { - Component.call(this) -} - -function toHexWei (value) { - return conversionUtil(value, { - fromNumericBase: 'dec', - toNumericBase: 'hex', - toDenomination: 'WEI', - }) -} - -CurrencyDisplay.prototype.componentWillMount = function () { - this.setState({ - valueToRender: this.getValueToRender(this.props), - }) -} - -CurrencyDisplay.prototype.componentWillReceiveProps = function (nextProps) { - const currentValueToRender = this.getValueToRender(this.props) - const newValueToRender = this.getValueToRender(nextProps) - if (currentValueToRender !== newValueToRender) { - this.setState({ - valueToRender: newValueToRender, - }) - } -} - -CurrencyDisplay.prototype.getAmount = function (value) { - const { selectedToken } = this.props - const { decimals } = selectedToken || {} - const multiplier = Math.pow(10, Number(decimals || 0)) - - const sendAmount = multiplyCurrencies(value || '0', multiplier, {toNumericBase: 'hex'}) - - return selectedToken - ? sendAmount - : toHexWei(value) -} - -CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value, readOnly }) { - if (value === '0x0') return readOnly ? '0' : '' - const { decimals, symbol } = selectedToken || {} - const multiplier = Math.pow(10, Number(decimals || 0)) - - return selectedToken - ? conversionUtil(ethUtil.addHexPrefix(value), { - fromNumericBase: 'hex', - toNumericBase: 'dec', - toCurrency: symbol, - conversionRate: multiplier, - invertConversionRate: true, - }) - : conversionUtil(ethUtil.addHexPrefix(value), { - fromNumericBase: 'hex', - toNumericBase: 'dec', - fromDenomination: 'WEI', - numberOfDecimals: 9, - conversionRate, - }) -} - -CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { - const { primaryCurrency, convertedCurrency, conversionRate } = this.props - - if (conversionRate === 0 || conversionRate === null || conversionRate === undefined) { - if (nonFormattedValue !== 0) { - return null - } - } - - let convertedValue = conversionUtil(nonFormattedValue, { - fromNumericBase: 'dec', - fromCurrency: primaryCurrency, - toCurrency: convertedCurrency, - numberOfDecimals: 2, - conversionRate, - }) - - convertedValue = Number(convertedValue).toFixed(2) - const upperCaseCurrencyCode = convertedCurrency.toUpperCase() - return currencies.find(currency => currency.code === upperCaseCurrencyCode) - ? currencyFormatter.format(Number(convertedValue), { - code: upperCaseCurrencyCode, - }) - : convertedValue - } - -CurrencyDisplay.prototype.handleChange = function (newVal) { - this.setState({ valueToRender: removeLeadingZeroes(newVal) }) - this.props.onChange(this.getAmount(newVal)) -} - -CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) { - const valueString = String(valueToRender) - const valueLength = valueString.length || 1 - const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 - return (valueLength + decimalPointDeficit + 0.75) + 'ch' -} - -CurrencyDisplay.prototype.onlyRenderConversions = function (convertedValueToRender) { - const { - convertedBalanceClassName = 'currency-display__converted-value', - convertedCurrency, - } = this.props - return h('div', { - className: convertedBalanceClassName, - }, convertedValueToRender == null - ? this.context.t('noConversionRateAvailable') - : `${convertedValueToRender} ${convertedCurrency.toUpperCase()}` -) - } - -CurrencyDisplay.prototype.render = function () { - const { - className = 'currency-display', - primaryBalanceClassName = 'currency-display__input', - primaryCurrency, - readOnly = false, - inError = false, - onBlur, - step, - } = this.props - const { valueToRender } = this.state - - const convertedValueToRender = this.getConvertedValueToRender(valueToRender) - - return h('div', { - className, - style: { - borderColor: inError ? 'red' : null, - }, - onClick: () => { - this.currencyInput && this.currencyInput.focus() - }, - }, [ - - h('div.currency-display__primary-row', [ - - h('div.currency-display__input-wrapper', [ - - h('input', { - className: primaryBalanceClassName, - value: `${valueToRender}`, - placeholder: '0', - type: 'number', - readOnly, - ...(!readOnly ? { - onChange: e => this.handleChange(e.target.value), - onBlur: () => onBlur(this.getAmount(valueToRender)), - } : {}), - ref: input => { this.currencyInput = input }, - style: { - width: this.getInputWidth(valueToRender, readOnly), - }, - min: 0, - step, - }), - - h('span.currency-display__currency-symbol', primaryCurrency), - - ]), - - ]), this.onlyRenderConversions(convertedValueToRender), - - ]) - -} - diff --git a/ui/app/components/send/currency-display/index.js b/ui/app/components/send/currency-display/index.js deleted file mode 100644 index 5dc269c5a..000000000 --- a/ui/app/components/send/currency-display/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './currency-display.js' diff --git a/ui/app/components/send/currency-display/tests/currency-display.test.js b/ui/app/components/send/currency-display/tests/currency-display.test.js deleted file mode 100644 index c9560b81c..000000000 --- a/ui/app/components/send/currency-display/tests/currency-display.test.js +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react' -import assert from 'assert' -import sinon from 'sinon' -import { shallow, mount } from 'enzyme' -import CurrencyDisplay from '../currency-display' - -describe('', () => { - - const token = { - address: '0xTest', - symbol: 'TST', - decimals: '13', - } - - it('retuns ETH value for wei value', () => { - const wrapper = mount(<CurrencyDisplay />, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getValueToRender({ - // 1000000000000000000 - value: 'DE0B6B3A7640000', - }) - - assert.equal(value, 1) - }) - - it('returns value of token based on token decimals', () => { - const wrapper = mount(<CurrencyDisplay />, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getValueToRender({ - selectedToken: token, - // 1000000000000000000 - value: 'DE0B6B3A7640000', - }) - - assert.equal(value, 100000) - }) - - it('returns hex value with decimal adjustment', () => { - - const wrapper = mount( - <CurrencyDisplay - selectedToken={token} - />, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getAmount(1) - // 10000000000000 - assert.equal(value, '9184e72a000') - }) - - it('#getConvertedValueToRender converts input value based on conversionRate', () => { - - const wrapper = mount( - <CurrencyDisplay - primaryCurrency={'usd'} - convertedCurrency={'ja'} - conversionRate={2} - />, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().getConvertedValueToRender(32) - - assert.equal(value, 64) - }) - - it('#onlyRenderConversions renders single element for converted currency and value', () => { - const wrapper = mount( - <CurrencyDisplay - convertedCurrency={'test'} - />, {context: {t: str => str + '_t'}}) - - const value = wrapper.instance().onlyRenderConversions(10) - assert.equal(value.props.className, 'currency-display__converted-value') - assert.equal(value.props.children, '10 TEST') - }) - - it('simulates change value in input', () => { - const handleChangeSpy = sinon.spy() - - const wrapper = shallow( - <CurrencyDisplay - onChange={handleChangeSpy} - />, {context: {t: str => str + '_t'}}) - - const input = wrapper.find('input') - input.simulate('focus') - input.simulate('change', { target: { value: '100' } }) - - assert.equal(wrapper.state().valueToRender, '100') - assert.equal(wrapper.find('input').prop('value'), '100') - }) - -}) diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js index 4d0d36ab4..ceb620941 100644 --- a/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js +++ b/ui/app/components/send/send-content/send-amount-row/amount-max-button/amount-max-button.component.js @@ -34,21 +34,27 @@ export default class AmountMaxButton extends Component { }) } + onMaxClick = (event) => { + const { setMaxModeTo } = this.props + + event.preventDefault() + setMaxModeTo(true) + this.setMaxAmount() + } + render () { - const { setMaxModeTo, maxModeOn } = this.props - - return ( - <div - className="send-v2__amount-max" - onClick={(event) => { - event.preventDefault() - setMaxModeTo(true) - this.setMaxAmount() - }} - > - {!maxModeOn ? this.context.t('max') : ''} - </div> - ) + return this.props.maxModeOn + ? null + : ( + <div> + <span + className="send-v2__amount-max" + onClick={this.onMaxClick} + > + {this.context.t('max')} + </span> + </div> + ) } } diff --git a/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js b/ui/app/components/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js index 86a05ff21..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/send/send-content/send-amount-row/amount-max-button/tests/amount-max-button-component.test.js @@ -56,9 +56,8 @@ describe('AmountMaxButton Component', function () { }) describe('render', () => { - it('should render a div with a send-v2__amount-max class', () => { - assert.equal(wrapper.find('.send-v2__amount-max').length, 1) - assert(wrapper.find('.send-v2__amount-max').is('div')) + it('should render an element with a send-v2__amount-max class', () => { + assert(wrapper.exists('.send-v2__amount-max')) }) it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => { @@ -77,9 +76,9 @@ describe('AmountMaxButton Component', function () { ) }) - it('should not render text when maxModeOn is true', () => { + it('should not render anything when maxModeOn is true', () => { wrapper.setProps({ maxModeOn: true }) - assert.equal(wrapper.find('.send-v2__amount-max').text(), '') + assert.ok(!wrapper.exists('.send-v2__amount-max')) }) it('should render the expected text when maxModeOn is false', () => { diff --git a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js index c548a5695..0268376bf 100644 --- a/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js +++ b/ui/app/components/send/send-content/send-amount-row/send-amount-row.component.js @@ -2,7 +2,8 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import SendRowWrapper from '../send-row-wrapper/' import AmountMaxButton from './amount-max-button/' -import CurrencyDisplay from '../../currency-display' +import UserPreferencedCurrencyInput from '../../../user-preferenced-currency-input' +import UserPreferencedTokenInput from '../../../user-preferenced-token-input' export default class SendAmountRow extends Component { @@ -84,16 +85,25 @@ export default class SendAmountRow extends Component { } } + renderInput () { + const { amount, inError, selectedToken } = this.props + const Component = selectedToken ? UserPreferencedTokenInput : UserPreferencedCurrencyInput + + return ( + <Component + onChange={newAmount => this.validateAmount(newAmount)} + onBlur={newAmount => { + this.updateGas(newAmount) + this.updateAmount(newAmount) + }} + error={inError} + value={amount} + /> + ) + } + render () { - const { - amount, - amountConversionRate, - convertedCurrency, - gasTotal, - inError, - primaryCurrency, - selectedToken, - } = this.props + const { gasTotal, inError } = this.props return ( <SendRowWrapper @@ -102,20 +112,7 @@ export default class SendAmountRow extends Component { errorType={'amount'} > {!inError && gasTotal && <AmountMaxButton />} - <CurrencyDisplay - conversionRate={amountConversionRate} - convertedCurrency={convertedCurrency} - onBlur={newAmount => { - this.updateGas(newAmount) - this.updateAmount(newAmount) - }} - onChange={newAmount => this.validateAmount(newAmount)} - inError={inError} - primaryCurrency={primaryCurrency || 'ETH'} - selectedToken={selectedToken} - value={amount} - step="any" - /> + { this.renderInput() } </SendRowWrapper> ) } diff --git a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js index 8425e076e..56e80cb83 100644 --- a/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js +++ b/ui/app/components/send/send-content/send-amount-row/tests/send-amount-row-component.test.js @@ -6,7 +6,7 @@ import SendAmountRow from '../send-amount-row.component.js' import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' import AmountMaxButton from '../amount-max-button/amount-max-button.container' -import CurrencyDisplay from '../../../currency-display' +import UserPreferencedTokenInput from '../../../../user-preferenced-token-input' const propsMethodSpies = { setMaxModeTo: sinon.spy(), @@ -150,26 +150,18 @@ describe('SendAmountRow Component', function () { assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton)) }) - it('should render a CurrencyDisplay as the second child of the SendRowWrapper', () => { - assert(wrapper.find(SendRowWrapper).childAt(1).is(CurrencyDisplay)) + it('should render a UserPreferencedTokenInput as the second child of the SendRowWrapper', () => { + assert(wrapper.find(SendRowWrapper).childAt(1).is(UserPreferencedTokenInput)) }) - it('should render the CurrencyDisplay with the correct props', () => { + it('should render the UserPreferencedTokenInput with the correct props', () => { const { - conversionRate, - convertedCurrency, onBlur, onChange, - inError, - primaryCurrency, - selectedToken, + error, value, } = wrapper.find(SendRowWrapper).childAt(1).props() - assert.equal(conversionRate, 'mockAmountConversionRate') - assert.equal(convertedCurrency, 'mockConvertedCurrency') - assert.equal(inError, false) - assert.equal(primaryCurrency, 'mockPrimaryCurrency') - assert.deepEqual(selectedToken, { address: 'mockTokenAddress' }) + assert.equal(error, false) assert.equal(value, 'mockAmount') assert.equal(SendAmountRow.prototype.updateGas.callCount, 0) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0) @@ -192,11 +184,5 @@ describe('SendAmountRow Component', function () { ['mockNewAmount'] ) }) - - it('should pass the default primaryCurrency to the CurrencyDisplay if primaryCurrency is falsy', () => { - wrapper.setProps({ primaryCurrency: null }) - const { primaryCurrency } = wrapper.find(SendRowWrapper).childAt(1).props() - assert.equal(primaryCurrency, 'ETH') - }) }) }) diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js index bb9a94428..9bbb67506 100644 --- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js +++ b/ui/app/components/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 CurrencyDisplay from '../../../../send/currency-display' - +import UserPreferencedCurrencyDisplay from '../../../../user-preferenced-currency-display' +import { PRIMARY, SECONDARY } from '../../../../../constants/common' export default class GasFeeDisplay extends Component { @@ -19,27 +19,24 @@ export default class GasFeeDisplay extends Component { }; render () { - const { - conversionRate, - gasTotal, - onClick, - primaryCurrency = 'ETH', - convertedCurrency, - gasLoadingError, - } = this.props + const { gasTotal, onClick, gasLoadingError } = this.props return ( <div className="send-v2__gas-fee-display"> {gasTotal - ? <CurrencyDisplay - primaryCurrency={primaryCurrency} - convertedCurrency={convertedCurrency} - value={gasTotal} - conversionRate={conversionRate} - gasLoadingError={gasLoadingError} - convertedPrefix={'$'} - readOnly - /> + ? ( + <div className="currency-display"> + <UserPreferencedCurrencyDisplay + value={gasTotal} + type={PRIMARY} + /> + <UserPreferencedCurrencyDisplay + className="currency-display__converted-value" + value={gasTotal} + type={SECONDARY} + /> + </div> + ) : gasLoadingError ? <div className="currency-display.currency-display--message"> {this.context.t('setGasPrice')} 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/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js index 7cbe8d0df..9ff01493a 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/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js @@ -2,7 +2,7 @@ import React from 'react' import assert from 'assert' import {shallow} from 'enzyme' import GasFeeDisplay from '../gas-fee-display.component' -import CurrencyDisplay from '../../../../../send/currency-display' +import UserPreferencedCurrencyDisplay from '../../../../../user-preferenced-currency-display' import sinon from 'sinon' @@ -29,17 +29,15 @@ describe('SendGasRow Component', function () { describe('render', () => { it('should render a CurrencyDisplay component', () => { - assert.equal(wrapper.find(CurrencyDisplay).length, 1) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) }) it('should render the CurrencyDisplay with the correct props', () => { const { - conversionRate, - convertedCurrency, + type, value, - } = wrapper.find(CurrencyDisplay).props() - assert.equal(conversionRate, 20) - assert.equal(convertedCurrency, 'mockConvertedCurrency') + } = wrapper.find(UserPreferencedCurrencyDisplay).at(0).props() + assert.equal(type, 'PRIMARY') assert.equal(value, 'mockGasTotal') }) diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js index 22e379693..eb22a08b7 100644 --- a/ui/app/components/send/send.selectors.js +++ b/ui/app/components/send/send.selectors.js @@ -19,6 +19,7 @@ const selectors = { getCurrentNetwork, getCurrentViewContext, getForceGasMin, + getNativeCurrency, getGasLimit, getGasPrice, getGasPriceFromRecentBlocks, @@ -111,6 +112,10 @@ function getCurrentCurrency (state) { return state.metamask.currentCurrency } +function getNativeCurrency (state) { + return state.metamask.nativeCurrency +} + function getCurrentNetwork (state) { return state.metamask.network } diff --git a/ui/app/components/send/send.utils.js b/ui/app/components/send/send.utils.js index a18a9e4b3..eb1667c63 100644 --- a/ui/app/components/send/send.utils.js +++ b/ui/app/components/send/send.utils.js @@ -215,7 +215,9 @@ async function estimateGas ({ // if recipient has no code, gas is 21k max: if (!selectedToken && !data) { const code = Boolean(to) && await global.eth.getCode(to) - if (!code || code === '0x') { + // Geth will return '0x', and ganache-core v2.2.1 will return '0x0' + const codeIsEmpty = !code || code === '0x' || code === '0x0' + if (codeIsEmpty) { return SIMPLE_GAS_COST } } else if (selectedToken && !to) { diff --git a/ui/app/components/send/tests/send-selectors-test-data.js b/ui/app/components/send/tests/send-selectors-test-data.js index 8b939dadb..30a2666cf 100644 --- a/ui/app/components/send/tests/send-selectors-test-data.js +++ b/ui/app/components/send/tests/send-selectors-test-data.js @@ -26,6 +26,7 @@ module.exports = { 'currentCurrency': 'USD', 'conversionRate': 1200.88200327, 'conversionDate': 1489013762, + 'nativeCurrency': 'ETH', 'noActiveNotices': true, 'frequentRpcList': [], 'network': '3', diff --git a/ui/app/components/send/tests/send-selectors.test.js b/ui/app/components/send/tests/send-selectors.test.js index 1a47cd209..e7e901f0d 100644 --- a/ui/app/components/send/tests/send-selectors.test.js +++ b/ui/app/components/send/tests/send-selectors.test.js @@ -12,6 +12,7 @@ const { getCurrentCurrency, getCurrentNetwork, getCurrentViewContext, + getNativeCurrency, getForceGasMin, getGasLimit, getGasPrice, @@ -178,6 +179,15 @@ describe('send selectors', () => { }) }) + describe('getNativeCurrency()', () => { + it('should return the ticker symbol of the selected network', () => { + assert.equal( + getNativeCurrency(mockState), + 'ETH' + ) + }) + }) + describe('getCurrentNetwork()', () => { it('should return the id of the currently selected network', () => { assert.equal( diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index b87bf959e..0461b615a 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -52,12 +52,12 @@ ShiftListItem.prototype.render = function () { }, }, [ h('img', { - src: 'https://info.shapeshift.io/sites/default/files/logo.png', + src: 'https://shapeshift.io/logo.png', style: { height: '35px', width: '132px', position: 'absolute', - clip: 'rect(0px,23px,34px,0px)', + clip: 'rect(0px,30px,34px,0px)', }, }), ]), @@ -132,7 +132,6 @@ ShiftListItem.prototype.renderInfo = function () { case 'no_deposits': return h('.flex-column', { style: { - width: '200px', overflow: 'hidden', }, }, [ diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js index d76eb5ef8..85af3b00b 100644 --- a/ui/app/components/signature-request.js +++ b/ui/app/components/signature-request.js @@ -2,7 +2,7 @@ const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') const inherits = require('util').inherits -const Identicon = require('./identicon') +import Identicon from './identicon' const connect = require('react-redux').connect const ethUtil = require('ethereumjs-util') const classnames = require('classnames') diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 477d97597..75ba347fa 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -2,7 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect -const Identicon = require('./identicon') +import Identicon from './identicon' const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') const selectors = require('../selectors') const actions = require('../actions') diff --git a/ui/app/components/token-input/index.js b/ui/app/components/token-input/index.js new file mode 100644 index 000000000..22c06111e --- /dev/null +++ b/ui/app/components/token-input/index.js @@ -0,0 +1 @@ +export { default } from './token-input.container' diff --git a/ui/app/components/token-input/tests/token-input.component.test.js b/ui/app/components/token-input/tests/token-input.component.test.js new file mode 100644 index 000000000..2dacb9bc4 --- /dev/null +++ b/ui/app/components/token-input/tests/token-input.component.test.js @@ -0,0 +1,308 @@ +import React from 'react' +import PropTypes from 'prop-types' +import assert from 'assert' +import { shallow, mount } from 'enzyme' +import sinon from 'sinon' +import { Provider } from 'react-redux' +import configureMockStore from 'redux-mock-store' +import TokenInput from '../token-input.component' +import UnitInput from '../../unit-input' +import CurrencyDisplay from '../../currency-display' + +describe('TokenInput Component', () => { + const t = key => `translate ${key}` + + describe('rendering', () => { + it('should render properly without a token', () => { + const wrapper = shallow( + <TokenInput />, + { context: { t } } + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(UnitInput).length, 1) + }) + + it('should render properly with a token', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <TokenInput + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + /> + </Provider>, + { context: { t }, + childContextTypes: { + t: PropTypes.func, + }, + }, + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') + assert.equal(wrapper.find('.currency-input__conversion-component').length, 1) + assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'translate noConversionRateAvailable') + }) + + it('should render properly with a token and selectedTokenExchangeRate', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + + const wrapper = mount( + <Provider store={store}> + <TokenInput + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + selectedTokenExchangeRate={2} + /> + </Provider>, + { context: { t }, + childContextTypes: { + t: PropTypes.func, + }, + }, + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC') + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + }) + + it('should render properly with a token value for ETH', () => { + 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} + /> + </Provider> + ) + + 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-display-component').text(), '2ETH') + }) + + it('should render properly with a token value for fiat', () => { + 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 + /> + </Provider> + ) + + 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-display-component').text(), '$462.12USD') + }) + }) + + describe('handling actions', () => { + const handleChangeSpy = sinon.spy() + const handleBlurSpy = sinon.spy() + + afterEach(() => { + handleChangeSpy.resetHistory() + handleBlurSpy.resetHistory() + }) + + it('should call onChange and onBlur on input changes with the hex value for ETH', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = mount( + <Provider store={store}> + <TokenInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + selectedTokenExchangeRate={2} + /> + </Provider> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + + const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() + assert.equal(tokenInputInstance.state.decimalValue, 0) + assert.equal(tokenInputInstance.state.hexValue, undefined) + assert.equal(wrapper.find('.currency-display-component').text(), '0ETH') + 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('2710')) + assert.equal(wrapper.find('.currency-display-component').text(), '2ETH') + assert.equal(tokenInputInstance.state.decimalValue, 1) + assert.equal(tokenInputInstance.state.hexValue, '2710') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('2710')) + }) + + it('should call onChange and onBlur on input changes with the hex value for fiat', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = mount( + <Provider store={store}> + <TokenInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + selectedTokenExchangeRate={2} + showFiat + /> + </Provider> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + + const tokenInputInstance = wrapper.find(TokenInput).at(0).instance() + assert.equal(tokenInputInstance.state.decimalValue, 0) + assert.equal(tokenInputInstance.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('2710')) + assert.equal(wrapper.find('.currency-display-component').text(), '$462.12USD') + assert.equal(tokenInputInstance.state.decimalValue, 1) + assert.equal(tokenInputInstance.state.hexValue, '2710') + + assert.equal(handleBlurSpy.callCount, 0) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith('2710')) + }) + + it('should change the state and pass in a new decimalValue when props.value changes', () => { + const mockStore = { + metamask: { + currentCurrency: 'usd', + conversionRate: 231.06, + }, + } + const store = configureMockStore()(mockStore) + const wrapper = shallow( + <Provider store={store}> + <TokenInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + selectedToken={{ + address: '0x1', + decimals: '4', + symbol: 'ABC', + }} + suffix="ABC" + selectedTokenExchangeRate={2} + showFiat + /> + </Provider> + ) + + assert.ok(wrapper) + const tokenInputInstance = wrapper.find(TokenInput).dive() + assert.equal(tokenInputInstance.state('decimalValue'), 0) + assert.equal(tokenInputInstance.state('hexValue'), undefined) + assert.equal(tokenInputInstance.find(UnitInput).props().value, 0) + + tokenInputInstance.setProps({ value: '2710' }) + tokenInputInstance.update() + assert.equal(tokenInputInstance.state('decimalValue'), 1) + assert.equal(tokenInputInstance.state('hexValue'), '2710') + assert.equal(tokenInputInstance.find(UnitInput).props().value, 1) + }) + }) +}) 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 new file mode 100644 index 000000000..d73bc9a94 --- /dev/null +++ b/ui/app/components/token-input/tests/token-input.container.test.js @@ -0,0 +1,129 @@ +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/token-input/token-input.component.js b/ui/app/components/token-input/token-input.component.js new file mode 100644 index 000000000..d1388945b --- /dev/null +++ b/ui/app/components/token-input/token-input.component.js @@ -0,0 +1,136 @@ +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 ethUtil from 'ethereumjs-util' +import { conversionUtil, multiplyCurrencies } from '../../conversion-util' +import { ETH } from '../../constants/common' + +/** + * Component that allows user to enter token values as a number, and props receive a converted + * hex value. props.value, used as a default or forced value, should be a hex value, which + * gets converted into a decimal value. + */ +export default class TokenInput extends PureComponent { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + currentCurrency: PropTypes.string, + onChange: PropTypes.func, + onBlur: PropTypes.func, + value: PropTypes.string, + suffix: PropTypes.string, + showFiat: PropTypes.bool, + selectedToken: PropTypes.object, + selectedTokenExchangeRate: PropTypes.number, + } + + constructor (props) { + super(props) + + const { value: hexValue } = props + const decimalValue = hexValue ? this.getDecimalValue(props) : 0 + + this.state = { + decimalValue, + hexValue, + } + } + + componentDidUpdate (prevProps) { + const { value: prevPropsHexValue } = prevProps + const { value: propsHexValue } = this.props + const { hexValue: stateHexValue } = this.state + + if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) { + const decimalValue = this.getDecimalValue(this.props) + this.setState({ hexValue: propsHexValue, decimalValue }) + } + } + + getDecimalValue (props) { + const { value: hexValue, selectedToken: { decimals, symbol } = {} } = props + + const multiplier = Math.pow(10, Number(decimals || 0)) + const decimalValueString = conversionUtil(ethUtil.addHexPrefix(hexValue), { + fromNumericBase: 'hex', + toNumericBase: 'dec', + toCurrency: symbol, + conversionRate: multiplier, + invertConversionRate: true, + }) + + return Number(decimalValueString) || 0 + } + + handleChange = decimalValue => { + const { selectedToken: { decimals } = {}, onChange } = this.props + + const multiplier = Math.pow(10, Number(decimals || 0)) + const hexValue = multiplyCurrencies(decimalValue || 0, multiplier, { toNumericBase: 'hex' }) + + this.setState({ hexValue, decimalValue }) + onChange(hexValue) + } + + handleBlur = () => { + this.props.onBlur(this.state.hexValue) + } + + renderConversionComponent () { + const { selectedTokenExchangeRate, showFiat, currentCurrency } = this.props + const { decimalValue } = this.state + let currency, numberOfDecimals + + if (showFiat) { + // Display Fiat + currency = currentCurrency + numberOfDecimals = 2 + } else { + // Display ETH + currency = ETH + numberOfDecimals = 6 + } + + const decimalEthValue = (decimalValue * selectedTokenExchangeRate) || 0 + const hexWeiValue = getWeiHexFromDecimalValue({ + value: decimalEthValue, + fromCurrency: ETH, + fromDenomination: ETH, + }) + + return selectedTokenExchangeRate + ? ( + <CurrencyDisplay + className="currency-input__conversion-component" + currency={currency} + value={hexWeiValue} + numberOfDecimals={numberOfDecimals} + /> + ) : ( + <div className="currency-input__conversion-component"> + { this.context.t('noConversionRateAvailable') } + </div> + ) + } + + render () { + const { suffix, ...restProps } = this.props + const { decimalValue } = this.state + + return ( + <UnitInput + {...restProps} + suffix={suffix} + onChange={this.handleChange} + onBlur={this.handleBlur} + value={decimalValue} + > + { this.renderConversionComponent() } + </UnitInput> + ) + } +} diff --git a/ui/app/components/token-input/token-input.container.js b/ui/app/components/token-input/token-input.container.js new file mode 100644 index 000000000..ec233b1b8 --- /dev/null +++ b/ui/app/components/token-input/token-input.container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux' +import TokenInput from './token-input.component' +import { getSelectedToken, getSelectedTokenExchangeRate } from '../../selectors' + +const mapStateToProps = state => { + const { metamask: { currentCurrency } } = state + + return { + currentCurrency, + selectedToken: getSelectedToken(state), + selectedTokenExchangeRate: getSelectedTokenExchangeRate(state), + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { selectedToken } = stateProps + const suffix = selectedToken && selectedToken.symbol + + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + suffix, + } +} + +export default connect(mapStateToProps, null, mergeProps)(TokenInput) diff --git a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js index 85d56a6a2..a7c35f51e 100644 --- a/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js +++ b/ui/app/components/transaction-activity-log/tests/transaction-activity-log.container.test.js @@ -18,10 +18,11 @@ describe('TransactionActivityLog container', () => { const mockState = { metamask: { conversionRate: 280.45, + nativeCurrency: 'ETH', }, } - assert.deepEqual(mapStateToProps(mockState), { conversionRate: 280.45 }) + assert.deepEqual(mapStateToProps(mockState), { conversionRate: 280.45, nativeCurrency: 'ETH' }) }) }) }) 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 index c4cf57d14..58d932a0f 100644 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.component.js +++ b/ui/app/components/transaction-activity-log/transaction-activity-log.component.js @@ -4,7 +4,6 @@ import classnames from 'classnames' import { getActivities } from './transaction-activity-log.util' import Card from '../card' import { getEthConversionFromWeiHex, getValueFromWeiHex } from '../../helpers/conversions.util' -import { ETH } from '../../constants/common' import { formatDate } from '../../util' export default class TransactionActivityLog extends PureComponent { @@ -16,6 +15,7 @@ export default class TransactionActivityLog extends PureComponent { transaction: PropTypes.object, className: PropTypes.string, conversionRate: PropTypes.number, + nativeCurrency: PropTypes.string, } state = { @@ -27,10 +27,14 @@ export default class TransactionActivityLog extends PureComponent { } componentDidUpdate (prevProps) { - const { transaction: { history: prevHistory = [] } = {} } = prevProps - const { transaction: { history = [] } = {} } = this.props + const { + transaction: { history: prevHistory = [], txReceipt: { status: prevStatus } = {} } = {}, + } = prevProps + const { + transaction: { history = [], txReceipt: { status } = {} } = {}, + } = this.props - if (prevHistory.length !== history.length) { + if (prevHistory.length !== history.length || prevStatus !== status) { this.setActivites() } } @@ -41,16 +45,17 @@ export default class TransactionActivityLog extends PureComponent { } renderActivity (activity, index) { - const { conversionRate } = this.props + const { conversionRate, nativeCurrency } = this.props const { eventKey, value, timestamp } = activity const ethValue = index === 0 ? `${getValueFromWeiHex({ value, - toCurrency: ETH, + fromCurrency: nativeCurrency, + toCurrency: nativeCurrency, conversionRate, numberOfDecimals: 6, - })} ${ETH}` - : getEthConversionFromWeiHex({ value, toCurrency: ETH, conversionRate }) + })} ${nativeCurrency}` + : getEthConversionFromWeiHex({ value, fromCurrency: nativeCurrency, conversionRate }) const formattedTimestamp = formatDate(timestamp) const activityText = this.context.t(eventKey, [ethValue, formattedTimestamp]) 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 index 4c8b6d971..622f77df1 100644 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.container.js +++ b/ui/app/components/transaction-activity-log/transaction-activity-log.container.js @@ -1,10 +1,11 @@ import { connect } from 'react-redux' import TransactionActivityLog from './transaction-activity-log.component' -import { conversionRateSelector } from '../../selectors' +import { conversionRateSelector, getNativeCurrency } from '../../selectors' const mapStateToProps = state => { return { conversionRate: conversionRateSelector(state), + nativeCurrency: getNativeCurrency(state), } } 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 index 32834ff47..16597ae1a 100644 --- a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js +++ b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js @@ -18,6 +18,7 @@ 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, @@ -39,18 +40,22 @@ function eventCreator (eventKey, timestamp, value) { } export function getActivities (transaction) { - const { history = [] } = transaction + const { history = [], txReceipt: { status } = {} } = transaction - return history.reduce((acc, base) => { + 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 } = 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) { @@ -79,4 +84,10 @@ export function getActivities (transaction) { 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/index.js b/ui/app/components/transaction-breakdown/index.js index c887f504f..4a5b52663 100644 --- a/ui/app/components/transaction-breakdown/index.js +++ b/ui/app/components/transaction-breakdown/index.js @@ -1 +1 @@ -export { default } from './transaction-breakdown.component' +export { default } from './transaction-breakdown.container' diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js index bb6075e9f..3a7647873 100644 --- a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js +++ b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js @@ -4,8 +4,9 @@ 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 { ETH, GWEI } from '../../constants/common' +import { GWEI, PRIMARY, SECONDARY } from '../../constants/common' import { getHexGasTotal } from '../../helpers/confirm-transaction/util' import { sumHexes } from '../../helpers/transactions.util' @@ -17,6 +18,7 @@ export default class TransactionBreakdown extends PureComponent { static propTypes = { transaction: PropTypes.object, className: PropTypes.string, + nativeCurrency: PropTypes.string.isRequired, } static defaultProps = { @@ -25,9 +27,12 @@ export default class TransactionBreakdown extends PureComponent { render () { const { t } = this.context - const { transaction, className } = this.props - const { txParams: { gas, gasPrice, value } = {} } = transaction - const hexGasTotal = getHexGasTotal({ gasLimit: gas, gasPrice }) + 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 ( @@ -37,9 +42,9 @@ export default class TransactionBreakdown extends PureComponent { className="transaction-breakdown__card" > <TransactionBreakdownRow title={t('amount')}> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-breakdown__value" - currency={ETH} + type={PRIMARY} value={value} /> </TransactionBreakdownRow> @@ -52,10 +57,23 @@ export default class TransactionBreakdown extends PureComponent { 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={ETH} + currency={nativeCurrency} denomination={GWEI} value={gasPrice} hideLabel @@ -63,14 +81,14 @@ export default class TransactionBreakdown extends PureComponent { </TransactionBreakdownRow> <TransactionBreakdownRow title={t('total')}> <div> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-breakdown__value transaction-breakdown__value--eth-total" - currency={ETH} + type={PRIMARY} value={totalInHex} - numberOfDecimals={6} /> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-breakdown__value" + type={SECONDARY} value={totalInHex} /> </div> diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.container.js b/ui/app/components/transaction-breakdown/transaction-breakdown.container.js new file mode 100644 index 000000000..ed2708e03 --- /dev/null +++ b/ui/app/components/transaction-breakdown/transaction-breakdown.container.js @@ -0,0 +1,11 @@ +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 index 13cb51349..a4f28fd63 100644 --- 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 @@ -5,6 +5,7 @@ import { CARDS_VARIANT } from '../sender-to-recipient/sender-to-recipient.consta 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 { @@ -75,13 +76,15 @@ export default class TransactionListItemDetails extends PureComponent { </Button> ) } - <Button - type="raised" - onClick={this.handleEtherscanClick} - className="transaction-list-item-details__header-button" - > - <img src="/images/arrow-popout.svg" /> - </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"> diff --git a/ui/app/components/transaction-list-item/index.scss b/ui/app/components/transaction-list-item/index.scss index 9d694546b..ac0e7beeb 100644 --- a/ui/app/components/transaction-list-item/index.scss +++ b/ui/app/components/transaction-list-item/index.scss @@ -85,6 +85,7 @@ text-align: end; grid-area: primary-amount; align-self: end; + justify-self: end; @media screen and (max-width: $break-small) { padding-bottom: 2px; @@ -97,6 +98,7 @@ color: #5e6064; grid-area: secondary-amount; align-self: start; + justify-self: end; } } diff --git a/ui/app/components/transaction-list-item/transaction-list-item.component.js b/ui/app/components/transaction-list-item/transaction-list-item.component.js index c1c69f59b..696634fe0 100644 --- a/ui/app/components/transaction-list-item/transaction-list-item.component.js +++ b/ui/app/components/transaction-list-item/transaction-list-item.component.js @@ -4,13 +4,13 @@ import classnames from 'classnames' import Identicon from '../identicon' import TransactionStatus from '../transaction-status' import TransactionAction from '../transaction-action' -import CurrencyDisplay from '../currency-display' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import TokenCurrencyDisplay from '../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 { ETH } from '../../constants/common' -import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../app/scripts/lib/enums' +import { PRIMARY, SECONDARY } from '../../constants/common' +import { getStatusKey } from '../../helpers/transactions.util' export default class TransactionListItem extends PureComponent { static propTypes = { @@ -23,7 +23,6 @@ export default class TransactionListItem extends PureComponent { showCancelModal: PropTypes.func, showCancel: PropTypes.bool, showRetry: PropTypes.bool, - showTransactionDetailsModal: PropTypes.func, token: PropTypes.object, tokenData: PropTypes.object, transaction: PropTypes.object, @@ -38,31 +37,16 @@ export default class TransactionListItem extends PureComponent { const { transaction, history, - showTransactionDetailsModal, - methodData, - showCancel, - showRetry, } = this.props const { id, status } = transaction const { showTransactionDetails } = this.state - const windowType = window.METAMASK_UI_TYPE if (status === UNAPPROVED_STATUS) { history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`) return } - if (windowType === ENVIRONMENT_TYPE_FULLSCREEN) { - this.setState({ showTransactionDetails: !showTransactionDetails }) - } else { - showTransactionDetailsModal({ - transaction, - onRetry: this.handleRetry, - showRetry: showRetry && methodData.done, - onCancel: this.handleCancel, - showCancel, - }) - } + this.setState({ showTransactionDetails: !showTransactionDetails }) } handleCancel = () => { @@ -102,12 +86,11 @@ export default class TransactionListItem extends PureComponent { prefix="-" /> ) : ( - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-list-item__amount transaction-list-item__amount--primary" value={value} + type={PRIMARY} prefix="-" - numberOfDecimals={2} - currency={ETH} /> ) } @@ -118,10 +101,11 @@ export default class TransactionListItem extends PureComponent { return token ? null : ( - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-list-item__amount transaction-list-item__amount--secondary" - prefix="-" value={value} + prefix="-" + type={SECONDARY} /> ) } @@ -167,7 +151,7 @@ export default class TransactionListItem extends PureComponent { </div> <TransactionStatus className="transaction-list-item__status" - statusKey={transaction.status} + statusKey={getStatusKey(transaction)} title={( (transaction.err && transaction.err.rpc) ? transaction.err.rpc.message 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 index 72f5f5d61..62ed7a73f 100644 --- a/ui/app/components/transaction-list-item/transaction-list-item.container.js +++ b/ui/app/components/transaction-list-item/transaction-list-item.container.js @@ -28,16 +28,6 @@ const mapDispatchToProps = dispatch => { showCancelModal: (transactionId, originalGasPrice) => { return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice })) }, - showTransactionDetailsModal: ({ transaction, onRetry, showRetry, onCancel, showCancel }) => { - return dispatch(showModal({ - name: 'TRANSACTION_DETAILS', - transaction, - onRetry, - showRetry, - onCancel, - showCancel, - })) - }, } } diff --git a/ui/app/components/transaction-list/index.scss b/ui/app/components/transaction-list/index.scss index 777f701f9..ba7ffd87b 100644 --- a/ui/app/components/transaction-list/index.scss +++ b/ui/app/components/transaction-list/index.scss @@ -2,9 +2,7 @@ display: flex; flex-direction: column; flex: 1; - overflow-y: hidden; margin-top: 8px; - border-top: 1px solid $geyser; &__completed-transactions { display: flex; @@ -26,7 +24,6 @@ &__transactions { flex: 1; - overflow-y: auto; } &__pending-transactions { diff --git a/ui/app/components/transaction-status/index.scss b/ui/app/components/transaction-status/index.scss index 35be550f7..26a1f5d38 100644 --- a/ui/app/components/transaction-status/index.scss +++ b/ui/app/components/transaction-status/index.scss @@ -25,4 +25,9 @@ background-color: #FFF2DB; color: #CA810A; } -}
\ No newline at end of file + + &--failed { + background: lighten($monzo, 56%); + color: $monzo; + } +} diff --git a/ui/app/components/transaction-view-balance/index.scss b/ui/app/components/transaction-view-balance/index.scss index 12045ab6d..659f896ff 100644 --- a/ui/app/components/transaction-view-balance/index.scss +++ b/ui/app/components/transaction-view-balance/index.scss @@ -4,11 +4,13 @@ align-items: center; flex: 1; height: 54px; + min-width: 0; &__balance { - margin-left: 12px; + margin: 0 12px; display: flex; flex-direction: column; + min-width: 0; @media screen and (max-width: $break-small) { align-items: center; @@ -21,7 +23,8 @@ font-size: 1.5rem; @media screen and (max-width: $break-small) { - margin-bottom: 12px; + margin: 12px 0; + margin-left: 0; font-size: 1.75rem; } } @@ -30,7 +33,6 @@ font-size: 1.5rem; @media screen and (max-width: $break-small) { - margin-bottom: 12px; font-size: 1.75rem; } } @@ -45,6 +47,7 @@ display: flex; flex-direction: row; align-items: center; + min-width: 0; @media screen and (max-width: $break-small) { flex-direction: column; diff --git a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js index bb95cb27e..513a8aac9 100644 --- a/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js +++ b/ui/app/components/transaction-view-balance/tests/token-view-balance.component.test.js @@ -3,7 +3,7 @@ import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' import TokenBalance from '../../token-balance' -import CurrencyDisplay from '../../currency-display' +import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display' import { SEND_ROUTE } from '../../../routes' import TransactionViewBalance from '../transaction-view-balance.component' @@ -35,7 +35,7 @@ describe('TransactionViewBalance Component', () => { assert.equal(wrapper.find('.transaction-view-balance').length, 1) assert.equal(wrapper.find('.transaction-view-balance__button').length, 2) - assert.equal(wrapper.find(CurrencyDisplay).length, 2) + assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2) const buttons = wrapper.find('.transaction-view-balance__buttons') assert.equal(propsMethodSpies.showDepositModal.callCount, 0) 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 index 1b7a29c87..402b0f486 100644 --- a/ui/app/components/transaction-view-balance/transaction-view-balance.component.js +++ b/ui/app/components/transaction-view-balance/transaction-view-balance.component.js @@ -3,9 +3,9 @@ import PropTypes from 'prop-types' import Button from '../button' import Identicon from '../identicon' import TokenBalance from '../token-balance' -import CurrencyDisplay from '../currency-display' +import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import { SEND_ROUTE } from '../../routes' -import { ETH } from '../../constants/common' +import { PRIMARY, SECONDARY } from '../../constants/common' export default class TransactionViewBalance extends PureComponent { static contextTypes = { @@ -33,15 +33,17 @@ export default class TransactionViewBalance extends PureComponent { /> ) : ( <div className="transaction-view-balance__balance"> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-view-balance__primary-balance" value={balance} - currency={ETH} - numberOfDecimals={3} + type={PRIMARY} + ethNumberOfDecimals={4} /> - <CurrencyDisplay + <UserPreferencedCurrencyDisplay className="transaction-view-balance__secondary-balance" value={balance} + type={SECONDARY} + ethNumberOfDecimals={4} /> </div> ) diff --git a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js b/ui/app/components/transaction-view-balance/transaction-view-balance.container.js index 30c5cab16..cb8078ec1 100644 --- a/ui/app/components/transaction-view-balance/transaction-view-balance.container.js +++ b/ui/app/components/transaction-view-balance/transaction-view-balance.container.js @@ -2,7 +2,7 @@ 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, getSelectedTokenAssetImage } from '../../selectors' +import { getSelectedToken, getSelectedAddress, getNativeCurrency, getSelectedTokenAssetImage } from '../../selectors' import { showModal } from '../../actions' const mapStateToProps = state => { @@ -15,6 +15,7 @@ const mapStateToProps = state => { selectedToken: getSelectedToken(state), network, balance, + nativeCurrency: getNativeCurrency(state), assetImage: getSelectedTokenAssetImage(state), } } diff --git a/ui/app/components/transaction-view/index.scss b/ui/app/components/transaction-view/index.scss index af9771ce0..13187f0e5 100644 --- a/ui/app/components/transaction-view/index.scss +++ b/ui/app/components/transaction-view/index.scss @@ -4,6 +4,7 @@ min-width: 0; display: flex; flex-direction: column; + overflow-y: auto; &__balance-wrapper { @media screen and (max-width: $break-small) { diff --git a/ui/app/components/unit-input/index.js b/ui/app/components/unit-input/index.js new file mode 100644 index 000000000..7c33c9e5c --- /dev/null +++ b/ui/app/components/unit-input/index.js @@ -0,0 +1 @@ +export { default } from './unit-input.component' diff --git a/ui/app/components/unit-input/index.scss b/ui/app/components/unit-input/index.scss new file mode 100644 index 000000000..28c5bf6f0 --- /dev/null +++ b/ui/app/components/unit-input/index.scss @@ -0,0 +1,44 @@ +.unit-input { + min-height: 54px; + border: 1px solid #dedede; + border-radius: 4px; + background-color: #fff; + color: #4d4d4d; + font-size: 1rem; + padding: 8px 10px; + position: relative; + + input[type="number"] { + -moz-appearance: textfield; + } + + 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; + } + + &__input { + color: #4d4d4d; + font-size: 1rem; + font-family: Roboto; + border: none; + outline: 0 !important; + max-width: 22ch; + } + + &__input-container { + display: flex; + align-items: center; + } + + &--error { + border-color: $red; + } +} diff --git a/ui/app/components/unit-input/tests/unit-input.component.test.js b/ui/app/components/unit-input/tests/unit-input.component.test.js new file mode 100644 index 000000000..97d987bc7 --- /dev/null +++ b/ui/app/components/unit-input/tests/unit-input.component.test.js @@ -0,0 +1,146 @@ +import React from 'react' +import assert from 'assert' +import { shallow, mount } from 'enzyme' +import sinon from 'sinon' +import UnitInput from '../unit-input.component' + +describe('UnitInput Component', () => { + describe('rendering', () => { + it('should render properly without a suffix', () => { + const wrapper = shallow( + <UnitInput /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 0) + }) + + it('should render properly with a suffix', () => { + const wrapper = shallow( + <UnitInput + suffix="ETH" + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') + }) + + it('should render properly with a child omponent', () => { + const wrapper = shallow( + <UnitInput> + <div className="testing"> + TESTCOMPONENT + </div> + </UnitInput> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.testing').length, 1) + assert.equal(wrapper.find('.testing').text(), 'TESTCOMPONENT') + }) + + it('should render with an error class when props.error === true', () => { + const wrapper = shallow( + <UnitInput + error + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input--error').length, 1) + }) + }) + + describe('handling actions', () => { + const handleChangeSpy = sinon.spy() + const handleBlurSpy = sinon.spy() + + afterEach(() => { + handleChangeSpy.resetHistory() + handleBlurSpy.resetHistory() + }) + + it('should focus the input on component click', () => { + const wrapper = mount( + <UnitInput /> + ) + + assert.ok(wrapper) + const handleFocusSpy = sinon.spy(wrapper.instance(), 'handleFocus') + wrapper.instance().forceUpdate() + wrapper.update() + assert.equal(handleFocusSpy.callCount, 0) + wrapper.find('.unit-input').simulate('click') + assert.equal(handleFocusSpy.callCount, 1) + }) + + it('should call onChange on input changes with the value', () => { + const wrapper = mount( + <UnitInput + onChange={handleChangeSpy} + /> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + assert.equal(wrapper.state('value'), 123) + }) + + it('should call onBlur on blur with the value', () => { + const wrapper = mount( + <UnitInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + /> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + assert.equal(wrapper.state('value'), 123) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith(123)) + }) + + it('should set the component state value with props.value', () => { + const wrapper = mount( + <UnitInput + value={123} + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.state('value'), 123) + }) + + it('should update the component state value with props.value', () => { + const wrapper = mount( + <UnitInput + onChange={handleChangeSpy} + /> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(wrapper.state('value'), 123) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + wrapper.setProps({ value: 456 }) + assert.equal(wrapper.state('value'), 456) + assert.equal(handleChangeSpy.callCount, 1) + }) + }) +}) diff --git a/ui/app/components/unit-input/unit-input.component.js b/ui/app/components/unit-input/unit-input.component.js new file mode 100644 index 000000000..f1ebf4d77 --- /dev/null +++ b/ui/app/components/unit-input/unit-input.component.js @@ -0,0 +1,104 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { removeLeadingZeroes } from '../send/send.utils' + +/** + * Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also + * allows rendering a child component underneath the input to, for example, display conversions of + * the shown suffix. + */ +export default class UnitInput extends PureComponent { + static propTypes = { + children: PropTypes.node, + error: PropTypes.bool, + onBlur: PropTypes.func, + onChange: PropTypes.func, + placeholder: PropTypes.string, + suffix: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + } + + static defaultProps = { + placeholder: '0', + } + + constructor (props) { + super(props) + + this.state = { + value: props.value || '', + } + } + + componentDidUpdate (prevProps) { + const { value: prevPropsValue } = prevProps + const { value: propsValue } = this.props + const { value: stateValue } = this.state + + if (prevPropsValue !== propsValue && propsValue !== stateValue) { + this.setState({ value: propsValue }) + } + } + + handleFocus = () => { + this.unitInput.focus() + } + + handleChange = event => { + const { value: userInput } = event.target + let value = userInput + + if (userInput.length && userInput.length > 1) { + value = removeLeadingZeroes(userInput) + } + + this.setState({ value }) + this.props.onChange(value) + } + + handleBlur = event => { + const { onBlur } = this.props + typeof onBlur === 'function' && onBlur(this.state.value) + } + + getInputWidth (value) { + const valueString = String(value) + const valueLength = valueString.length || 1 + const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 + return (valueLength + decimalPointDeficit + 0.75) + 'ch' + } + + render () { + const { error, placeholder, suffix, children } = this.props + const { value } = this.state + + return ( + <div + 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> + { children } + </div> + ) + } +} diff --git a/ui/app/components/user-preferenced-currency-display/index.js b/ui/app/components/user-preferenced-currency-display/index.js new file mode 100644 index 000000000..0deddaecf --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/index.js @@ -0,0 +1 @@ +export { default } from './user-preferenced-currency-display.container' diff --git a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js new file mode 100644 index 000000000..ead584c26 --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.component.test.js @@ -0,0 +1,34 @@ +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' + +describe('UserPreferencedCurrencyDisplay Component', () => { + describe('rendering', () => { + it('should render properly', () => { + const wrapper = shallow( + <UserPreferencedCurrencyDisplay /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + }) + + it('should pass all props to the CurrencyDisplay child component', () => { + const wrapper = shallow( + <UserPreferencedCurrencyDisplay + prop1={true} + prop2="test" + prop3={1} + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + assert.equal(wrapper.find(CurrencyDisplay).props().prop1, true) + assert.equal(wrapper.find(CurrencyDisplay).props().prop2, 'test') + assert.equal(wrapper.find(CurrencyDisplay).props().prop3, 1) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js new file mode 100644 index 000000000..ba1c23d83 --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/tests/user-preferenced-currency-display.container.test.js @@ -0,0 +1,115 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps, mergeProps + +proxyquire('../user-preferenced-currency-display.container.js', { + 'react-redux': { + connect: (ms, md, mp) => { + mapStateToProps = ms + mergeProps = mp + return () => ({}) + }, + }, +}) + +describe('UserPreferencedCurrencyDisplay container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + nativeCurrency: 'ETH', + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + nativeCurrency: 'ETH', + useNativeCurrencyAsPrimaryCurrency: true, + }) + }) + }) + + describe('mergeProps()', () => { + it('should return the correct props', () => { + const mockDispatchProps = {} + + const tests = [ + { + stateProps: { + useNativeCurrencyAsPrimaryCurrency: true, + nativeCurrency: 'ETH', + }, + ownProps: { + type: 'PRIMARY', + }, + result: { + currency: 'ETH', + nativeCurrency: 'ETH', + numberOfDecimals: 6, + prefix: undefined, + }, + }, + { + stateProps: { + useNativeCurrencyAsPrimaryCurrency: false, + nativeCurrency: 'ETH', + }, + ownProps: { + type: 'PRIMARY', + }, + result: { + currency: undefined, + nativeCurrency: 'ETH', + numberOfDecimals: 2, + prefix: undefined, + }, + }, + { + stateProps: { + useNativeCurrencyAsPrimaryCurrency: true, + nativeCurrency: 'ETH', + }, + ownProps: { + type: 'SECONDARY', + fiatNumberOfDecimals: 4, + fiatPrefix: '-', + }, + result: { + nativeCurrency: 'ETH', + currency: undefined, + numberOfDecimals: 4, + prefix: '-', + }, + }, + { + stateProps: { + useNativeCurrencyAsPrimaryCurrency: false, + nativeCurrency: 'ETH', + }, + ownProps: { + type: 'SECONDARY', + fiatNumberOfDecimals: 4, + numberOfDecimals: 3, + fiatPrefix: 'a', + prefix: 'b', + }, + result: { + currency: 'ETH', + nativeCurrency: 'ETH', + numberOfDecimals: 3, + prefix: 'b', + }, + }, + ] + + tests.forEach(({ stateProps, ownProps, result }) => { + assert.deepEqual(mergeProps({ ...stateProps }, mockDispatchProps, { ...ownProps }), { + ...result, + }) + }) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js new file mode 100644 index 000000000..f2a834ea7 --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.component.js @@ -0,0 +1,46 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { PRIMARY, SECONDARY, ETH } from '../../constants/common' +import CurrencyDisplay from '../currency-display' + +export default class UserPreferencedCurrencyDisplay extends PureComponent { + static propTypes = { + className: PropTypes.string, + prefix: PropTypes.string, + value: PropTypes.string, + numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + hideLabel: PropTypes.bool, + style: PropTypes.object, + showEthLogo: PropTypes.bool, + ethLogoHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + // Used in container + type: PropTypes.oneOf([PRIMARY, SECONDARY]), + ethNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + fiatNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + ethPrefix: PropTypes.string, + fiatPrefix: PropTypes.string, + // From container + currency: PropTypes.string, + nativeCurrency: PropTypes.string, + } + + renderEthLogo () { + const { currency, showEthLogo, ethLogoHeight = 12 } = this.props + + return currency === ETH && showEthLogo && ( + <img + src="/images/eth.svg" + height={ethLogoHeight} + /> + ) + } + + render () { + return ( + <CurrencyDisplay + {...this.props} + prefixComponent={this.renderEthLogo()} + /> + ) + } +} diff --git a/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js new file mode 100644 index 000000000..7999301ad --- /dev/null +++ b/ui/app/components/user-preferenced-currency-display/user-preferenced-currency-display.container.js @@ -0,0 +1,54 @@ +import { connect } from 'react-redux' +import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display.component' +import { preferencesSelector } from '../../selectors' +import { ETH, PRIMARY, SECONDARY } from '../../constants/common' + +const mapStateToProps = (state, ownProps) => { + const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state) + + return { + useNativeCurrencyAsPrimaryCurrency, + nativeCurrency: state.metamask.nativeCurrency, + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { useNativeCurrencyAsPrimaryCurrency, nativeCurrency, ...restStateProps } = stateProps + const { + type, + numberOfDecimals: propsNumberOfDecimals, + ethNumberOfDecimals, + fiatNumberOfDecimals, + ethPrefix, + fiatPrefix, + prefix: propsPrefix, + ...restOwnProps + } = ownProps + + let currency, numberOfDecimals, prefix + + if (type === PRIMARY && useNativeCurrencyAsPrimaryCurrency || + type === SECONDARY && !useNativeCurrencyAsPrimaryCurrency) { + // Display ETH + currency = nativeCurrency || ETH + numberOfDecimals = propsNumberOfDecimals || ethNumberOfDecimals || 6 + prefix = propsPrefix || ethPrefix + } else if (type === SECONDARY && useNativeCurrencyAsPrimaryCurrency || + type === PRIMARY && !useNativeCurrencyAsPrimaryCurrency) { + // Display Fiat + numberOfDecimals = propsNumberOfDecimals || fiatNumberOfDecimals || 2 + prefix = propsPrefix || fiatPrefix + } + + return { + ...restStateProps, + ...dispatchProps, + ...restOwnProps, + nativeCurrency, + currency, + numberOfDecimals, + prefix, + } +} + +export default connect(mapStateToProps, null, mergeProps)(UserPreferencedCurrencyDisplay) diff --git a/ui/app/components/user-preferenced-currency-input/index.js b/ui/app/components/user-preferenced-currency-input/index.js new file mode 100644 index 000000000..4dc70db3d --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/index.js @@ -0,0 +1 @@ +export { default } from './user-preferenced-currency-input.container' diff --git a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js new file mode 100644 index 000000000..710b5d519 --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.component.test.js @@ -0,0 +1,32 @@ +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' + +describe('UserPreferencedCurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly', () => { + const wrapper = shallow( + <UserPreferencedCurrencyInput /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyInput).length, 1) + }) + + it('should render useFiat for CurrencyInput based on preferences.useNativeCurrencyAsPrimaryCurrency', () => { + const wrapper = shallow( + <UserPreferencedCurrencyInput + useNativeCurrencyAsPrimaryCurrency + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(CurrencyInput).length, 1) + assert.equal(wrapper.find(CurrencyInput).props().useFiat, false) + wrapper.setProps({ useNativeCurrencyAsPrimaryCurrency: false }) + assert.equal(wrapper.find(CurrencyInput).props().useFiat, true) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js new file mode 100644 index 000000000..959726443 --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/tests/user-preferenced-currency-input.container.test.js @@ -0,0 +1,31 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../user-preferenced-currency-input.container.js', { + 'react-redux': { + connect: ms => { + mapStateToProps = ms + return () => ({}) + }, + }, +}) + +describe('UserPreferencedCurrencyInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + useNativeCurrencyAsPrimaryCurrency: true, + }) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js new file mode 100644 index 000000000..463e66b80 --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.component.js @@ -0,0 +1,20 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import CurrencyInput from '../currency-input' + +export default class UserPreferencedCurrencyInput extends PureComponent { + static propTypes = { + useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, + } + + render () { + const { useNativeCurrencyAsPrimaryCurrency, ...restProps } = this.props + + return ( + <CurrencyInput + {...restProps} + useFiat={!useNativeCurrencyAsPrimaryCurrency} + /> + ) + } +} diff --git a/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js new file mode 100644 index 000000000..0b88eb5a7 --- /dev/null +++ b/ui/app/components/user-preferenced-currency-input/user-preferenced-currency-input.container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import UserPreferencedCurrencyInput from './user-preferenced-currency-input.component' +import { preferencesSelector } from '../../selectors' + +const mapStateToProps = state => { + const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state) + + return { + useNativeCurrencyAsPrimaryCurrency, + } +} + +export default connect(mapStateToProps)(UserPreferencedCurrencyInput) diff --git a/ui/app/components/user-preferenced-token-input/index.js b/ui/app/components/user-preferenced-token-input/index.js new file mode 100644 index 000000000..54167e633 --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/index.js @@ -0,0 +1 @@ +export { default } from './user-preferenced-token-input.container' diff --git a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js new file mode 100644 index 000000000..d85bddeeb --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.component.test.js @@ -0,0 +1,32 @@ +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' + +describe('UserPreferencedCurrencyInput Component', () => { + describe('rendering', () => { + it('should render properly', () => { + const wrapper = shallow( + <UserPreferencedTokenInput /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(TokenInput).length, 1) + }) + + it('should render showFiat for TokenInput based on preferences.useNativeCurrencyAsPrimaryCurrency', () => { + const wrapper = shallow( + <UserPreferencedTokenInput + useNativeCurrencyAsPrimaryCurrency + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find(TokenInput).length, 1) + assert.equal(wrapper.find(TokenInput).props().showFiat, false) + wrapper.setProps({ useNativeCurrencyAsPrimaryCurrency: false }) + assert.equal(wrapper.find(TokenInput).props().showFiat, true) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js new file mode 100644 index 000000000..2f89fba90 --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/tests/user-preferenced-token-input.container.test.js @@ -0,0 +1,31 @@ +import assert from 'assert' +import proxyquire from 'proxyquire' + +let mapStateToProps + +proxyquire('../user-preferenced-token-input.container.js', { + 'react-redux': { + connect: ms => { + mapStateToProps = ms + return () => ({}) + }, + }, +}) + +describe('UserPreferencedTokenInput container', () => { + describe('mapStateToProps()', () => { + it('should return the correct props', () => { + const mockState = { + metamask: { + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, + }, + } + + assert.deepEqual(mapStateToProps(mockState), { + useNativeCurrencyAsPrimaryCurrency: true, + }) + }) + }) +}) diff --git a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js new file mode 100644 index 000000000..8f14231ac --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.component.js @@ -0,0 +1,20 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import TokenInput from '../token-input' + +export default class UserPreferencedTokenInput extends PureComponent { + static propTypes = { + useNativeCurrencyAsPrimaryCurrency: PropTypes.bool, + } + + render () { + const { useNativeCurrencyAsPrimaryCurrency, ...restProps } = this.props + + return ( + <TokenInput + {...restProps} + showFiat={!useNativeCurrencyAsPrimaryCurrency} + /> + ) + } +} diff --git a/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js new file mode 100644 index 000000000..3305d4e29 --- /dev/null +++ b/ui/app/components/user-preferenced-token-input/user-preferenced-token-input.container.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux' +import UserPreferencedTokenInput from './user-preferenced-token-input.component' +import { preferencesSelector } from '../../selectors' + +const mapStateToProps = state => { + const { useNativeCurrencyAsPrimaryCurrency } = preferencesSelector(state) + + return { + useNativeCurrencyAsPrimaryCurrency, + } +} + +export default connect(mapStateToProps)(UserPreferencedTokenInput) diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 064a6ab55..e050e0ee6 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -7,7 +7,7 @@ const { compose } = require('recompose') const inherits = require('util').inherits const classnames = require('classnames') const { checksumAddress } = require('../util') -const Identicon = require('./identicon') +import Identicon from './identicon' // const AccountDropdowns = require('./dropdowns/index.js').AccountDropdowns const Tooltip = require('./tooltip-v2.js').default const copyToClipboard = require('copy-to-clipboard') @@ -17,7 +17,7 @@ const TokenList = require('./token-list') const selectors = require('../selectors') const { ADD_TOKEN_ROUTE } = require('../routes') -import Button from './button' +import AddTokenButton from './add-token-button' module.exports = compose( withRouter, @@ -100,19 +100,33 @@ WalletView.prototype.renderWalletBalance = function () { ]) } +WalletView.prototype.renderAddToken = function () { + const { + sidebarOpen, + hideSidebar, + history, + } = this.props + + return h(AddTokenButton, { + onClick () { + history.push(ADD_TOKEN_ROUTE) + if (sidebarOpen) { + hideSidebar() + } + }, + }) +} + WalletView.prototype.render = function () { const { responsiveDisplayClassname, selectedAddress, keyrings, showAccountDetailModal, - sidebarOpen, hideSidebar, - history, identities, } = this.props // temporary logs + fake extra wallets - // console.log('walletview, selectedAccount:', selectedAccount) const checksummedAddress = checksumAddress(selectedAddress) @@ -201,14 +215,7 @@ WalletView.prototype.render = function () { h(TokenList), - h(Button, { - type: 'primary', - className: 'wallet-view__add-token-button', - onClick: () => { - history.push(ADD_TOKEN_ROUTE) - sidebarOpen && hideSidebar() - }, - }, this.context.t('addToken')), + this.renderAddToken(), ]) } diff --git a/ui/app/constants/common.js b/ui/app/constants/common.js index a20f6cc02..4ff4dc837 100644 --- a/ui/app/constants/common.js +++ b/ui/app/constants/common.js @@ -1,3 +1,6 @@ export const ETH = 'ETH' export const GWEI = 'GWEI' export const WEI = 'WEI' + +export const PRIMARY = 'PRIMARY' +export const SECONDARY = 'SECONDARY' diff --git a/ui/app/constants/error-keys.js b/ui/app/constants/error-keys.js index f70ed3b19..704064c96 100644 --- a/ui/app/constants/error-keys.js +++ b/ui/app/constants/error-keys.js @@ -1,3 +1,4 @@ export const INSUFFICIENT_FUNDS_ERROR_KEY = 'insufficientFunds' export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow' export const TRANSACTION_ERROR_KEY = 'transactionError' +export const TRANSACTION_NO_CONTRACT_ERROR_KEY = 'transactionErrorNoContract' diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss index 655188a3e..5826a8b49 100644 --- a/ui/app/css/itcss/components/buttons.scss +++ b/ui/app/css/itcss/components/buttons.scss @@ -3,7 +3,7 @@ */ .button { - height: 44px; + min-height: 44px; background: $white; display: flex; justify-content: center; @@ -82,12 +82,13 @@ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08); padding: 6px; height: initial; + min-height: initial; width: initial; min-width: initial; } .btn--large { - height: 54px; + min-height: 54px; } .btn-green { diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index 8e963d495..233e781ef 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -120,18 +120,6 @@ $wallet-view-bg: $alabaster; } } } - - &__add-token-button { - flex: 0 0 auto; - margin: 36px auto; - background: none; - transition: border-color .3s ease; - width: 150px; - - &:hover { - border-color: $curious-blue; - } - } } @media screen and (min-width: 576px) { diff --git a/ui/app/css/itcss/components/pages/index.scss b/ui/app/css/itcss/components/pages/index.scss index 709f8baf6..45e47f447 100644 --- a/ui/app/css/itcss/components/pages/index.scss +++ b/ui/app/css/itcss/components/pages/index.scss @@ -1 +1,3 @@ @import './reveal-seed.scss'; + +@import './provider-approval.scss'; diff --git a/ui/app/css/itcss/components/pages/provider-approval.scss b/ui/app/css/itcss/components/pages/provider-approval.scss new file mode 100644 index 000000000..f172165ab --- /dev/null +++ b/ui/app/css/itcss/components/pages/provider-approval.scss @@ -0,0 +1,11 @@ +.provider_approval_content { + height: auto; + overflow: auto; + padding: 16px; +} + +.provider_approval_origin { + font-weight: 999; + margin-top: 16px; + word-wrap: break-word; +} diff --git a/ui/app/css/itcss/components/request-signature.scss b/ui/app/css/itcss/components/request-signature.scss index 445b9ebf5..6c950d846 100644 --- a/ui/app/css/itcss/components/request-signature.scss +++ b/ui/app/css/itcss/components/request-signature.scss @@ -19,7 +19,7 @@ } @media screen and (min-width: $break-large) { - max-height: 620px; + height: 620px; } } diff --git a/ui/app/css/itcss/components/wallet-balance.scss b/ui/app/css/itcss/components/wallet-balance.scss index 293771550..3c3349ae0 100644 --- a/ui/app/css/itcss/components/wallet-balance.scss +++ b/ui/app/css/itcss/components/wallet-balance.scss @@ -29,7 +29,7 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( align-items: center; margin: 20px 24px; flex-direction: row; - flex-grow: 3; + min-width: 0; @media #{$wallet-balance-breakpoint-range} { margin: 10% 4%; @@ -38,8 +38,7 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( .balance-display { margin-left: 15px; - justify-content: flex-start; - align-items: flex-start; + min-width: 0; .token-amount { font-size: 1.5rem; diff --git a/ui/app/ducks/confirm-transaction.duck.js b/ui/app/ducks/confirm-transaction.duck.js index 30c32f2bf..275eb1551 100644 --- a/ui/app/ducks/confirm-transaction.duck.js +++ b/ui/app/ducks/confirm-transaction.duck.js @@ -2,6 +2,7 @@ import { conversionRateSelector, currentCurrencySelector, unconfirmedTransactionsHashSelector, + getNativeCurrency, } from '../selectors/confirm-transaction' import { @@ -14,7 +15,13 @@ import { hexGreaterThan, } from '../helpers/confirm-transaction/util' -import { getTokenData, getMethodData, isSmartContractAddress } from '../helpers/transactions.util' +import { + getTokenData, + getMethodData, + isSmartContractAddress, + sumHexes, +} from '../helpers/transactions.util' + import { getSymbolAndDecimals } from '../token-util' import { conversionUtil } from '../conversion-util' @@ -31,7 +38,6 @@ const CLEAR_CONFIRM_TRANSACTION = createActionType('CLEAR_CONFIRM_TRANSACTION') const UPDATE_TRANSACTION_AMOUNTS = createActionType('UPDATE_TRANSACTION_AMOUNTS') const UPDATE_TRANSACTION_FEES = createActionType('UPDATE_TRANSACTION_FEES') const UPDATE_TRANSACTION_TOTALS = createActionType('UPDATE_TRANSACTION_TOTALS') -const UPDATE_HEX_GAS_TOTAL = createActionType('UPDATE_HEX_GAS_TOTAL') const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS') const UPDATE_NONCE = createActionType('UPDATE_NONCE') const UPDATE_TO_SMART_CONTRACT = createActionType('UPDATE_TO_SMART_CONTRACT') @@ -53,7 +59,9 @@ const initState = { ethTransactionAmount: '', ethTransactionFee: '', ethTransactionTotal: '', - hexGasTotal: '', + hexTransactionAmount: '', + hexTransactionFee: '', + hexTransactionTotal: '', nonce: '', toSmartContract: false, fetchingData: false, @@ -99,30 +107,28 @@ export default function reducer ({ confirmTransaction: confirmState = initState methodData: {}, } case UPDATE_TRANSACTION_AMOUNTS: - const { fiatTransactionAmount, ethTransactionAmount } = action.payload + const { fiatTransactionAmount, ethTransactionAmount, hexTransactionAmount } = action.payload return { ...confirmState, fiatTransactionAmount: fiatTransactionAmount || confirmState.fiatTransactionAmount, ethTransactionAmount: ethTransactionAmount || confirmState.ethTransactionAmount, + hexTransactionAmount: hexTransactionAmount || confirmState.hexTransactionAmount, } case UPDATE_TRANSACTION_FEES: - const { fiatTransactionFee, ethTransactionFee } = action.payload + const { fiatTransactionFee, ethTransactionFee, hexTransactionFee } = action.payload return { ...confirmState, fiatTransactionFee: fiatTransactionFee || confirmState.fiatTransactionFee, ethTransactionFee: ethTransactionFee || confirmState.ethTransactionFee, + hexTransactionFee: hexTransactionFee || confirmState.hexTransactionFee, } case UPDATE_TRANSACTION_TOTALS: - const { fiatTransactionTotal, ethTransactionTotal } = action.payload + const { fiatTransactionTotal, ethTransactionTotal, hexTransactionTotal } = action.payload return { ...confirmState, fiatTransactionTotal: fiatTransactionTotal || confirmState.fiatTransactionTotal, ethTransactionTotal: ethTransactionTotal || confirmState.ethTransactionTotal, - } - case UPDATE_HEX_GAS_TOTAL: - return { - ...confirmState, - hexGasTotal: action.payload, + hexTransactionTotal: hexTransactionTotal || confirmState.hexTransactionTotal, } case UPDATE_TOKEN_PROPS: const { tokenSymbol = '', tokenDecimals = '' } = action.payload @@ -222,13 +228,6 @@ export function updateTransactionTotals (totals) { } } -export function updateHexGasTotal (hexGasTotal) { - return { - type: UPDATE_HEX_GAS_TOTAL, - payload: hexGasTotal, - } -} - export function updateTokenProps (tokenProps) { return { type: UPDATE_TOKEN_PROPS, @@ -294,43 +293,53 @@ export function updateTxDataAndCalculate (txData) { const state = getState() const currentCurrency = currentCurrencySelector(state) const conversionRate = conversionRateSelector(state) + const nativeCurrency = getNativeCurrency(state) dispatch(updateTxData(txData)) - const { txParams: { value, gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData + const { txParams: { value = '0x0', gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData const fiatTransactionAmount = getValueFromWeiHex({ - value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2, + value, fromCurrency: nativeCurrency, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2, }) const ethTransactionAmount = getValueFromWeiHex({ - value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6, + value, fromCurrency: nativeCurrency, toCurrency: nativeCurrency, conversionRate, numberOfDecimals: 6, }) - dispatch(updateTransactionAmounts({ fiatTransactionAmount, ethTransactionAmount })) - - const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice }) + dispatch(updateTransactionAmounts({ + fiatTransactionAmount, + ethTransactionAmount, + hexTransactionAmount: value, + })) - dispatch(updateHexGasTotal(hexGasTotal)) + const hexTransactionFee = getHexGasTotal({ gasLimit, gasPrice }) const fiatTransactionFee = getTransactionFee({ - value: hexGasTotal, + value: hexTransactionFee, + fromCurrency: nativeCurrency, toCurrency: currentCurrency, numberOfDecimals: 2, conversionRate, }) const ethTransactionFee = getTransactionFee({ - value: hexGasTotal, - toCurrency: 'ETH', + value: hexTransactionFee, + fromCurrency: nativeCurrency, + toCurrency: nativeCurrency, numberOfDecimals: 6, conversionRate, }) - dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee })) + dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee, hexTransactionFee })) const fiatTransactionTotal = addFiat(fiatTransactionFee, fiatTransactionAmount) const ethTransactionTotal = addEth(ethTransactionFee, ethTransactionAmount) + const hexTransactionTotal = sumHexes(value, hexTransactionFee) - dispatch(updateTransactionTotals({ fiatTransactionTotal, ethTransactionTotal })) + dispatch(updateTransactionTotals({ + fiatTransactionTotal, + ethTransactionTotal, + hexTransactionTotal, + })) } } diff --git a/ui/app/ducks/tests/confirm-transaction.duck.test.js b/ui/app/ducks/tests/confirm-transaction.duck.test.js index 1bab0add0..eceacd0bd 100644 --- a/ui/app/ducks/tests/confirm-transaction.duck.test.js +++ b/ui/app/ducks/tests/confirm-transaction.duck.test.js @@ -19,7 +19,9 @@ const initialState = { ethTransactionAmount: '', ethTransactionFee: '', ethTransactionTotal: '', - hexGasTotal: '', + hexTransactionAmount: '', + hexTransactionFee: '', + hexTransactionTotal: '', nonce: '', toSmartContract: false, fetchingData: false, @@ -34,7 +36,6 @@ const CLEAR_METHOD_DATA = 'metamask/confirm-transaction/CLEAR_METHOD_DATA' const UPDATE_TRANSACTION_AMOUNTS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS' const UPDATE_TRANSACTION_FEES = 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES' const UPDATE_TRANSACTION_TOTALS = 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS' -const UPDATE_HEX_GAS_TOTAL = 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL' const UPDATE_TOKEN_PROPS = 'metamask/confirm-transaction/UPDATE_TOKEN_PROPS' const UPDATE_NONCE = 'metamask/confirm-transaction/UPDATE_NONCE' const UPDATE_TO_SMART_CONTRACT = 'metamask/confirm-transaction/UPDATE_TO_SMART_CONTRACT' @@ -65,7 +66,9 @@ describe('Confirm Transaction Duck', () => { ethTransactionAmount: '1', ethTransactionFee: '0.000021', ethTransactionTotal: '469.27', - hexGasTotal: '0x1319718a5000', + hexTransactionAmount: '', + hexTransactionFee: '0x1319718a5000', + hexTransactionTotal: '', nonce: '0x0', toSmartContract: false, fetchingData: false, @@ -186,12 +189,14 @@ describe('Confirm Transaction Duck', () => { payload: { fiatTransactionAmount: '123.45', ethTransactionAmount: '.5', + hexTransactionAmount: '0x1', }, }), { ...mockState.confirmTransaction, fiatTransactionAmount: '123.45', ethTransactionAmount: '.5', + hexTransactionAmount: '0x1', } ) }) @@ -203,12 +208,14 @@ describe('Confirm Transaction Duck', () => { payload: { fiatTransactionFee: '123.45', ethTransactionFee: '.5', + hexTransactionFee: '0x1', }, }), { ...mockState.confirmTransaction, fiatTransactionFee: '123.45', ethTransactionFee: '.5', + hexTransactionFee: '0x1', } ) }) @@ -220,25 +227,14 @@ describe('Confirm Transaction Duck', () => { payload: { fiatTransactionTotal: '123.45', ethTransactionTotal: '.5', + hexTransactionTotal: '0x1', }, }), { ...mockState.confirmTransaction, fiatTransactionTotal: '123.45', ethTransactionTotal: '.5', - } - ) - }) - - it('should update hexGasTotal when receiving an UPDATE_HEX_GAS_TOTAL action', () => { - assert.deepEqual( - ConfirmTransactionReducer(mockState, { - type: UPDATE_HEX_GAS_TOTAL, - payload: '0x0', - }), - { - ...mockState.confirmTransaction, - hexGasTotal: '0x0', + hexTransactionTotal: '0x1', } ) }) @@ -435,19 +431,6 @@ describe('Confirm Transaction Duck', () => { ) }) - it('should create an action to update hexGasTotal', () => { - const hexGasTotal = '0x0' - const expectedAction = { - type: UPDATE_HEX_GAS_TOTAL, - payload: hexGasTotal, - } - - assert.deepEqual( - actions.updateHexGasTotal(hexGasTotal), - expectedAction - ) - }) - it('should create an action to update tokenProps', () => { const tokenProps = { tokenDecimals: '1', @@ -568,7 +551,6 @@ describe('Confirm Transaction Duck', () => { const expectedActions = [ 'metamask/confirm-transaction/UPDATE_TX_DATA', 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS', - 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL', 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES', 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS', ] @@ -637,7 +619,6 @@ describe('Confirm Transaction Duck', () => { const expectedActions = [ 'metamask/confirm-transaction/UPDATE_TX_DATA', 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS', - 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL', 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES', 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS', ] @@ -687,7 +668,6 @@ describe('Confirm Transaction Duck', () => { const expectedActions = [ 'metamask/confirm-transaction/UPDATE_TX_DATA', 'metamask/confirm-transaction/UPDATE_TRANSACTION_AMOUNTS', - 'metamask/confirm-transaction/UPDATE_HEX_GAS_TOTAL', 'metamask/confirm-transaction/UPDATE_TRANSACTION_FEES', 'metamask/confirm-transaction/UPDATE_TRANSACTION_TOTALS', ] diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js index bcac22500..eb334a4b8 100644 --- a/ui/app/helpers/confirm-transaction/util.js +++ b/ui/app/helpers/confirm-transaction/util.js @@ -55,6 +55,7 @@ export function addFiat (...args) { export function getValueFromWeiHex ({ value, + fromCurrency = 'ETH', toCurrency, conversionRate, numberOfDecimals, @@ -63,7 +64,7 @@ export function getValueFromWeiHex ({ return conversionUtil(value, { fromNumericBase: 'hex', toNumericBase: 'dec', - fromCurrency: 'ETH', + fromCurrency, toCurrency, numberOfDecimals, fromDenomination: 'WEI', @@ -74,6 +75,7 @@ export function getValueFromWeiHex ({ export function getTransactionFee ({ value, + fromCurrency = 'ETH', toCurrency, conversionRate, numberOfDecimals, @@ -82,7 +84,7 @@ export function getTransactionFee ({ fromNumericBase: 'BN', toNumericBase: 'dec', fromDenomination: 'WEI', - fromCurrency: 'ETH', + fromCurrency, toCurrency, numberOfDecimals, conversionRate, @@ -99,6 +101,7 @@ export function formatCurrency (value, currencyCode) { export function convertTokenToFiat ({ value, + fromCurrency = 'ETH', toCurrency, conversionRate, contractExchangeRate, @@ -108,6 +111,7 @@ export function convertTokenToFiat ({ return conversionUtil(value, { fromNumericBase: 'dec', toNumericBase: 'dec', + fromCurrency, toCurrency, numberOfDecimals: 2, conversionRate: totalExchangeRate, diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js index 20ef9e35b..cb5e1b90b 100644 --- a/ui/app/helpers/conversions.util.js +++ b/ui/app/helpers/conversions.util.js @@ -20,8 +20,8 @@ export function decimalToHex (decimal) { }) } -export function getEthConversionFromWeiHex ({ value, conversionRate, numberOfDecimals = 6 }) { - const denominations = [ETH, GWEI, WEI] +export function getEthConversionFromWeiHex ({ value, fromCurrency = ETH, conversionRate, numberOfDecimals = 6 }) { + const denominations = [fromCurrency, GWEI, WEI] let nonZeroDenomination @@ -29,7 +29,8 @@ export function getEthConversionFromWeiHex ({ value, conversionRate, numberOfDec const convertedValue = getValueFromWeiHex({ value, conversionRate, - toCurrency: ETH, + fromCurrency, + toCurrency: fromCurrency, numberOfDecimals, toDenomination: denominations[i], }) @@ -45,6 +46,7 @@ export function getEthConversionFromWeiHex ({ value, conversionRate, numberOfDec export function getValueFromWeiHex ({ value, + fromCurrency = ETH, toCurrency, conversionRate, numberOfDecimals, @@ -53,7 +55,7 @@ export function getValueFromWeiHex ({ return conversionUtil(value, { fromNumericBase: 'hex', toNumericBase: 'dec', - fromCurrency: ETH, + fromCurrency, toCurrency, numberOfDecimals, fromDenomination: WEI, @@ -61,3 +63,22 @@ export function getValueFromWeiHex ({ conversionRate, }) } + +export function getWeiHexFromDecimalValue ({ + value, + fromCurrency, + conversionRate, + fromDenomination, + invertConversionRate, +}) { + return conversionUtil(value, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + toCurrency: ETH, + fromCurrency, + conversionRate, + invertConversionRate, + fromDenomination, + toDenomination: WEI, + }) +} diff --git a/ui/app/helpers/tests/transactions.util.test.js b/ui/app/helpers/tests/transactions.util.test.js index 103a84a8c..838522e35 100644 --- a/ui/app/helpers/tests/transactions.util.test.js +++ b/ui/app/helpers/tests/transactions.util.test.js @@ -19,4 +19,39 @@ describe('Transactions utils', () => { assert.doesNotThrow(() => utils.getTokenData()) }) }) + + describe('getStatusKey', () => { + it('should return the correct status', () => { + const tests = [ + { + transaction: { + status: 'confirmed', + txReceipt: { + status: '0x0', + }, + }, + expected: 'failed', + }, + { + transaction: { + status: 'confirmed', + txReceipt: { + status: '0x1', + }, + }, + expected: 'confirmed', + }, + { + transaction: { + status: 'pending', + }, + expected: 'pending', + }, + ] + + tests.forEach(({ transaction, expected }) => { + assert.equal(utils.getStatusKey(transaction), expected) + }) + }) + }) }) diff --git a/ui/app/helpers/transactions.util.js b/ui/app/helpers/transactions.util.js index f7d249e63..2f4b1d095 100644 --- a/ui/app/helpers/transactions.util.js +++ b/ui/app/helpers/transactions.util.js @@ -27,10 +27,21 @@ export function getTokenData (data = '') { const registry = new MethodRegistry({ provider: global.ethereumProvider }) +/** + * Attempts to return the method data from the MethodRegistry library, if the method exists in the + * registry. Otherwise, returns an empty object. + * @param {string} data - The hex data (@code txParams.data) of a transaction + * @returns {Object} + */ export async function getMethodData (data = '') { const prefixedData = ethUtil.addHexPrefix(data) const fourBytePrefix = prefixedData.slice(0, 10) const sig = await registry.lookup(fourBytePrefix) + + if (!sig) { + return {} + } + const parsedResult = registry.parse(sig) return { @@ -114,7 +125,9 @@ export function getLatestSubmittedTxWithNonce (transactions = [], nonce = '0x0') export async function isSmartContractAddress (address) { const code = await global.eth.getCode(address) - return code && code !== '0x' + // Geth will return '0x', and ganache-core v2.2.1 will return '0x0' + const codeIsEmpty = !code || code === '0x' || code === '0x0' + return !codeIsEmpty } export function sumHexes (...args) { @@ -126,3 +139,21 @@ export function sumHexes (...args) { return ethUtil.addHexPrefix(total) } + +/** + * Returns a status key for a transaction. Requires parsing the txMeta.txReceipt on top of + * txMeta.status because txMeta.status does not reflect on-chain errors. + * @param {Object} transaction - The txMeta object of a transaction. + * @param {Object} transaction.txReceipt - The transaction receipt. + * @returns {string} + */ +export function getStatusKey (transaction) { + const { txReceipt: { status } = {} } = transaction + + // There was an on-chain failure + if (status === '0x0') { + return 'failed' + } + + return transaction.status +} diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 80e76d570..e1a982f93 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -1,3 +1,4 @@ +const clone = require('clone') const extend = require('xtend') const copyToClipboard = require('copy-to-clipboard') @@ -52,19 +53,24 @@ function rootReducer (state, action) { return state } +window.getCleanAppState = function () { + const state = clone(window.METAMASK_CACHED_LOG_STATE) + // append additional information + state.version = global.platform.getVersion() + state.browser = window.navigator.userAgent + // ensure seedWords are not included + if (state.metamask) delete state.metamask.seedWords + if (state.appState.currentView) delete state.appState.currentView.seedWords + return state +} + window.logStateString = function (cb) { - const state = window.METAMASK_CACHED_LOG_STATE - const version = global.platform.getVersion() - const browser = window.navigator.userAgent - return global.platform.getPlatformInfo((err, platform) => { - if (err) { - return cb(err) - } - state.version = version + const state = window.getCleanAppState() + global.platform.getPlatformInfo((err, platform) => { + if (err) return cb(err) state.platform = platform - state.browser = browser const stateString = JSON.stringify(state, removeSeedWords, 2) - return cb(null, stateString) + cb(null, stateString) }) } diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 3f1d3394f..dfa2f3656 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -51,6 +51,9 @@ function reduceMetamask (state, action) { isRevealingSeedWords: false, welcomeScreenSeen: false, currentLocale: '', + preferences: { + useNativeCurrencyAsPrimaryCurrency: true, + }, }, state.metamask) switch (action.type) { @@ -71,6 +74,7 @@ function reduceMetamask (state, action) { case actions.CLEAR_NOTICES: return extend(metamaskState, { noActiveNotices: true, + nextUnreadNotice: undefined, }) case actions.UPDATE_METAMASK_STATE: @@ -291,8 +295,10 @@ function reduceMetamask (state, action) { amount: '0x0', memo: '', errors: {}, + maxModeOn: false, editingTransactionId: null, forceGasMin: null, + toNickname: '', }, }) @@ -330,9 +336,9 @@ function reduceMetamask (state, action) { }) case actions.SET_USE_BLOCKIE: - return extend(metamaskState, { - useBlockie: action.value, - }) + return extend(metamaskState, { + useBlockie: action.value, + }) case actions.UPDATE_FEATURE_FLAGS: return extend(metamaskState, { @@ -365,6 +371,12 @@ function reduceMetamask (state, action) { }) } + case actions.UPDATE_PREFERENCES: { + return extend(metamaskState, { + preferences: { ...action.payload }, + }) + } + default: return metamaskState diff --git a/ui/app/selectors.js b/ui/app/selectors.js index fb4517628..7209f19d1 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -26,6 +26,7 @@ const selectors = { getAddressBook, getSendFrom, getCurrentCurrency, + getNativeCurrency, getSendAmount, getSelectedTokenToFiatRate, getSelectedTokenContract, @@ -33,6 +34,7 @@ const selectors = { getSendMaxModeState, getCurrentViewContext, getTotalUnapprovedCount, + preferencesSelector, } module.exports = selectors @@ -142,6 +144,10 @@ function getCurrentCurrency (state) { return state.metamask.currentCurrency } +function getNativeCurrency (state) { + return state.metamask.nativeCurrency +} + function getSelectedTokenToFiatRate (state) { const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state) const conversionRate = conversionRateSelector(state) @@ -195,3 +201,7 @@ function getTotalUnapprovedCount ({ metamask }) { return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount + unapprovedTypedMessagesCount } + +function preferencesSelector ({ metamask }) { + return metamask.preferences +} diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js index 86b10bac3..90924c036 100644 --- a/ui/app/selectors/confirm-transaction.js +++ b/ui/app/selectors/confirm-transaction.js @@ -93,6 +93,7 @@ export const unconfirmedTransactionsCountSelector = createSelector( export const currentCurrencySelector = state => state.metamask.currentCurrency export const conversionRateSelector = state => state.metamask.conversionRate +export const getNativeCurrency = state => state.metamask.nativeCurrency const txDataSelector = state => state.confirmTransaction.txData const tokenDataSelector = state => state.confirmTransaction.tokenData diff --git a/ui/app/util.js b/ui/app/util.js index 37c0fb698..b19a028cc 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -128,7 +128,7 @@ function parseBalance (balance) { // Takes wei hex, returns an object with three properties. // Its "formatted" property is what we generally use to render values. -function formatBalance (balance, decimalsToKeep, needsParse = true) { +function formatBalance (balance, decimalsToKeep, needsParse = true, ticker = 'ETH') { var parsed = needsParse ? parseBalance(balance) : balance.split('.') var beforeDecimal = parsed[0] var afterDecimal = parsed[1] @@ -138,14 +138,14 @@ function formatBalance (balance, decimalsToKeep, needsParse = true) { if (afterDecimal !== '0') { var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits if (sigFigs) { afterDecimal = sigFigs[0] } - formatted = '0.' + afterDecimal + ' ETH' + formatted = '0.' + afterDecimal + ` ${ticker}` } } else { - formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' + formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${ticker}` } } else { afterDecimal += Array(decimalsToKeep).join('0') - formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' + formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${ticker}` } return formatted } diff --git a/ui/i18n-helper.js b/ui/i18n-helper.js index c6a7d0bf1..db07049e1 100644 --- a/ui/i18n-helper.js +++ b/ui/i18n-helper.js @@ -13,7 +13,7 @@ const getMessage = (locale, key, substitutions) => { return null } if (!locale[key]) { - log.error(`Translator - Unable to find value for key "${key}"`) + log.warn(`Translator - Unable to find value for key "${key}"`) return null } const entry = locale[key] |