mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 01:39:44 +01:00
warn user when sending from different account (#8601)
This commit is contained in:
parent
dd41870f1d
commit
389df9dd37
@ -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"
|
||||
},
|
||||
|
@ -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,
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if (!showEdit && isFullScreen) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className="confirm-page-container-header">
|
||||
<div className="confirm-page-container-header__row">
|
||||
{ !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') }
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
@ -67,23 +63,22 @@ export default class ConfirmPageContainerHeader extends Component {
|
||||
<div className="confirm-page-container-header__address">
|
||||
{ shortenAddress(accountAddress) }
|
||||
</div>
|
||||
<AccountMismatchWarning address={accountAddress} />
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
{ !isFullScreen && <NetworkDisplay /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children } = this.props
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-header">
|
||||
{ this.renderTop() }
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ConfirmPageContainerHeader.propTypes = {
|
||||
accountAddress: PropTypes.string,
|
||||
showAccountInHeader: PropTypes.bool,
|
||||
showEdit: PropTypes.bool,
|
||||
onEdit: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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-list-item__top-row {
|
||||
margin: 0px;
|
||||
&__account-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__top-row {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,16 @@ describe('Signature Request', function () {
|
||||
provider: {
|
||||
type: 'test',
|
||||
},
|
||||
accounts: {
|
||||
'0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5': {
|
||||
address: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5',
|
||||
balance: '0x03',
|
||||
},
|
||||
},
|
||||
cachedBalances: {
|
||||
|
||||
},
|
||||
selectedAddress: '0xd8f6a2ffb0fc5952d16c9768b71cfd35b6399aa5',
|
||||
},
|
||||
}
|
||||
const store = configureMockStore()(mockStore)
|
||||
|
@ -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 (
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
html={<p>{t('notCurrentAccount')}</p>}
|
||||
wrapperClassName="account-mismatch-warning__tooltip-wrapper"
|
||||
containerClassName="account-mismatch-warning__tooltip-container"
|
||||
>
|
||||
<div className="account-mismatch-warning__tooltip-container-icon"><InfoIcon severity="warning" /></div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
AccountMismatchWarning.propTypes = {
|
||||
address: PropTypes.string.isRequired,
|
||||
}
|
8
ui/app/components/ui/account-mismatch-warning/index.scss
Normal file
8
ui/app/components/ui/account-mismatch-warning/index.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.account-mismatch-warning {
|
||||
&__tooltip-container {
|
||||
&-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
@ -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(<AccountMismatchWarning address="mockedAddress" />)
|
||||
assert.equal(wrapper.find(InfoIcon).length, 0)
|
||||
})
|
||||
it('renders a warning info icon when addresses do not match', function () {
|
||||
const wrapper = shallow(<AccountMismatchWarning address="mockedAddress2" />)
|
||||
assert.equal(wrapper.find(InfoIcon).length, 1)
|
||||
})
|
||||
after(function () {
|
||||
sinon.restore()
|
||||
})
|
||||
})
|
19
ui/app/components/ui/icon/index.scss
Normal file
19
ui/app/components/ui/icon/index.scss
Normal file
@ -0,0 +1,19 @@
|
||||
.info-icon {
|
||||
margin: 4px;
|
||||
|
||||
&--success {
|
||||
fill: $success-green;
|
||||
}
|
||||
|
||||
&--info {
|
||||
fill: $info-blue;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
fill: $warning-yellow;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
fill: $danger-red;
|
||||
}
|
||||
}
|
21
ui/app/components/ui/icon/info-icon.component.js
Normal file
21
ui/app/components/ui/icon/info-icon.component.js
Normal file
@ -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 (
|
||||
<svg className={className} width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.2 5.6H8.8V4H7.2V5.6ZM8 14.4C4.472 14.4 1.6 11.528 1.6 8C1.6 4.472 4.472 1.6 8 1.6C11.528 1.6 14.4 4.472 14.4 8C14.4 11.528 11.528 14.4 8 14.4ZM8 0C6.94943 0 5.90914 0.206926 4.93853 0.608964C3.96793 1.011 3.08601 1.60028 2.34315 2.34315C0.842855 3.84344 0 5.87827 0 8C0 10.1217 0.842855 12.1566 2.34315 13.6569C3.08601 14.3997 3.96793 14.989 4.93853 15.391C5.90914 15.7931 6.94943 16 8 16C10.1217 16 12.1566 15.1571 13.6569 13.6569C15.1571 12.1566 16 10.1217 16 8C16 6.94943 15.7931 5.90914 15.391 4.93853C14.989 3.96793 14.3997 3.08601 13.6569 2.34315C12.914 1.60028 12.0321 1.011 11.0615 0.608964C10.0909 0.206926 9.05058 0 8 0ZM7.2 12H8.8V7.2H7.2V12Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
InfoIcon.propTypes = {
|
||||
severity: PropTypes.oneOf(['success', 'info', 'warning', 'danger']),
|
||||
}
|
@ -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,
|
||||
function SenderAddress ({
|
||||
addressOnly,
|
||||
checksummedSenderAddress,
|
||||
senderName,
|
||||
onSenderClick,
|
||||
senderAddress,
|
||||
}) {
|
||||
const t = useI18nContext()
|
||||
const [addressCopied, setAddressCopied] = useState(false)
|
||||
let tooltipHtml = <p>{t('copiedExclamation')}</p>
|
||||
if (!addressCopied) {
|
||||
tooltipHtml = addressOnly
|
||||
? <p>{t('copyAddress')}</p>
|
||||
: (
|
||||
<p>
|
||||
{shortenAddress(checksummedSenderAddress)}<br />
|
||||
{t('copyAddress')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
variant: DEFAULT_VARIANT,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
state = {
|
||||
senderAddressCopied: false,
|
||||
}
|
||||
|
||||
renderSenderIdenticon () {
|
||||
return !this.props.addressOnly && (
|
||||
<div className="sender-to-recipient__sender-icon">
|
||||
<Identicon
|
||||
address={checksumAddress(this.props.senderAddress)}
|
||||
diameter={24}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderSenderAddress () {
|
||||
const { t } = this.context
|
||||
const { senderName, senderAddress, addressOnly } = this.props
|
||||
const checksummedSenderAddress = checksumAddress(senderAddress)
|
||||
|
||||
return (
|
||||
return (
|
||||
<div
|
||||
className={classnames('sender-to-recipient__party sender-to-recipient__party--sender')}
|
||||
onClick={() => {
|
||||
setAddressCopied(true)
|
||||
copyToClipboard(checksummedSenderAddress)
|
||||
if (onSenderClick) {
|
||||
onSenderClick()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!addressOnly && (
|
||||
<div className="sender-to-recipient__sender-icon">
|
||||
<Identicon
|
||||
address={checksumAddress(senderAddress)}
|
||||
diameter={24}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
html={
|
||||
this.state.senderAddressCopied
|
||||
? <p>{t('copiedExclamation')}</p>
|
||||
: addressOnly
|
||||
? <p>{t('copyAddress')}</p>
|
||||
: (
|
||||
<p>
|
||||
{shortenAddress(checksummedSenderAddress)}<br />
|
||||
{t('copyAddress')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
html={tooltipHtml}
|
||||
wrapperClassName="sender-to-recipient__tooltip-wrapper"
|
||||
containerClassName="sender-to-recipient__tooltip-container"
|
||||
onHidden={() => this.setState({ senderAddressCopied: false })}
|
||||
onHidden={() => setAddressCopied(false)}
|
||||
>
|
||||
<div className="sender-to-recipient__name">
|
||||
{
|
||||
@ -83,129 +70,184 @@ export default class SenderToRecipient extends PureComponent {
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
<AccountMismatchWarning address={senderAddress} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
SenderAddress.propTypes = {
|
||||
senderName: PropTypes.string,
|
||||
checksummedSenderAddress: PropTypes.string,
|
||||
addressOnly: PropTypes.bool,
|
||||
senderAddress: PropTypes.string,
|
||||
onSenderClick: PropTypes.func,
|
||||
}
|
||||
|
||||
function RecipientWithAddress ({
|
||||
checksummedRecipientAddress,
|
||||
assetImage,
|
||||
onRecipientClick,
|
||||
addressOnly,
|
||||
recipientNickname,
|
||||
recipientEns,
|
||||
recipientName,
|
||||
}) {
|
||||
const t = useI18nContext()
|
||||
const [addressCopied, setAddressCopied] = useState(false)
|
||||
|
||||
let tooltipHtml = <p>{t('copiedExclamation')}</p>
|
||||
if (!addressCopied) {
|
||||
if (addressOnly && !recipientNickname && !recipientEns) {
|
||||
tooltipHtml = <p>{t('copyAddress')}</p>
|
||||
} else {
|
||||
tooltipHtml = (
|
||||
<p>
|
||||
{shortenAddress(checksummedRecipientAddress)}<br />
|
||||
{t('copyAddress')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="sender-to-recipient__party sender-to-recipient__party--recipient sender-to-recipient__party--recipient-with-address"
|
||||
onClick={() => {
|
||||
setAddressCopied(true)
|
||||
copyToClipboard(checksummedRecipientAddress)
|
||||
if (onRecipientClick) {
|
||||
onRecipientClick()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!addressOnly && (
|
||||
<div className="sender-to-recipient__sender-icon">
|
||||
<Identicon
|
||||
address={checksummedRecipientAddress}
|
||||
diameter={24}
|
||||
image={assetImage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
html={tooltipHtml}
|
||||
offset={-10}
|
||||
wrapperClassName="sender-to-recipient__tooltip-wrapper"
|
||||
containerClassName="sender-to-recipient__tooltip-container"
|
||||
onHidden={() => setAddressCopied(false)}
|
||||
>
|
||||
<div className="sender-to-recipient__name">
|
||||
<span>{ addressOnly ? `${t('to')}: ` : '' }</span>
|
||||
{
|
||||
addressOnly
|
||||
? (recipientNickname || recipientEns || checksummedRecipientAddress)
|
||||
: (recipientNickname || recipientEns || recipientName || t('newContract'))
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderRecipientIdenticon () {
|
||||
const { recipientAddress, assetImage } = this.props
|
||||
const checksummedRecipientAddress = checksumAddress(recipientAddress)
|
||||
RecipientWithAddress.propTypes = {
|
||||
checksummedRecipientAddress: PropTypes.string,
|
||||
recipientName: PropTypes.string,
|
||||
recipientEns: PropTypes.string,
|
||||
recipientNickname: PropTypes.string,
|
||||
addressOnly: PropTypes.bool,
|
||||
assetImage: PropTypes.string,
|
||||
onRecipientClick: PropTypes.func,
|
||||
}
|
||||
|
||||
return !this.props.addressOnly && (
|
||||
<div className="sender-to-recipient__sender-icon">
|
||||
<Identicon
|
||||
address={checksummedRecipientAddress}
|
||||
diameter={24}
|
||||
image={assetImage}
|
||||
|
||||
function Arrow ({ variant }) {
|
||||
return variant === DEFAULT_VARIANT
|
||||
? (
|
||||
<div className="sender-to-recipient__arrow-container">
|
||||
<div className="sender-to-recipient__arrow-circle">
|
||||
<img
|
||||
height={15}
|
||||
width={15}
|
||||
src="./images/arrow-right.svg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="sender-to-recipient__arrow-container">
|
||||
<img
|
||||
height={20}
|
||||
src="./images/caret-right.svg"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderRecipientWithAddress () {
|
||||
const { t } = this.context
|
||||
const { recipientEns, recipientName, recipientAddress, recipientNickname, addressOnly, onRecipientClick } = this.props
|
||||
const checksummedRecipientAddress = checksumAddress(recipientAddress)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="sender-to-recipient__party sender-to-recipient__party--recipient sender-to-recipient__party--recipient-with-address"
|
||||
onClick={() => {
|
||||
copyToClipboard(checksummedRecipientAddress)
|
||||
if (onRecipientClick) {
|
||||
onRecipientClick()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ this.renderRecipientIdenticon() }
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
html={
|
||||
this.state.senderAddressCopied
|
||||
? <p>{t('copiedExclamation')}</p>
|
||||
: (addressOnly && !recipientNickname && !recipientEns)
|
||||
? <p>{t('copyAddress')}</p>
|
||||
: (
|
||||
<p>
|
||||
{shortenAddress(checksummedRecipientAddress)}<br />
|
||||
{t('copyAddress')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
wrapperClassName="sender-to-recipient__tooltip-wrapper"
|
||||
containerClassName="sender-to-recipient__tooltip-container"
|
||||
>
|
||||
<div className="sender-to-recipient__name">
|
||||
<span>{ addressOnly ? `${t('to')}: ` : '' }</span>
|
||||
{
|
||||
addressOnly
|
||||
? (recipientNickname || recipientEns || checksummedRecipientAddress)
|
||||
: (recipientNickname || recipientEns || recipientName || this.context.t('newContract'))
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderRecipientWithoutAddress () {
|
||||
return (
|
||||
<div className="sender-to-recipient__party sender-to-recipient__party--recipient">
|
||||
{ !this.props.addressOnly && <i className="fa fa-file-text-o" /> }
|
||||
<div className="sender-to-recipient__name">
|
||||
{ this.context.t('newContract') }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderArrow () {
|
||||
return this.props.variant === DEFAULT_VARIANT
|
||||
? (
|
||||
<div className="sender-to-recipient__arrow-container">
|
||||
<div className="sender-to-recipient__arrow-circle">
|
||||
<img
|
||||
height={15}
|
||||
width={15}
|
||||
src="./images/arrow-right.svg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="sender-to-recipient__arrow-container">
|
||||
<img
|
||||
height={20}
|
||||
src="./images/caret-right.svg"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { senderAddress, recipientAddress, variant, onSenderClick } = this.props
|
||||
const checksummedSenderAddress = checksumAddress(senderAddress)
|
||||
|
||||
return (
|
||||
<div className={classnames('sender-to-recipient', variantHash[variant])}>
|
||||
<div
|
||||
className={classnames('sender-to-recipient__party sender-to-recipient__party--sender')}
|
||||
onClick={() => {
|
||||
this.setState({ senderAddressCopied: true })
|
||||
copyToClipboard(checksummedSenderAddress)
|
||||
if (onSenderClick) {
|
||||
onSenderClick()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ this.renderSenderIdenticon() }
|
||||
{ this.renderSenderAddress() }
|
||||
</div>
|
||||
{ this.renderArrow() }
|
||||
{
|
||||
recipientAddress
|
||||
? this.renderRecipientWithAddress()
|
||||
: this.renderRecipientWithoutAddress()
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={classnames('sender-to-recipient', variantHash[variant])}>
|
||||
<SenderAddress
|
||||
checksummedSenderAddress={checksummedSenderAddress}
|
||||
addressOnly={addressOnly}
|
||||
senderName={senderName}
|
||||
onSenderClick={onSenderClick}
|
||||
senderAddress={senderAddress}
|
||||
/>
|
||||
<Arrow variant={variant} />
|
||||
{recipientAddress
|
||||
? (
|
||||
<RecipientWithAddress
|
||||
assetImage={assetImage}
|
||||
checksummedRecipientAddress={checksummedRecipientAddress}
|
||||
onRecipientClick={onRecipientClick}
|
||||
addressOnly={addressOnly}
|
||||
recipientNickname={recipientNickname}
|
||||
recipientEns={recipientEns}
|
||||
recipientName={recipientName}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className="sender-to-recipient__party sender-to-recipient__party--recipient">
|
||||
{ !addressOnly && <i className="fa fa-file-text-o" /> }
|
||||
<div className="sender-to-recipient__name">
|
||||
{t('newContract') }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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 (
|
||||
<div
|
||||
className={`account-list-item ${className}`}
|
||||
onClick={() => handleClick && handleClick({ name, address, balance })}
|
||||
>
|
||||
|
||||
static defaultProps = {
|
||||
showFiat: true,
|
||||
}
|
||||
<div className="account-list-item__top-row">
|
||||
<Identicon
|
||||
address={address}
|
||||
className="account-list-item__identicon"
|
||||
diameter={18}
|
||||
/>
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
<div className="account-list-item__account-name">{ name || address }</div>
|
||||
|
||||
render () {
|
||||
const {
|
||||
account,
|
||||
className,
|
||||
displayAddress = false,
|
||||
displayBalance = true,
|
||||
handleClick,
|
||||
icon = null,
|
||||
balanceIsCached,
|
||||
showFiat,
|
||||
} = this.props
|
||||
|
||||
const { name, address, balance } = account || {}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`account-list-item ${className}`}
|
||||
onClick={() => handleClick && handleClick({ name, address, balance })}
|
||||
>
|
||||
|
||||
<div className="account-list-item__top-row">
|
||||
<Identicon
|
||||
address={address}
|
||||
className="account-list-item__identicon"
|
||||
diameter={18}
|
||||
/>
|
||||
|
||||
<div className="account-list-item__account-name">{ name || address }</div>
|
||||
|
||||
{icon && <div className="account-list-item__icon">{ icon }</div>}
|
||||
|
||||
</div>
|
||||
|
||||
{displayAddress && name && (
|
||||
<div className="account-list-item__account-address">
|
||||
{ checksumAddress(address) }
|
||||
</div>
|
||||
)}
|
||||
|
||||
{displayBalance && (
|
||||
<Tooltip
|
||||
position="left"
|
||||
title={this.context.t('balanceOutdated')}
|
||||
disabled={!balanceIsCached}
|
||||
style={{
|
||||
left: '-20px !important',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classnames('account-list-item__account-balances', {
|
||||
'account-list-item__cached-balances': balanceIsCached,
|
||||
})}
|
||||
>
|
||||
<div className="account-list-item__primary-cached-container">
|
||||
<UserPreferencedCurrencyDisplay
|
||||
type={PRIMARY}
|
||||
value={balance}
|
||||
hideTitle
|
||||
/>
|
||||
{
|
||||
balanceIsCached
|
||||
? <span className="account-list-item__cached-star">*</span>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
{showFiat && (
|
||||
<UserPreferencedCurrencyDisplay
|
||||
type={SECONDARY}
|
||||
value={balance}
|
||||
hideTitle
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{icon && <div className="account-list-item__icon">{ icon }</div>}
|
||||
|
||||
<AccountMismatchWarning address={address} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{displayAddress && name && (
|
||||
<div className="account-list-item__account-address">
|
||||
{ checksumAddress(address) }
|
||||
</div>
|
||||
)}
|
||||
|
||||
{displayBalance && (
|
||||
<Tooltip
|
||||
position="left"
|
||||
title={t('balanceOutdated')}
|
||||
disabled={!balanceIsCached}
|
||||
style={{
|
||||
left: '-20px !important',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classnames('account-list-item__account-balances', {
|
||||
'account-list-item__cached-balances': balanceIsCached,
|
||||
})}
|
||||
>
|
||||
<div className="account-list-item__primary-cached-container">
|
||||
<UserPreferencedCurrencyDisplay
|
||||
type={PRIMARY}
|
||||
value={balance}
|
||||
hideTitle
|
||||
/>
|
||||
{
|
||||
balanceIsCached
|
||||
? <span className="account-list-item__cached-star">*</span>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
{showFiat && (
|
||||
<UserPreferencedCurrencyDisplay
|
||||
type={SECONDARY}
|
||||
value={balance}
|
||||
hideTitle
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
@ -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((
|
||||
<AccountListItem
|
||||
@ -41,6 +35,11 @@ describe('AccountListItem Component', function () {
|
||||
|
||||
afterEach(function () {
|
||||
propsMethodSpies.handleClick.resetHistory()
|
||||
checksumAddressStub.resetHistory()
|
||||
})
|
||||
|
||||
after(function () {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('should render a div with the passed className', function () {
|
||||
@ -100,7 +99,7 @@ describe('AccountListItem Component', function () {
|
||||
assert.equal(wrapper.find('.account-list-item__account-address').length, 1)
|
||||
assert.equal(wrapper.find('.account-list-item__account-address').text(), 'mockCheckSumAddress')
|
||||
assert.deepEqual(
|
||||
utilsMethodStubs.checksumAddress.getCall(0).args,
|
||||
checksumAddressStub.getCall(0).args,
|
||||
['mockAddress']
|
||||
)
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user