Add CurrencyDisplay and TokenCurrencyDisplay components

This commit is contained in:
Alexander Tseung 2018-08-15 19:18:01 -07:00
parent 6670bc0e09
commit da0df79047
14 changed files with 425 additions and 74 deletions

View File

@ -0,0 +1,24 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
export default class CurrencyDisplay extends PureComponent {
static propTypes = {
className: PropTypes.string,
displayValue: PropTypes.string,
prefix: PropTypes.string,
}
render () {
const { className, displayValue, prefix } = this.props
const text = `${prefix || ''}${displayValue}`
return (
<div
className={className}
title={text}
>
{ text }
</div>
)
}
}

View File

@ -0,0 +1,20 @@
import { connect } from 'react-redux'
import CurrencyDisplay from './currency-display.component'
import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
import { ETH } from '../../constants/common'
const mapStateToProps = (state, ownProps) => {
const { value, numberOfDecimals = 2, currency } = ownProps
const { metamask: { currentCurrency, conversionRate } } = state
const toCurrency = currency === ETH ? ETH : currentCurrency
const convertedValue = getValueFromWeiHex({ value, toCurrency, conversionRate, numberOfDecimals })
const formattedValue = formatCurrency(convertedValue, toCurrency)
const displayValue = `${formattedValue} ${toCurrency.toUpperCase()}`
return {
displayValue,
}
}
export default connect(mapStateToProps)(CurrencyDisplay)

View File

@ -0,0 +1 @@
export { default } from './currency-display.container'

View File

@ -0,0 +1,27 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import CurrencyDisplay from '../currency-display.component'
describe('CurrencyDisplay Component', () => {
it('should render text with a className', () => {
const wrapper = shallow(<CurrencyDisplay
displayValue="$123.45"
className="currency-display"
/>)
assert.ok(wrapper.hasClass('currency-display'))
assert.equal(wrapper.text(), '$123.45')
})
it('should render text with a prefix', () => {
const wrapper = shallow(<CurrencyDisplay
displayValue="$123.45"
className="currency-display"
prefix="-"
/>)
assert.ok(wrapper.hasClass('currency-display'))
assert.equal(wrapper.text(), '-$123.45')
})
})

View File

@ -0,0 +1,61 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps
proxyquire('../currency-display.container.js', {
'react-redux': {
connect: ms => {
mapStateToProps = ms
return () => ({})
},
},
})
describe('CurrencyDisplay container', () => {
describe('mapStateToProps()', () => {
it('should return the correct props', () => {
const mockState = {
metamask: {
conversionRate: 280.45,
currentCurrency: 'usd',
},
}
const tests = [
{
props: {
value: '0x2386f26fc10000',
numberOfDecimals: 2,
currency: 'usd',
},
result: {
displayValue: '$2.80 USD',
},
},
{
props: {
value: '0x2386f26fc10000',
},
result: {
displayValue: '$2.80 USD',
},
},
{
props: {
value: '0x1193461d01595930',
currency: 'ETH',
numberOfDecimals: 3,
},
result: {
displayValue: '1.266 ETH',
},
},
]
tests.forEach(({ props, result }) => {
assert.deepEqual(mapStateToProps(mockState, props), result)
})
})
})
})

View File

@ -0,0 +1 @@
export { default } from './token-currency-display.component'

View File

@ -0,0 +1,54 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import CurrencyDisplay from '../currency-display/currency-display.component'
import { getTokenData } from '../../helpers/transactions.util'
import { calcTokenAmount } from '../../token-util'
export default class TokenCurrencyDisplayContainer extends PureComponent {
static propTypes = {
transactionData: PropTypes.string,
token: PropTypes.object,
}
state = {
displayValue: '',
}
componentDidMount () {
this.setDisplayValue()
}
componentDidUpdate (prevProps) {
const { transactionData } = this.props
const { transactionData: prevTransactionData } = prevProps
if (transactionData !== prevTransactionData) {
this.setDisplayValue()
}
}
setDisplayValue () {
const { transactionData: data, token } = this.props
const { decimals = '', symbol = '' } = token
const tokenData = getTokenData(data)
let displayValue
if (tokenData.params && tokenData.params.length === 2) {
const tokenValue = tokenData.params[1].value
const tokenAmount = calcTokenAmount(tokenValue, decimals)
displayValue = `${tokenAmount} ${symbol}`
}
this.setState({ displayValue })
}
render () {
return (
<CurrencyDisplay
{...this.props}
displayValue={this.state.displayValue}
/>
)
}
}

View File

@ -0,0 +1,71 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import sinon from 'sinon'
import TokenBalance from '../../token-balance'
import CurrencyDisplay from '../../currency-display'
import { SEND_ROUTE } from '../../../routes'
import TokenViewBalance from '../token-view-balance.component'
const propsMethodSpies = {
showDepositModal: sinon.spy(),
}
const historySpies = {
push: sinon.spy(),
}
const t = (str1, str2) => str2 ? str1 + str2 : str1
describe('TokenViewBalance Component', () => {
afterEach(() => {
propsMethodSpies.showDepositModal.resetHistory()
historySpies.push.resetHistory()
})
it('should render ETH balance properly', () => {
const wrapper = shallow(<TokenViewBalance
showDepositModal={propsMethodSpies.showDepositModal}
history={historySpies}
network="3"
ethBalance={123}
fiatBalance={456}
currentCurrency="usd"
/>, { context: { t } })
assert.equal(wrapper.find('.token-view-balance').length, 1)
assert.equal(wrapper.find('.token-view-balance__button').length, 2)
assert.equal(wrapper.find(CurrencyDisplay).length, 2)
const buttons = wrapper.find('.token-view-balance__buttons')
assert.equal(propsMethodSpies.showDepositModal.callCount, 0)
buttons.childAt(0).simulate('click')
assert.equal(propsMethodSpies.showDepositModal.callCount, 1)
assert.equal(historySpies.push.callCount, 0)
buttons.childAt(1).simulate('click')
assert.equal(historySpies.push.callCount, 1)
assert.equal(historySpies.push.getCall(0).args[0], SEND_ROUTE)
})
it('should render token balance properly', () => {
const token = {
address: '0x35865238f0bec9d5ce6abff0fdaebe7b853dfcc5',
decimals: '2',
symbol: 'ABC',
}
const wrapper = shallow(<TokenViewBalance
showDepositModal={propsMethodSpies.showDepositModal}
history={historySpies}
network="3"
ethBalance={123}
fiatBalance={456}
currentCurrency="usd"
selectedToken={token}
/>, { context: { t } })
assert.equal(wrapper.find('.token-view-balance').length, 1)
assert.equal(wrapper.find('.token-view-balance__button').length, 1)
assert.equal(wrapper.find(TokenBalance).length, 1)
})
})

View File

@ -3,8 +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 { SEND_ROUTE } from '../../routes'
import { formatCurrency } from '../../helpers/confirm-transaction/util'
import { ETH } from '../../constants/common'
export default class TokenViewBalance extends PureComponent {
static contextTypes = {
@ -16,14 +17,11 @@ export default class TokenViewBalance extends PureComponent {
selectedToken: PropTypes.object,
history: PropTypes.object,
network: PropTypes.string,
ethBalance: PropTypes.string,
fiatBalance: PropTypes.string,
currentCurrency: PropTypes.string,
balance: PropTypes.string,
}
renderBalance () {
const { selectedToken, ethBalance, fiatBalance, currentCurrency } = this.props
const formattedFiatBalance = formatCurrency(fiatBalance, currentCurrency)
const { selectedToken, balance } = this.props
return selectedToken
? (
@ -34,12 +32,16 @@ export default class TokenViewBalance extends PureComponent {
/>
) : (
<div className="token-view-balance__balance">
<div className="token-view-balance__primary-balance">
{ `${ethBalance} ETH` }
</div>
<div className="token-view-balance__secondary-balance">
{ formattedFiatBalance }
</div>
<CurrencyDisplay
className="token-view-balance__primary-balance"
value={balance}
currency={ETH}
numberOfDecimals={3}
/>
<CurrencyDisplay
className="token-view-balance__secondary-balance"
value={balance}
/>
</div>
)
}

View File

@ -4,29 +4,17 @@ import { compose } from 'recompose'
import TokenViewBalance from './token-view-balance.component'
import { getSelectedToken, getSelectedAddress } from '../../selectors'
import { showModal } from '../../actions'
import { getValueFromWeiHex } from '../../helpers/confirm-transaction/util'
const mapStateToProps = state => {
const selectedAddress = getSelectedAddress(state)
const { metamask } = state
const { network, accounts, currentCurrency, conversionRate } = metamask
const { metamask: { network, accounts } } = state
const account = accounts[selectedAddress]
const { balance: value } = account
const ethBalance = getValueFromWeiHex({
value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 3,
})
const fiatBalance = getValueFromWeiHex({
value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
})
const { balance } = account
return {
selectedToken: getSelectedToken(state),
network,
ethBalance,
fiatBalance,
currentCurrency,
balance,
}
}

View File

@ -0,0 +1,102 @@
import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import TransactionAction from '../transaction-action.component'
describe('TransactionAction Component', () => {
const tOrDefault = key => key
describe('Outgoing transaction', () => {
it('should render -- when methodData is still fetching', () => {
const methodData = { data: {}, done: false, error: null }
const transaction = {
id: 1,
status: 'confirmed',
submittedTime: 1534045442919,
time: 1534045440641,
txParams: {
from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0x96',
to: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
value: '0x2386f26fc10000',
},
}
const wrapper = shallow(<TransactionAction
methodData={methodData}
transaction={transaction}
className="transaction-action"
/>, { context: { tOrDefault }})
assert.equal(wrapper.find('.transaction-action').length, 1)
assert.equal(wrapper.text(), '--')
})
it('should render Outgoing', () => {
const methodData = { data: {}, done: true, error: null }
const transaction = {
id: 1,
status: 'confirmed',
submittedTime: 1534045442919,
time: 1534045440641,
txParams: {
from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0x96',
to: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
value: '0x2386f26fc10000',
},
}
const wrapper = shallow(<TransactionAction
methodData={methodData}
transaction={transaction}
className="transaction-action"
/>, { context: { tOrDefault }})
assert.equal(wrapper.find('.transaction-action').length, 1)
assert.equal(wrapper.text(), 'outgoing')
})
it('should render Approved', () => {
const methodData = {
data: {
name: 'Approve',
params: [
{ type: 'address' },
{ type: 'uint256' },
],
},
done: true,
error: null,
}
const transaction = {
id: 1,
status: 'confirmed',
submittedTime: 1534045442919,
time: 1534045440641,
txParams: {
from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
gas: '0x5208',
gasPrice: '0x3b9aca00',
nonce: '0x96',
to: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
value: '0x2386f26fc10000',
data: '0x095ea7b300000000000000000000000050a9d56c2b8ba9a5c7f2c08c3d26e0499f23a7060000000000000000000000000000000000000000000000000000000000000003',
},
}
const wrapper = shallow(<TransactionAction
methodData={methodData}
transaction={transaction}
className="transaction-action"
/>, { context: { tOrDefault }})
assert.equal(wrapper.find('.transaction-action').length, 1)
assert.equal(wrapper.text(), 'approve')
})
})
})

View File

@ -3,21 +3,24 @@ import PropTypes from 'prop-types'
import Identicon from '../identicon'
import TransactionStatus from '../transaction-status'
import TransactionAction from '../transaction-action'
import CurrencyDisplay from '../currency-display'
import TokenCurrencyDisplay from '../token-currency-display'
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
import { CONFIRM_TRANSACTION_ROUTE } from '../../routes'
import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions'
import { ETH } from '../../constants/common'
export default class TransactionListItem extends PureComponent {
static propTypes = {
history: PropTypes.object,
transaction: PropTypes.object,
ethTransactionAmount: PropTypes.string,
fiatDisplayValue: PropTypes.string,
value: PropTypes.string,
methodData: PropTypes.object,
showRetry: PropTypes.bool,
retryTransaction: PropTypes.func,
setSelectedToken: PropTypes.func,
nonceAndDate: PropTypes.string,
token: PropTypes.object,
}
handleClick = () => {
@ -55,18 +58,50 @@ export default class TransactionListItem extends PureComponent {
.then(id => history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
}
renderPrimaryCurrency () {
const { token, transaction: { txParams: { data } = {} } = {}, value } = this.props
return token
? (
<TokenCurrencyDisplay
className="transaction-list-item__amount transaction-list-item__amount--primary"
token={token}
transactionData={data}
prefix="-"
/>
) : (
<CurrencyDisplay
className="transaction-list-item__amount transaction-list-item__amount--primary"
value={value}
prefix="-"
/>
)
}
renderSecondaryCurrency () {
const { token, value } = this.props
return token
? null
: (
<CurrencyDisplay
className="transaction-list-item__amount transaction-list-item__amount--secondary"
prefix="-"
value={value}
numberOfDecimals={2}
currency={ETH}
/>
)
}
render () {
const {
transaction,
ethTransactionAmount,
fiatDisplayValue,
methodData,
showRetry,
nonceAndDate,
} = this.props
const { txParams = {} } = transaction
const fiatDisplayText = `-${fiatDisplayValue}`
const ethDisplayText = ethTransactionAmount && `-${ethTransactionAmount} ETH`
return (
<div
@ -94,18 +129,8 @@ export default class TransactionListItem extends PureComponent {
className="transaction-list-item__status"
status={transaction.status}
/>
<div
className="transaction-list-item__amount transaction-list-item__amount--primary"
title={fiatDisplayText}
>
{ fiatDisplayText }
</div>
<div
className="transaction-list-item__amount transaction-list-item__amount--secondary"
title={ethDisplayText}
>
{ ethDisplayText }
</div>
{ this.renderPrimaryCurrency() }
{ this.renderSecondaryCurrency() }
</div>
{
showRetry && !methodData.isFetching && (

View File

@ -4,42 +4,16 @@ import { compose } from 'recompose'
import withMethodData from '../../higher-order-components/with-method-data'
import TransactionListItem from './transaction-list-item.component'
import { setSelectedToken, retryTransaction } from '../../actions'
import { getEthFromWeiHex, getValueFromWeiHex, hexToDecimal } from '../../helpers/conversions.util'
import { getTokenData } from '../../helpers/transactions.util'
import { formatCurrency } from '../../helpers/confirm-transaction/util'
import { calcTokenAmount } from '../../token-util'
import { hexToDecimal } from '../../helpers/conversions.util'
import { formatDate } from '../../util'
const mapStateToProps = (state, ownProps) => {
const { metamask } = state
const { currentCurrency, conversionRate } = metamask
const { transaction: { txParams: { value, data, nonce } = {}, time } = {}, token } = ownProps
let ethTransactionAmount, fiatDisplayValue
if (token) {
const { decimals = '', symbol = '' } = token
const tokenData = getTokenData(data)
if (tokenData.params && tokenData.params.length === 2) {
const tokenValue = tokenData.params[1].value
const tokenAmount = calcTokenAmount(tokenValue, decimals)
fiatDisplayValue = `${tokenAmount} ${symbol}`
}
} else {
ethTransactionAmount = getEthFromWeiHex({ value, conversionRate })
const fiatTransactionAmount = getValueFromWeiHex({
value, conversionRate, toCurrency: currentCurrency, numberOfDecimals: 2,
})
const fiatFormattedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
fiatDisplayValue = `${fiatFormattedAmount} ${currentCurrency.toUpperCase()}`
}
const { transaction: { txParams: { value, nonce } = {}, time } = {} } = ownProps
const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
return {
ethTransactionAmount,
fiatDisplayValue,
value,
nonceAndDate,
}
}

View File

@ -0,0 +1 @@
export const ETH = 'ETH'