diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 7ddaba250..2973b24ae 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1030,6 +1030,9 @@ "noThanks": { "message": "No Thanks" }, + "notCurrentAccount": { + "message": "Is this the correct account? It's different from the currently selected account in your wallet" + }, "notEnoughGas": { "message": "Not Enough Gas" }, diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js index 620e54897..f1f10a797 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React from 'react' import PropTypes from 'prop-types' import { ENVIRONMENT_TYPE_POPUP, @@ -8,31 +8,27 @@ import { getEnvironmentType } from '../../../../../../app/scripts/lib/util' import NetworkDisplay from '../../network-display' import Identicon from '../../../ui/identicon' import { shortenAddress } from '../../../../helpers/utils/util' +import AccountMismatchWarning from '../../../ui/account-mismatch-warning/account-mismatch-warning.component' +import { useI18nContext } from '../../../../hooks/useI18nContext' -export default class ConfirmPageContainerHeader extends Component { - static contextTypes = { - t: PropTypes.func, - } - - static propTypes = { - accountAddress: PropTypes.string, - showAccountInHeader: PropTypes.bool, - showEdit: PropTypes.bool, - onEdit: PropTypes.func, - children: PropTypes.node, - } - renderTop () { - const { onEdit, showEdit, accountAddress, showAccountInHeader } = this.props - const windowType = getEnvironmentType() - const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && - windowType !== ENVIRONMENT_TYPE_POPUP +export default function ConfirmPageContainerHeader ({ + onEdit, + showEdit, + accountAddress, + showAccountInHeader, + children, +}) { + const t = useI18nContext() + const windowType = getEnvironmentType() + const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && + windowType !== ENVIRONMENT_TYPE_POPUP - if (!showEdit && isFullScreen) { - return null - } - - return ( + if (!showEdit && isFullScreen) { + return null + } + return ( +
{ !showAccountInHeader ? ( @@ -49,7 +45,7 @@ export default class ConfirmPageContainerHeader extends Component { className="confirm-page-container-header__back-button" onClick={() => onEdit()} > - { this.context.t('edit') } + { t('edit') }
) @@ -67,23 +63,22 @@ export default class ConfirmPageContainerHeader extends Component {
{ shortenAddress(accountAddress) }
+
) : null } { !isFullScreen && } - ) - } - - render () { - const { children } = this.props + { children } + + ) +} - return ( -
- { this.renderTop() } - { children } -
- ) - } +ConfirmPageContainerHeader.propTypes = { + accountAddress: PropTypes.string, + showAccountInHeader: PropTypes.bool, + showEdit: PropTypes.bool, + onEdit: PropTypes.func, + children: PropTypes.node, } diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss index d633e7e05..227e9ce76 100644 --- a/ui/app/components/app/index.scss +++ b/ui/app/components/app/index.scss @@ -94,6 +94,8 @@ @import '../ui/icon-with-fallback/index'; +@import '../ui/icon/index'; + @import '../ui/circle-icon/index'; @import '../ui/alert-circle-icon/index'; @@ -111,3 +113,5 @@ @import 'permissions-connect-footer/index'; @import 'wallet-overview/index'; + +@import '../ui/account-mismatch-warning/index'; diff --git a/ui/app/components/app/signature-request/signature-request-header/index.scss b/ui/app/components/app/signature-request/signature-request-header/index.scss index 7a33f85f2..1d32b4328 100644 --- a/ui/app/components/app/signature-request/signature-request-header/index.scss +++ b/ui/app/components/app/signature-request/signature-request-header/index.scss @@ -13,13 +13,20 @@ display: flex; align-items: center; - .account-list-item__account-name { - font-size: 12px; - font-weight: 500; - } + .account-list-item { + &__top-row { + display: flex; + align-items: center; + } + + &__account-name { + font-size: 12px; + font-weight: 500; + } - .account-list-item__top-row { - margin: 0px; + &__top-row { + margin: 0px; + } } } -} \ No newline at end of file +} diff --git a/ui/app/components/app/tests/signature-request.test.js b/ui/app/components/app/tests/signature-request.test.js index e8054c4f9..493c31df1 100644 --- a/ui/app/components/app/tests/signature-request.test.js +++ b/ui/app/components/app/tests/signature-request.test.js @@ -14,6 +14,16 @@ describe('Signature Request', function () { provider: { type: 'test', }, + accounts: { + '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': { + address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', + balance: '0x03', + }, + }, + cachedBalances: { + + }, + selectedAddress: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5', }, } const store = configureMockStore()(mockStore) diff --git a/ui/app/components/ui/account-mismatch-warning/account-mismatch-warning.component.js b/ui/app/components/ui/account-mismatch-warning/account-mismatch-warning.component.js new file mode 100644 index 000000000..a2f3a0dd7 --- /dev/null +++ b/ui/app/components/ui/account-mismatch-warning/account-mismatch-warning.component.js @@ -0,0 +1,30 @@ +import React from 'react' +import Tooltip from '../tooltip-v2' +import { useSelector } from 'react-redux' +import PropTypes from 'prop-types' +import { getSelectedAccount } from '../../../selectors' +import InfoIcon from '../icon/info-icon.component' +import { useI18nContext } from '../../../hooks/useI18nContext' + +export default function AccountMismatchWarning ({ address }) { + const selectedAccount = useSelector(getSelectedAccount) + const t = useI18nContext() + if (selectedAccount.address === address) { + return null + } + + return ( + {t('notCurrentAccount')}

} + wrapperClassName="account-mismatch-warning__tooltip-wrapper" + containerClassName="account-mismatch-warning__tooltip-container" + > +
+
+ ) +} + +AccountMismatchWarning.propTypes = { + address: PropTypes.string.isRequired, +} diff --git a/ui/app/components/ui/account-mismatch-warning/index.scss b/ui/app/components/ui/account-mismatch-warning/index.scss new file mode 100644 index 000000000..751640871 --- /dev/null +++ b/ui/app/components/ui/account-mismatch-warning/index.scss @@ -0,0 +1,8 @@ +.account-mismatch-warning { + &__tooltip-container { + &-icon { + display: flex; + align-items: center; + } + } +} diff --git a/ui/app/components/ui/account-mismatch-warning/tests/acccount-mismatch-warning.component.test.js b/ui/app/components/ui/account-mismatch-warning/tests/acccount-mismatch-warning.component.test.js new file mode 100644 index 000000000..eb71110b5 --- /dev/null +++ b/ui/app/components/ui/account-mismatch-warning/tests/acccount-mismatch-warning.component.test.js @@ -0,0 +1,32 @@ +import React from 'react' +import * as reactRedux from 'react-redux' +import assert from 'assert' +import sinon from 'sinon' +import { shallow } from 'enzyme' +import InfoIcon from '../../icon/info-icon.component' +import AccountMismatchWarning from '../account-mismatch-warning.component' +import { getSelectedAccount } from '../../../../selectors' + +describe('AccountMismatchWarning', function () { + before(function () { + sinon.stub(reactRedux, 'useSelector').callsFake((selector) => { + if (selector === getSelectedAccount) { + return { address: 'mockedAddress' } + } + throw new Error( + `${selector.name} is not cared for in the AccountMismatchWarning test useSelector stub` + ) + }) + }) + it('renders nothing when the addresses match', function () { + const wrapper = shallow() + assert.equal(wrapper.find(InfoIcon).length, 0) + }) + it('renders a warning info icon when addresses do not match', function () { + const wrapper = shallow() + assert.equal(wrapper.find(InfoIcon).length, 1) + }) + after(function () { + sinon.restore() + }) +}) diff --git a/ui/app/components/ui/icon/index.scss b/ui/app/components/ui/icon/index.scss new file mode 100644 index 000000000..535b52acc --- /dev/null +++ b/ui/app/components/ui/icon/index.scss @@ -0,0 +1,19 @@ +.info-icon { + margin: 4px; + + &--success { + fill: $success-green; + } + + &--info { + fill: $info-blue; + } + + &--warning { + fill: $warning-yellow; + } + + &--danger { + fill: $danger-red; + } +} diff --git a/ui/app/components/ui/icon/info-icon.component.js b/ui/app/components/ui/icon/info-icon.component.js new file mode 100644 index 000000000..1df0ec5e7 --- /dev/null +++ b/ui/app/components/ui/icon/info-icon.component.js @@ -0,0 +1,21 @@ +import React from 'react' +import classnames from 'classnames' +import PropTypes from 'prop-types' + +export default function InfoIcon ({ severity }) { + const className = classnames('info-icon', { + 'info-icon--success': severity === 'success', + 'info-icon--warning': severity === 'warning', + 'info-icon--danger': severity === 'danger', + 'info-icon--info': severity === 'info', + }) + return ( + + + + ) +} + +InfoIcon.propTypes = { + severity: PropTypes.oneOf(['success', 'info', 'warning', 'danger']), +} diff --git a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js index e1021e738..3055682b4 100644 --- a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js +++ b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react' +import React, { useState } from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import Identicon from '../identicon' @@ -6,6 +6,9 @@ import Tooltip from '../tooltip-v2' import copyToClipboard from 'copy-to-clipboard' import { DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT } from './sender-to-recipient.constants' import { checksumAddress, shortenAddress } from '../../../helpers/utils/util' +import AccountMismatchWarning from '../account-mismatch-warning/account-mismatch-warning.component' +import { useI18nContext } from '../../../hooks/useI18nContext' + const variantHash = { [DEFAULT_VARIANT]: 'sender-to-recipient--default', @@ -13,67 +16,51 @@ const variantHash = { [FLAT_VARIANT]: 'sender-to-recipient--flat', } -export default class SenderToRecipient extends PureComponent { - static propTypes = { - senderName: PropTypes.string, - senderAddress: PropTypes.string, - recipientName: PropTypes.string, - recipientEns: PropTypes.string, - recipientAddress: PropTypes.string, - recipientNickname: PropTypes.string, - variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT]), - addressOnly: PropTypes.bool, - assetImage: PropTypes.string, - onRecipientClick: PropTypes.func, - onSenderClick: PropTypes.func, - } - - static defaultProps = { - variant: DEFAULT_VARIANT, - } - - static contextTypes = { - t: PropTypes.func, - } - - state = { - senderAddressCopied: false, - } - - renderSenderIdenticon () { - return !this.props.addressOnly && ( -
- -
- ) +function SenderAddress ({ + addressOnly, + checksummedSenderAddress, + senderName, + onSenderClick, + senderAddress, +}) { + const t = useI18nContext() + const [addressCopied, setAddressCopied] = useState(false) + let tooltipHtml =

{t('copiedExclamation')}

+ if (!addressCopied) { + tooltipHtml = addressOnly + ?

{t('copyAddress')}

+ : ( +

+ {shortenAddress(checksummedSenderAddress)}
+ {t('copyAddress')} +

+ ) } - - renderSenderAddress () { - const { t } = this.context - const { senderName, senderAddress, addressOnly } = this.props - const checksummedSenderAddress = checksumAddress(senderAddress) - - return ( + return ( +
{ + setAddressCopied(true) + copyToClipboard(checksummedSenderAddress) + if (onSenderClick) { + onSenderClick() + } + }} + > + {!addressOnly && ( +
+ +
+ )} {t('copiedExclamation')}

- : addressOnly - ?

{t('copyAddress')}

- : ( -

- {shortenAddress(checksummedSenderAddress)}
- {t('copyAddress')} -

- ) - } + html={tooltipHtml} wrapperClassName="sender-to-recipient__tooltip-wrapper" containerClassName="sender-to-recipient__tooltip-container" - onHidden={() => this.setState({ senderAddressCopied: false })} + onHidden={() => setAddressCopied(false)} >
{ @@ -83,129 +70,184 @@ export default class SenderToRecipient extends PureComponent { }
- ) - } + +
+ ) +} - renderRecipientIdenticon () { - const { recipientAddress, assetImage } = this.props - const checksummedRecipientAddress = checksumAddress(recipientAddress) +SenderAddress.propTypes = { + senderName: PropTypes.string, + checksummedSenderAddress: PropTypes.string, + addressOnly: PropTypes.bool, + senderAddress: PropTypes.string, + onSenderClick: PropTypes.func, +} - return !this.props.addressOnly && ( -
- -
- ) +function RecipientWithAddress ({ + checksummedRecipientAddress, + assetImage, + onRecipientClick, + addressOnly, + recipientNickname, + recipientEns, + recipientName, +}) { + const t = useI18nContext() + const [addressCopied, setAddressCopied] = useState(false) + + let tooltipHtml =

{t('copiedExclamation')}

+ if (!addressCopied) { + if (addressOnly && !recipientNickname && !recipientEns) { + tooltipHtml =

{t('copyAddress')}

+ } else { + tooltipHtml = ( +

+ {shortenAddress(checksummedRecipientAddress)}
+ {t('copyAddress')} +

+ ) + } } - - renderRecipientWithAddress () { - const { t } = this.context - const { recipientEns, recipientName, recipientAddress, recipientNickname, addressOnly, onRecipientClick } = this.props - const checksummedRecipientAddress = checksumAddress(recipientAddress) - - return ( -
{ - copyToClipboard(checksummedRecipientAddress) - if (onRecipientClick) { - onRecipientClick() - } - }} + return ( +
{ + setAddressCopied(true) + copyToClipboard(checksummedRecipientAddress) + if (onRecipientClick) { + onRecipientClick() + } + }} + > + {!addressOnly && ( +
+ +
+ )} + setAddressCopied(false)} > - { this.renderRecipientIdenticon() } - {t('copiedExclamation')}

- : (addressOnly && !recipientNickname && !recipientEns) - ?

{t('copyAddress')}

- : ( -

- {shortenAddress(checksummedRecipientAddress)}
- {t('copyAddress')} -

- ) - } - wrapperClassName="sender-to-recipient__tooltip-wrapper" - containerClassName="sender-to-recipient__tooltip-container" - > -
- { addressOnly ? `${t('to')}: ` : '' } - { - addressOnly - ? (recipientNickname || recipientEns || checksummedRecipientAddress) - : (recipientNickname || recipientEns || recipientName || this.context.t('newContract')) - } -
-
-
- ) - } - - renderRecipientWithoutAddress () { - return ( -
- { !this.props.addressOnly && }
- { this.context.t('newContract') } + { addressOnly ? `${t('to')}: ` : '' } + { + addressOnly + ? (recipientNickname || recipientEns || checksummedRecipientAddress) + : (recipientNickname || recipientEns || recipientName || t('newContract')) + }
-
- ) - } + +
+ ) +} - renderArrow () { - return this.props.variant === DEFAULT_VARIANT - ? ( -
-
- -
-
- ) : ( -
- -
- ) - } +RecipientWithAddress.propTypes = { + checksummedRecipientAddress: PropTypes.string, + recipientName: PropTypes.string, + recipientEns: PropTypes.string, + recipientNickname: PropTypes.string, + addressOnly: PropTypes.bool, + assetImage: PropTypes.string, + onRecipientClick: PropTypes.func, +} - render () { - const { senderAddress, recipientAddress, variant, onSenderClick } = this.props - const checksummedSenderAddress = checksumAddress(senderAddress) - return ( -
-
{ - this.setState({ senderAddressCopied: true }) - copyToClipboard(checksummedSenderAddress) - if (onSenderClick) { - onSenderClick() - } - }} - > - { this.renderSenderIdenticon() } - { this.renderSenderAddress() } +function Arrow ({ variant }) { + return variant === DEFAULT_VARIANT + ? ( +
+
+
- { this.renderArrow() } - { - recipientAddress - ? this.renderRecipientWithAddress() - : this.renderRecipientWithoutAddress() - } +
+ ) : ( +
+
) - } +} + +Arrow.propTypes = { + variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT]), +} + +export default function SenderToRecipient ({ + senderAddress, + addressOnly, + assetImage, + senderName, + recipientNickname, + recipientName, + recipientEns, + onRecipientClick, + onSenderClick, + recipientAddress, + variant = DEFAULT_VARIANT, +}) { + const t = useI18nContext() + const checksummedSenderAddress = checksumAddress(senderAddress) + const checksummedRecipientAddress = checksumAddress(recipientAddress) + + return ( +
+ + + {recipientAddress + ? ( + + ) + : ( +
+ { !addressOnly && } +
+ {t('newContract') } +
+
+ ) + } +
+ ) +} + +SenderToRecipient.propTypes = { + senderName: PropTypes.string, + senderAddress: PropTypes.string, + recipientName: PropTypes.string, + recipientEns: PropTypes.string, + recipientAddress: PropTypes.string, + recipientNickname: PropTypes.string, + variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT]), + addressOnly: PropTypes.bool, + assetImage: PropTypes.string, + onRecipientClick: PropTypes.func, + onSenderClick: PropTypes.func, } diff --git a/ui/app/components/ui/tooltip-v2.js b/ui/app/components/ui/tooltip-v2.js index 941ae1e55..8941aa1fd 100644 --- a/ui/app/components/ui/tooltip-v2.js +++ b/ui/app/components/ui/tooltip-v2.js @@ -11,6 +11,7 @@ export default class Tooltip extends PureComponent { interactive: undefined, onHidden: null, position: 'left', + offset: 0, size: 'small', title: null, trigger: 'mouseenter', @@ -24,6 +25,7 @@ export default class Tooltip extends PureComponent { disabled: PropTypes.bool, html: PropTypes.node, interactive: PropTypes.bool, + offset: PropTypes.number, onHidden: PropTypes.func, position: PropTypes.oneOf([ 'top', @@ -53,6 +55,7 @@ export default class Tooltip extends PureComponent { title, trigger, onHidden, + offset, wrapperClassName, style, } = this.props @@ -77,6 +80,7 @@ export default class Tooltip extends PureComponent { onHidden={onHidden} position={position} size={size} + offset={offset} style={style} title={title} trigger={trigger} diff --git a/ui/app/pages/send/account-list-item/account-list-item.component.js b/ui/app/pages/send/account-list-item/account-list-item.component.js index 944bc35fb..58df336e5 100644 --- a/ui/app/pages/send/account-list-item/account-list-item.component.js +++ b/ui/app/pages/send/account-list-item/account-list-item.component.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import { checksumAddress } from '../../../helpers/utils/util' @@ -6,105 +6,96 @@ import Identicon from '../../../components/ui/identicon' import UserPreferencedCurrencyDisplay from '../../../components/app/user-preferenced-currency-display' import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' import Tooltip from '../../../components/ui/tooltip-v2' +import AccountMismatchWarning from '../../../components/ui/account-mismatch-warning/account-mismatch-warning.component' +import { useI18nContext } from '../../../hooks/useI18nContext' -export default class AccountListItem extends Component { +export default function AccountListItem ({ + account, + className, + displayAddress = false, + displayBalance = true, + handleClick, + icon = null, + balanceIsCached, + showFiat = true, +}) { + const t = useI18nContext() + const { name, address, balance } = account || {} - static propTypes = { - account: PropTypes.object, - className: PropTypes.string, - displayAddress: PropTypes.bool, - displayBalance: PropTypes.bool, - handleClick: PropTypes.func, - icon: PropTypes.node, - balanceIsCached: PropTypes.bool, - showFiat: PropTypes.bool, - } + return ( +
handleClick && handleClick({ name, address, balance })} + > - static defaultProps = { - showFiat: true, - } +
+ - static contextTypes = { - t: PropTypes.func, - } +
{ name || address }
- render () { - const { - account, - className, - displayAddress = false, - displayBalance = true, - handleClick, - icon = null, - balanceIsCached, - showFiat, - } = this.props + {icon &&
{ icon }
} - const { name, address, balance } = account || {} - - return ( -
handleClick && handleClick({ name, address, balance })} - > - -
- - -
{ name || address }
- - {icon &&
{ icon }
} + +
+ {displayAddress && name && ( +
+ { checksumAddress(address) }
+ )} - {displayAddress && name && ( -
- { checksumAddress(address) } -
- )} - - {displayBalance && ( - +
-
-
- - { - balanceIsCached - ? * - : null - } -
- {showFiat && ( - - )} +
+ + { + balanceIsCached + ? * + : null + }
- - )} + {showFiat && ( + + )} +
+ + )} -
- ) - } +
+ ) +} + +AccountListItem.propTypes = { + account: PropTypes.object, + className: PropTypes.string, + displayAddress: PropTypes.bool, + displayBalance: PropTypes.bool, + handleClick: PropTypes.func, + icon: PropTypes.node, + balanceIsCached: PropTypes.bool, + showFiat: PropTypes.bool, } diff --git a/ui/app/pages/send/account-list-item/tests/account-list-item-component.test.js b/ui/app/pages/send/account-list-item/tests/account-list-item-component.test.js index e64a0e6f2..83c55e232 100644 --- a/ui/app/pages/send/account-list-item/tests/account-list-item-component.test.js +++ b/ui/app/pages/send/account-list-item/tests/account-list-item-component.test.js @@ -2,27 +2,21 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' import sinon from 'sinon' -import proxyquire from 'proxyquire' +import * as utils from '../../../../helpers/utils/util' import Identicon from '../../../../components/ui/identicon' import UserPreferencedCurrencyDisplay from '../../../../components/app/user-preferenced-currency-display' - -const utilsMethodStubs = { - checksumAddress: sinon.stub().returns('mockCheckSumAddress'), -} - -const AccountListItem = proxyquire('../account-list-item.component.js', { - '../../../helpers/utils/util': utilsMethodStubs, -}).default - - -const propsMethodSpies = { - handleClick: sinon.spy(), -} +import AccountListItem from '../account-list-item.component' describe('AccountListItem Component', function () { - let wrapper + let wrapper, propsMethodSpies, checksumAddressStub describe('render', function () { + before(function () { + checksumAddressStub = sinon.stub(utils, 'checksumAddress').returns('mockCheckSumAddress') + propsMethodSpies = { + handleClick: sinon.spy(), + } + }) beforeEach(function () { wrapper = shallow((