1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

factor out containers for currency components (#8543)

This commit is contained in:
Brad Decker 2020-05-12 14:07:35 -05:00 committed by GitHub
parent 54b423d407
commit 0aa41e397e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 735 additions and 607 deletions

View File

@ -196,6 +196,7 @@
"@storybook/core": "^5.3.14",
"@storybook/react": "^5.3.14",
"@storybook/storybook-deployer": "^2.8.1",
"@testing-library/react-hooks": "^3.2.1",
"addons-linter": "1.14.0",
"babel-eslint": "^10.0.2",
"babel-loader": "^8.0.6",

View File

@ -1 +1 @@
export { default } from './user-preferenced-currency-display.container'
export { default } from './user-preferenced-currency-display.component'

View File

@ -3,9 +3,17 @@ import assert from 'assert'
import { shallow } from 'enzyme'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display.component'
import CurrencyDisplay from '../../../ui/currency-display'
import * as currencyHook from '../../../../hooks/useCurrencyDisplay'
import * as currencyPrefHook from '../../../../hooks/useUserPreferencedCurrency'
import sinon from 'sinon'
describe('UserPreferencedCurrencyDisplay Component', function () {
describe('rendering', function () {
beforeEach(function () {
sinon.stub(currencyHook, 'useCurrencyDisplay').returns(['1', {}])
sinon.stub(currencyPrefHook, 'useUserPreferencedCurrency').returns({ currency: 'ETH', decimals: 6 })
})
it('should render properly', function () {
const wrapper = shallow(
<UserPreferencedCurrencyDisplay />
@ -30,5 +38,8 @@ describe('UserPreferencedCurrencyDisplay Component', function () {
assert.equal(wrapper.find(CurrencyDisplay).props().prop2, 'test')
assert.equal(wrapper.find(CurrencyDisplay).props().prop3, 1)
})
afterEach(function () {
sinon.restore()
})
})
})

View File

@ -1,202 +0,0 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps, mergeProps
proxyquire('../user-preferenced-currency-display.container.js', {
'react-redux': {
connect: (ms, _, mp) => {
mapStateToProps = ms
mergeProps = mp
return () => ({})
},
},
})
describe('UserPreferencedCurrencyDisplay container', function () {
describe('mapStateToProps()', function () {
it('should return the correct props', function () {
const mockState = {
metamask: {
nativeCurrency: 'ETH',
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
showFiatInTestnets: false,
},
provider: {
type: 'mainnet',
},
},
}
assert.deepEqual(mapStateToProps(mockState), {
nativeCurrency: 'ETH',
useNativeCurrencyAsPrimaryCurrency: true,
isMainnet: true,
showFiatInTestnets: false,
})
})
it('should return the correct props when not in mainnet and showFiatInTestnets is true', function () {
const mockState = {
metamask: {
nativeCurrency: 'ETH',
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
showFiatInTestnets: true,
},
provider: {
type: 'rinkeby',
},
},
}
assert.deepEqual(mapStateToProps(mockState), {
nativeCurrency: 'ETH',
useNativeCurrencyAsPrimaryCurrency: true,
isMainnet: false,
showFiatInTestnets: true,
})
})
})
describe('mergeProps()', function () {
it('should return the correct props', function () {
const mockDispatchProps = {}
const tests = [
{
stateProps: {
useNativeCurrencyAsPrimaryCurrency: true,
nativeCurrency: 'ETH',
isMainnet: true,
showFiatInTestnets: false,
},
ownProps: {
type: 'PRIMARY',
},
result: {
currency: 'ETH',
nativeCurrency: 'ETH',
numberOfDecimals: 6,
prefix: undefined,
},
},
{
stateProps: {
useNativeCurrencyAsPrimaryCurrency: false,
nativeCurrency: 'ETH',
isMainnet: true,
showFiatInTestnets: false,
},
ownProps: {
type: 'PRIMARY',
},
result: {
currency: undefined,
nativeCurrency: 'ETH',
numberOfDecimals: 2,
prefix: undefined,
},
},
{
stateProps: {
useNativeCurrencyAsPrimaryCurrency: true,
nativeCurrency: 'ETH',
isMainnet: true,
showFiatInTestnets: false,
},
ownProps: {
type: 'SECONDARY',
fiatNumberOfDecimals: 4,
fiatPrefix: '-',
},
result: {
nativeCurrency: 'ETH',
currency: undefined,
numberOfDecimals: 4,
prefix: '-',
},
},
{
stateProps: {
useNativeCurrencyAsPrimaryCurrency: false,
nativeCurrency: 'ETH',
isMainnet: true,
showFiatInTestnets: false,
},
ownProps: {
type: 'SECONDARY',
fiatNumberOfDecimals: 4,
numberOfDecimals: 3,
fiatPrefix: 'a',
prefix: 'b',
},
result: {
currency: 'ETH',
nativeCurrency: 'ETH',
numberOfDecimals: 3,
prefix: 'b',
},
},
{
stateProps: {
useNativeCurrencyAsPrimaryCurrency: false,
nativeCurrency: 'ETH',
isMainnet: false,
showFiatInTestnets: false,
},
ownProps: {
type: 'PRIMARY',
},
result: {
currency: 'ETH',
nativeCurrency: 'ETH',
numberOfDecimals: 6,
prefix: undefined,
},
},
{
stateProps: {
useNativeCurrencyAsPrimaryCurrency: false,
nativeCurrency: 'ETH',
isMainnet: false,
showFiatInTestnets: true,
},
ownProps: {
type: 'PRIMARY',
},
result: {
currency: undefined,
nativeCurrency: 'ETH',
numberOfDecimals: 2,
prefix: undefined,
},
},
{
stateProps: {
useNativeCurrencyAsPrimaryCurrency: false,
nativeCurrency: 'ETH',
isMainnet: true,
showFiatInTestnets: true,
},
ownProps: {
type: 'PRIMARY',
},
result: {
currency: undefined,
nativeCurrency: 'ETH',
numberOfDecimals: 2,
prefix: undefined,
},
},
]
tests.forEach(({ stateProps, ownProps, result }) => {
assert.deepEqual(mergeProps({ ...stateProps }, mockDispatchProps, { ...ownProps }), {
...result,
})
})
})
})
})

View File

@ -1,47 +1,42 @@
import React, { PureComponent } from 'react'
import React, { useMemo } from 'react'
import PropTypes from 'prop-types'
import { PRIMARY, SECONDARY, ETH } from '../../../helpers/constants/common'
import CurrencyDisplay from '../../ui/currency-display'
import { useUserPreferencedCurrency } from '../../../hooks/useUserPreferencedCurrency'
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,
hideTitle: 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
export default function UserPreferencedCurrencyDisplay ({ type, showEthLogo, ethLogoHeight = 12, ethNumberOfDecimals, fiatNumberOfDecimals, numberOfDecimals: propsNumberOfDecimals, ...restProps }) {
const { currency, numberOfDecimals } = useUserPreferencedCurrency(type, { ethNumberOfDecimals, fiatNumberOfDecimals, numberOfDecimals: propsNumberOfDecimals })
const prefixComponent = useMemo(() => {
return currency === ETH && showEthLogo && (
<img
src="/images/eth.svg"
height={ethLogoHeight}
/>
)
}
}, [currency, showEthLogo, ethLogoHeight])
render () {
return (
<CurrencyDisplay
{...this.props}
prefixComponent={this.renderEthLogo()}
/>
)
}
return (
<CurrencyDisplay
{...restProps}
currency={currency}
numberOfDecimals={numberOfDecimals}
prefixComponent={prefixComponent}
/>
)
}
UserPreferencedCurrencyDisplay.propTypes = {
className: PropTypes.string,
prefix: PropTypes.string,
value: PropTypes.string,
numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
hideLabel: PropTypes.bool,
hideTitle: PropTypes.bool,
style: PropTypes.object,
showEthLogo: PropTypes.bool,
ethLogoHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
type: PropTypes.oneOf([PRIMARY, SECONDARY]),
ethNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
fiatNumberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}

View File

@ -1,67 +0,0 @@
import { connect } from 'react-redux'
import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display.component'
import { preferencesSelector, getIsMainnet } from '../../../selectors'
import { ETH, PRIMARY, SECONDARY } from '../../../helpers/constants/common'
const mapStateToProps = (state) => {
const {
useNativeCurrencyAsPrimaryCurrency,
showFiatInTestnets,
} = preferencesSelector(state)
const isMainnet = getIsMainnet(state)
return {
useNativeCurrencyAsPrimaryCurrency,
showFiatInTestnets,
isMainnet,
nativeCurrency: state.metamask.nativeCurrency,
}
}
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const { useNativeCurrencyAsPrimaryCurrency, showFiatInTestnets, isMainnet, 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
}
if (!isMainnet && !showFiatInTestnets) {
currency = nativeCurrency || ETH
numberOfDecimals = propsNumberOfDecimals || ethNumberOfDecimals || 6
prefix = propsPrefix || ethPrefix
}
return {
...restStateProps,
...dispatchProps,
...restOwnProps,
nativeCurrency,
currency,
numberOfDecimals,
prefix,
}
}
export default connect(mapStateToProps, null, mergeProps)(UserPreferencedCurrencyDisplay)

View File

@ -1,40 +1,62 @@
import React, { PureComponent } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { GWEI } from '../../../helpers/constants/common'
import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay'
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,
hideTitle: PropTypes.bool,
}
render () {
const { className, displayValue, prefix, prefixComponent, style, suffix, hideTitle } = this.props
const text = `${prefix || ''}${displayValue}`
const title = suffix ? `${text} ${suffix}` : text
return (
<div
className={classnames('currency-display-component', className)}
style={style}
title={(!hideTitle && title) || null}
>
{ prefixComponent }
<span className="currency-display-component__text">{ text }</span>
{
suffix && (
<span className="currency-display-component__suffix">
{ suffix }
</span>
)
}
</div>
)
}
export default function CurrencyDisplay ({
value,
displayValue,
style,
className,
prefix,
prefixComponent,
hideLabel,
hideTitle,
numberOfDecimals,
denomination,
currency,
suffix,
}) {
const [title, parts] = useCurrencyDisplay(value, {
displayValue,
prefix,
numberOfDecimals,
hideLabel,
denomination,
currency,
suffix,
})
return (
<div
className={classnames('currency-display-component', className)}
style={style}
title={(!hideTitle && title) || null}
>
{ prefixComponent }
<span className="currency-display-component__text">{ parts.prefix }{ parts.value }</span>
{
parts.suffix && (
<span className="currency-display-component__suffix">
{ parts.suffix }
</span>
)
}
</div>
)
}
CurrencyDisplay.propTypes = {
className: PropTypes.string,
currency: PropTypes.string,
denomination: PropTypes.oneOf([GWEI]),
displayValue: PropTypes.string,
hideLabel: PropTypes.bool,
hideTitle: PropTypes.bool,
numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
prefix: PropTypes.string,
prefixComponent: PropTypes.node,
style: PropTypes.object,
suffix: PropTypes.string,
value: PropTypes.string,
}

View File

@ -1,68 +0,0 @@
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import CurrencyDisplay from './currency-display.component'
import { getValueFromWeiHex, formatCurrency } from '../../../helpers/utils/confirm-tx.util'
import { GWEI } from '../../../helpers/constants/common'
const mapStateToProps = (state) => {
const { metamask: { nativeCurrency, currentCurrency, conversionRate } } = state
return {
currentCurrency,
conversionRate,
nativeCurrency,
}
}
const mergeProps = (stateProps, _, ownProps) => {
const { nativeCurrency, currentCurrency, conversionRate } = stateProps
const {
value,
numberOfDecimals = 2,
currency,
denomination,
hideLabel,
displayValue: propsDisplayValue,
suffix: propsSuffix,
...restOwnProps
} = ownProps
const toCurrency = currency || currentCurrency
const displayValue = propsDisplayValue || formatCurrency(
getValueFromWeiHex({
value,
fromCurrency: nativeCurrency,
toCurrency, conversionRate,
numberOfDecimals,
toDenomination: denomination,
}),
toCurrency
)
const suffix = propsSuffix || (hideLabel ? undefined : toCurrency.toUpperCase())
return {
...restOwnProps,
displayValue,
suffix,
}
}
const CurrencyDisplayContainer = connect(mapStateToProps, null, mergeProps)(CurrencyDisplay)
CurrencyDisplayContainer.propTypes = {
className: PropTypes.string,
currency: PropTypes.string,
denomination: PropTypes.oneOf([GWEI]),
displayValue: PropTypes.string,
hideLabel: PropTypes.bool,
hideTitle: PropTypes.bool,
numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
prefix: PropTypes.string,
prefixComponent: PropTypes.node,
style: PropTypes.object,
suffix: PropTypes.string,
value: PropTypes.string,
}
export default CurrencyDisplayContainer

View File

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

View File

@ -2,13 +2,24 @@ import React from 'react'
import assert from 'assert'
import { shallow } from 'enzyme'
import CurrencyDisplay from '../currency-display.component'
import sinon from 'sinon'
import * as reactRedux from 'react-redux'
describe('CurrencyDisplay Component', function () {
beforeEach(function () {
const stub = sinon.stub(reactRedux, 'useSelector')
stub.callsFake(() => ({
currentCurrency: 'usd',
nativeCurrency: 'ETH',
conversionRate: 280.45,
}))
})
it('should render text with a className', function () {
const wrapper = shallow((
<CurrencyDisplay
displayValue="$123.45"
className="currency-display"
hideLabel
/>
))
@ -22,10 +33,14 @@ describe('CurrencyDisplay Component', function () {
displayValue="$123.45"
className="currency-display"
prefix="-"
hideLabel
/>
))
assert.ok(wrapper.hasClass('currency-display'))
assert.equal(wrapper.text(), '-$123.45')
})
afterEach(function () {
sinon.restore()
})
})

View File

@ -1,145 +0,0 @@
import assert from 'assert'
import proxyquire from 'proxyquire'
let mapStateToProps, mergeProps
proxyquire('../currency-display.container.js', {
'react-redux': {
connect: (ms, _, mp) => {
mapStateToProps = ms
mergeProps = mp
return () => ({})
},
},
})
describe('CurrencyDisplay container', function () {
describe('mapStateToProps()', function () {
it('should return the correct props', function () {
const mockState = {
metamask: {
conversionRate: 280.45,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
},
}
assert.deepEqual(mapStateToProps(mockState), {
conversionRate: 280.45,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
})
})
})
describe('mergeProps()', function () {
it('should return the correct props', function () {
const mockStateProps = {
conversionRate: 280.45,
currentCurrency: 'usd',
nativeCurrency: 'ETH',
}
const tests = [
{
props: {
value: '0x2386f26fc10000',
numberOfDecimals: 2,
currency: 'usd',
nativeCurrency: 'ETH',
},
result: {
displayValue: '$2.80',
suffix: 'USD',
nativeCurrency: 'ETH',
},
},
{
props: {
value: '0x2386f26fc10000',
currency: 'usd',
nativeCurrency: 'ETH',
},
result: {
displayValue: '$2.80',
suffix: 'USD',
nativeCurrency: 'ETH',
},
},
{
props: {
value: '0x1193461d01595930',
currency: 'ETH',
nativeCurrency: 'ETH',
numberOfDecimals: 3,
},
result: {
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: '0.000000001',
suffix: undefined,
},
},
]
tests.forEach(({ props, result }) => {
assert.deepEqual(mergeProps(mockStateProps, {}, { ...props }), result)
})
})
})
})

View File

@ -1,57 +1,24 @@
import React, { PureComponent } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import CurrencyDisplay from '../currency-display'
import { getTokenData } from '../../../helpers/utils/transactions.util'
import { getTokenValue, calcTokenAmount } from '../../../helpers/utils/token-util'
import { useTokenDisplayValue } from '../../../hooks/useTokenDisplayValue'
export default class TokenCurrencyDisplay extends PureComponent {
static propTypes = {
transactionData: PropTypes.string,
token: PropTypes.object,
}
export default function TokenCurrencyDisplay ({ className, transactionData, token, prefix }) {
const displayValue = useTokenDisplayValue(transactionData, token)
state = {
displayValue: '',
suffix: '',
}
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: suffix = '' } = token
const tokenData = getTokenData(data)
let displayValue
if (tokenData && tokenData.params && tokenData.params.length) {
const tokenValue = getTokenValue(tokenData.params)
displayValue = calcTokenAmount(tokenValue, decimals).toString()
}
this.setState({ displayValue, suffix })
}
render () {
const { displayValue, suffix } = this.state
return (
<CurrencyDisplay
{...this.props}
displayValue={displayValue}
suffix={suffix}
/>
)
}
return (
<CurrencyDisplay
className={className}
prefix={prefix}
displayValue={displayValue}
suffix={token.symbol}
/>
)
}
TokenCurrencyDisplay.propTypes = {
className: PropTypes.string,
transactionData: PropTypes.string,
token: PropTypes.object,
prefix: PropTypes.string,
}

View File

@ -0,0 +1,121 @@
import assert from 'assert'
import { renderHook } from '@testing-library/react-hooks'
import * as reactRedux from 'react-redux'
import { useCurrencyDisplay } from '../useCurrencyDisplay'
import sinon from 'sinon'
const tests = [
{
input: {
value: '0x2386f26fc10000',
numberOfDecimals: 2,
currency: 'usd',
},
result: {
value: '$2.80',
suffix: 'USD',
displayValue: '$2.80 USD',
},
},
{
input: {
value: '0x2386f26fc10000',
currency: 'usd',
},
result: {
value: '$2.80',
suffix: 'USD',
displayValue: '$2.80 USD',
},
},
{
input: {
value: '0x1193461d01595930',
currency: 'ETH',
numberOfDecimals: 3,
},
result: {
value: '1.266',
suffix: 'ETH',
displayValue: '1.266 ETH',
},
},
{
input: {
value: '0x1193461d01595930',
currency: 'ETH',
numberOfDecimals: 3,
hideLabel: true,
},
result: {
value: '1.266',
suffix: undefined,
displayValue: '1.266',
},
},
{
input: {
value: '0x3b9aca00',
currency: 'ETH',
denomination: 'GWEI',
hideLabel: true,
},
result: {
value: '1',
suffix: undefined,
displayValue: '1',
},
},
{
input: {
value: '0x3b9aca00',
currency: 'ETH',
denomination: 'WEI',
hideLabel: true,
},
result: {
value: '1000000000',
suffix: undefined,
displayValue: '1000000000',
},
},
{
input: {
value: '0x3b9aca00',
currency: 'ETH',
numberOfDecimals: 100,
hideLabel: true,
},
result: {
value: '0.000000001',
suffix: undefined,
displayValue: '0.000000001',
},
},
]
describe('useCurrencyDisplay', function () {
tests.forEach(({ input: { value, ...restProps }, result }) => {
describe(`when input is { value: ${value}, decimals: ${restProps.numberOfDecimals}, denomation: ${restProps.denomination} }`, function () {
const stub = sinon.stub(reactRedux, 'useSelector')
stub.callsFake(() => ({
currentCurrency: 'usd',
nativeCurrency: 'ETH',
conversionRate: 280.45,
}))
const hookReturn = renderHook(() => useCurrencyDisplay(value, restProps))
const [ displayValue, parts ] = hookReturn.result.current
stub.restore()
it(`should return ${result.displayValue} as displayValue`, function () {
assert.equal(displayValue, result.displayValue)
})
it(`should return ${result.value} as value`, function () {
assert.equal(parts.value, result.value)
})
it(`should return ${result.suffix} as suffix`, function () {
assert.equal(parts.suffix, result.suffix)
})
})
})
})

View File

@ -0,0 +1,136 @@
import assert from 'assert'
import { renderHook } from '@testing-library/react-hooks'
import * as tokenUtil from '../../helpers/utils/token-util'
import * as txUtil from '../../helpers/utils/transactions.util'
import { useTokenDisplayValue } from '../useTokenDisplayValue'
import sinon from 'sinon'
const tests = [
{
token: {
symbol: 'DAI',
decimals: 18,
},
tokenData: {
params: 'decoded-params1',
},
tokenValue: '1000000000000000000',
displayValue: '1',
},
{
token: {
symbol: 'DAI',
decimals: 18,
},
tokenData: {
params: 'decoded-params2',
},
tokenValue: '10000000000000000000',
displayValue: '10',
},
{
token: {
symbol: 'DAI',
decimals: 18,
},
tokenData: {
params: 'decoded-params3',
},
tokenValue: '1500000000000000000',
displayValue: '1.5',
},
{
token: {
symbol: 'DAI',
decimals: 18,
},
tokenData: {
params: 'decoded-params4',
},
tokenValue: '1756000000000000000',
displayValue: '1.756',
},
{
token: {
symbol: 'DAI',
decimals: 18,
},
tokenData: {
params: 'decoded-params5',
},
tokenValue: '25500000000000000000',
displayValue: '25.5',
},
{
token: {
symbol: 'USDC',
decimals: 6,
},
tokenData: {
params: 'decoded-params6',
},
tokenValue: '1000000',
displayValue: '1',
},
{
token: {
symbol: 'USDC',
decimals: 6,
},
tokenData: {
params: 'decoded-params7',
},
tokenValue: '10000000',
displayValue: '10',
},
{
token: {
symbol: 'USDC',
decimals: 6,
},
tokenData: {
params: 'decoded-params8',
},
tokenValue: '1500000',
displayValue: '1.5',
},
{
token: {
symbol: 'USDC',
decimals: 6,
},
tokenData: {
params: 'decoded-params9',
},
tokenValue: '1756000',
displayValue: '1.756',
},
{
token: {
symbol: 'USDC',
decimals: 6,
},
tokenData: {
params: 'decoded-params10',
},
tokenValue: '25500000',
displayValue: '25.5',
},
]
describe('useTokenDisplayValue', function () {
tests.forEach((test, idx) => {
describe(`when input is decimals: ${test.token.decimals} and value: ${test.tokenValue}`, function () {
it(`should return ${test.displayValue} as displayValue`, function () {
const getTokenValueStub = sinon.stub(tokenUtil, 'getTokenValue')
const getTokenDataStub = sinon.stub(txUtil, 'getTokenData')
getTokenDataStub.callsFake(() => test.tokenData)
getTokenValueStub.callsFake(() => test.tokenValue)
const { result } = renderHook(() => useTokenDisplayValue(`${idx}-fakestring`, test.token))
sinon.restore()
assert.equal(result.current, test.displayValue)
})
})
})
})

View File

@ -0,0 +1,143 @@
import assert from 'assert'
import { renderHook } from '@testing-library/react-hooks'
import { useUserPreferencedCurrency } from '../useUserPreferencedCurrency'
import * as reactRedux from 'react-redux'
import { preferencesSelector, getShouldShowFiat } from '../../selectors'
import sinon from 'sinon'
const tests = [
{
state: {
useNativeCurrencyAsPrimaryCurrency: true,
nativeCurrency: 'ETH',
showFiat: true,
},
params: {
type: 'PRIMARY',
},
result: {
currency: 'ETH',
numberOfDecimals: 6,
},
},
{
state: {
useNativeCurrencyAsPrimaryCurrency: false,
nativeCurrency: 'ETH',
showFiat: true,
},
params: {
type: 'PRIMARY',
},
result: {
currency: undefined,
numberOfDecimals: 2,
},
},
{
state: {
useNativeCurrencyAsPrimaryCurrency: true,
nativeCurrency: 'ETH',
showFiat: true,
},
params: {
type: 'SECONDARY',
fiatNumberOfDecimals: 4,
fiatPrefix: '-',
},
result: {
currency: undefined,
numberOfDecimals: 4,
},
},
{
state: {
useNativeCurrencyAsPrimaryCurrency: false,
nativeCurrency: 'ETH',
showFiat: true,
},
params: {
type: 'SECONDARY',
fiatNumberOfDecimals: 4,
numberOfDecimals: 3,
fiatPrefix: 'a',
},
result: {
currency: 'ETH',
numberOfDecimals: 3,
},
},
{
state: {
useNativeCurrencyAsPrimaryCurrency: false,
nativeCurrency: 'ETH',
showFiat: false,
},
params: {
type: 'PRIMARY',
},
result: {
currency: 'ETH',
numberOfDecimals: 6,
},
},
{
state: {
useNativeCurrencyAsPrimaryCurrency: false,
nativeCurrency: 'ETH',
showFiat: true,
},
params: {
type: 'PRIMARY',
},
result: {
currency: undefined,
numberOfDecimals: 2,
},
},
{
state: {
useNativeCurrencyAsPrimaryCurrency: false,
nativeCurrency: 'ETH',
showFiat: true,
},
params: {
type: 'PRIMARY',
},
result: {
currency: undefined,
numberOfDecimals: 2,
},
},
]
function getFakeUseSelector (state) {
return (selector) => {
if (selector === preferencesSelector) {
return state
} else if (selector === getShouldShowFiat) {
return state.showFiat
} else {
return state.nativeCurrency
}
}
}
describe('useUserPreferencedCurrency', function () {
tests.forEach(({ params: { type, ...otherParams }, state, result }) => {
describe(`when showFiat is ${state.showFiat}, useNativeCurrencyAsPrimary is ${state.useNativeCurrencyAsPrimaryCurrency} and type is ${type}`, function () {
const stub = sinon.stub(reactRedux, 'useSelector')
stub.callsFake(getFakeUseSelector(state))
const { result: hookResult } = renderHook(() => useUserPreferencedCurrency(type, otherParams))
stub.restore()
it(`should return currency as ${result.currency || 'not modified by user preferences'}`, function () {
assert.equal(hookResult.current.currency, result.currency)
})
it(`should return decimals as ${result.numberOfDecimals || 'not modified by user preferences'}`, function () {
assert.equal(hookResult.current.numberOfDecimals, result.numberOfDecimals)
})
})
})
})

View File

@ -0,0 +1,69 @@
import { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { formatCurrency, getValueFromWeiHex } from '../helpers/utils/confirm-tx.util'
/**
* Defines the shape of the options parameter for useCurrencyDisplay
* @typedef {Object} UseCurrencyOptions
* @property {string} [displayValue] - When present is used in lieu of formatting the inputValue
* @property {string} [prefix] - String to prepend to the final result
* @property {number} [numberOfDecimals] - Number of significant decimals to display
* @property {string} [denomination] - Denomination (wei, gwei) to convert to for display
* @property {string} [currency] - Currency type to convert to. Will override nativeCurrency
*/
/**
* Defines the return shape of the second value in the tuple
* @typedef {Object} CurrencyDisplayParts
* @property {string} [prefix] - string to prepend to the value for display
* @property {string} value - string representing the value, formatted for display
* @property {string} [suffix] - string to append to the value for display
*/
/**
* useCurrencyDisplay hook
*
* Given a hexadecimal encoded value string and an object of parameters used for formatting the
* display, produce both a fully formed string and the pieces of that string used for displaying
* the currency to the user
* @param {string} inputValue - The value to format for display
* @param {UseCurrencyOptions} opts - An object for options to format the inputValue
* @return {[string, CurrencyDisplayParts]}
*/
export function useCurrencyDisplay (inputValue, { displayValue, prefix, numberOfDecimals, denomination, currency, ...opts }) {
const { currentCurrency, nativeCurrency, conversionRate } = useSelector(
({ metamask: { currentCurrency, nativeCurrency, conversionRate } }) => ({
currentCurrency,
nativeCurrency,
conversionRate,
})
)
const toCurrency = currency || currentCurrency
const value = useMemo(() => {
if (displayValue) {
return displayValue
}
return formatCurrency(
getValueFromWeiHex({
value: inputValue,
fromCurrency: nativeCurrency,
toCurrency,
conversionRate,
numberOfDecimals: numberOfDecimals || 2,
toDenomination: denomination,
}),
toCurrency
)
}, [inputValue, nativeCurrency, conversionRate, displayValue, numberOfDecimals, denomination, toCurrency])
let suffix
if (!opts.hideLabel) {
suffix = opts.suffix || toCurrency.toUpperCase()
}
return [`${prefix || ''}${value}${suffix ? ' ' + suffix : ''}`, { prefix, value, suffix }]
}

View File

@ -0,0 +1,38 @@
import { getTokenValue, calcTokenAmount } from '../helpers/utils/token-util'
import { getTokenData } from '../helpers/utils/transactions.util'
import { useMemo } from 'react'
/**
* Defines the shape for the Token input parameter for useTokenDisplayValue
* @typedef {Object} Token
* @property {string} symbol - The string to use as a suffix for the token (eg. DAI)
* @property {number} decimals - The number of decimals to show when displaying this type of token
*/
/**
* useTokenDisplayValue
* Given the data string from txParams and a token object with symbol and decimals, return
* a displayValue that represents a string representing that token amount as a string. Also
* return a tokenData object for downstream usage and the suffix for the token to use as props
* for other hooks and/or components
* @param {string} transactionData
* @param {Token} token
* @return {string} - The computed displayValue of the provided transactionData and token
*/
export function useTokenDisplayValue (transactionData, token) {
if (!transactionData || !token) {
return null
}
const tokenData = useMemo(() => getTokenData(transactionData), [transactionData])
if (!tokenData?.params?.length) {
return null
}
const { decimals } = token
const displayValue = useMemo(() => {
const tokenValue = getTokenValue(tokenData.params)
return calcTokenAmount(tokenValue, decimals).toString()
}, [tokenData, decimals])
return displayValue
}

View File

@ -0,0 +1,52 @@
import { preferencesSelector, getShouldShowFiat } from '../selectors'
import { useSelector } from 'react-redux'
import { PRIMARY, SECONDARY, ETH } from '../helpers/constants/common'
/**
* Defines the shape of the options parameter for useUserPreferencedCurrency
* @typedef {Object} UseUserPreferencedCurrencyOptions
* @property {number} [numberOfDecimals] - Number of significant decimals to display
* @property {number} [ethNumberOfDecimals] - Number of significant decimals to display
* when using ETH
* @property {number} [fiatNumberOfDecimals] - Number of significant decimals to display
* when using fiat
*/
/**
* Defines the return shape of useUserPreferencedCurrency
* @typedef {Object} UserPreferredCurrency
* @property {string} currency - the currency type to use (eg: 'ETH', 'usd')
* @property {number} numberOfDecimals - Number of significant decimals to display
*/
/**
* useUserPreferencedCurrency
*
* returns an object that contains what currency to use for displaying values based
* on the user's preference settings, as well as the significant number of decimals
* to display based on the currency
* @param {"PRIMARY" | "SECONDARY"} type - what display type is being rendered
* @param {UseUserPreferencedCurrencyOptions} opts - options to override default values
* @return {UserPreferredCurrency}
*/
export function useUserPreferencedCurrency (type, opts = {}) {
const nativeCurrency = useSelector((state) => state.metamask.nativeCurrency)
const {
useNativeCurrencyAsPrimaryCurrency,
} = useSelector(preferencesSelector)
const showFiat = useSelector(getShouldShowFiat)
let currency, numberOfDecimals
if (!showFiat || (type === PRIMARY && useNativeCurrencyAsPrimaryCurrency) ||
(type === SECONDARY && !useNativeCurrencyAsPrimaryCurrency)) {
// Display ETH
currency = nativeCurrency || ETH
numberOfDecimals = opts.numberOfDecimals || opts.ethNumberOfDecimals || 6
} else if ((type === SECONDARY && useNativeCurrencyAsPrimaryCurrency) ||
(type === PRIMARY && !useNativeCurrencyAsPrimaryCurrency)) {
// Display Fiat
numberOfDecimals = opts.numberOfDecimals || opts.fiatNumberOfDecimals || 2
}
return { currency, numberOfDecimals }
}

View File

@ -104,7 +104,6 @@ export default class ChooseAccount extends Component {
value={balance}
style={{ color: '#6A737D' }}
suffix={nativeCurrency}
hideLabel
/>
</div>
</div>

View File

@ -285,6 +285,12 @@ export function preferencesSelector ({ metamask }) {
return metamask.preferences
}
export function getShouldShowFiat (state) {
const isMainNet = getIsMainnet(state)
const { showFiatInTestnets } = preferencesSelector(state)
return isMainNet || showFiatInTestnets
}
export function getAdvancedInlineGasShown (state) {
return Boolean(state.metamask.featureFlags.advancedInlineGas)
}

View File

@ -1088,6 +1088,13 @@
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.5.4":
version "7.9.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
@ -2653,6 +2660,14 @@
dependencies:
defer-to-connect "^1.0.1"
"@testing-library/react-hooks@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.2.1.tgz#19b6caa048ef15faa69d439c469033873ea01294"
integrity sha512-1OB6Ksvlk6BCJA1xpj8/WWz0XVd1qRcgqdaFAq+xeC6l61Ucj0P6QpA5u+Db/x9gU4DCX8ziR5b66Mlfg0M2RA==
dependencies:
"@babel/runtime" "^7.5.4"
"@types/testing-library__react-hooks" "^3.0.0"
"@types/babel__core@^7.1.0":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30"
@ -2839,6 +2854,13 @@
dependencies:
"@types/react" "*"
"@types/react-test-renderer@*":
version "16.9.2"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.2.tgz#e1c408831e8183e5ad748fdece02214a7c2ab6c5"
integrity sha512-4eJr1JFLIAlWhzDkBCkhrOIWOvOxcCAfQh+jiKg7l/nNZcCIL2MHl2dZhogIFKyHzedVWHaVP1Yydq/Ruu4agw==
dependencies:
"@types/react" "*"
"@types/react-textarea-autosize@^4.3.3":
version "4.3.5"
resolved "https://registry.yarnpkg.com/@types/react-textarea-autosize/-/react-textarea-autosize-4.3.5.tgz#6c4d2753fa1864c98c0b2b517f67bb1f6e4c46de"
@ -2872,6 +2894,14 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/testing-library__react-hooks@^3.0.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.2.0.tgz#52f3a109bef06080e3b1e3ae7ea1c014ce859897"
integrity sha512-dE8iMTuR5lzB+MqnxlzORlXzXyCL0EKfzH0w/lau20OpkHD37EaWjZDz0iNG8b71iEtxT4XKGmSKAGVEqk46mw==
dependencies:
"@types/react" "*"
"@types/react-test-renderer" "*"
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
@ -23991,6 +24021,11 @@ regenerator-runtime@^0.13.2:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
regenerator-runtime@^0.13.4:
version "0.13.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
regenerator-transform@^0.10.0:
version "0.10.1"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"