Created "Token details" page (#13216)
* Created new screen/page "Token details" * Change color in scss * Modify elements to the latest requirements and added unit tests * Review requested changes * Condensing files into one component * Added unit tests for token details page * Added redirection when switching networks, added image for a token and update unit tests * Requested review changes * Modify index.scss regarding of the requested review * Delete data-testid's from Typography and token-details-page.js * Requested review changesfeature/default_network_editable
parent
3735a601d9
commit
2cd242252f
@ -0,0 +1 @@ |
||||
export { default } from './token-details-page'; |
@ -0,0 +1,49 @@ |
||||
.token-details { |
||||
&__title { |
||||
text-transform: capitalize; |
||||
} |
||||
|
||||
&__closeButton { |
||||
float: right; |
||||
width: 10px; |
||||
margin-top: -17px; |
||||
margin-inline-end: -8px; |
||||
|
||||
&::after { |
||||
font-size: 24px; |
||||
content: '\00D7'; |
||||
color: var(--black); |
||||
} |
||||
} |
||||
|
||||
&__token-value { |
||||
font-size: 32px; |
||||
} |
||||
|
||||
&__token-address { |
||||
width: 222px; |
||||
} |
||||
|
||||
&__copy-icon { |
||||
float: right; |
||||
margin-inline-start: 62px; |
||||
|
||||
@media screen and (min-width: $break-large) { |
||||
margin-inline-start: 112px; |
||||
} |
||||
} |
||||
|
||||
&__hide-token-button { |
||||
width: 319px; |
||||
height: 39px; |
||||
margin-top: 70px; |
||||
|
||||
@media screen and (min-width: $break-large) { |
||||
margin-inline-start: 20px; |
||||
} |
||||
} |
||||
|
||||
.btn--rounded.btn-primary { |
||||
background-color: #fff; |
||||
} |
||||
} |
@ -0,0 +1,200 @@ |
||||
import React, { useContext } from 'react'; |
||||
import { useDispatch, useSelector } from 'react-redux'; |
||||
import { Redirect, useHistory } from 'react-router-dom'; |
||||
import { getTokens } from '../../ducks/metamask/metamask'; |
||||
import { getSendAssetAddress } from '../../ducks/send'; |
||||
import { getUseTokenDetection, getTokenList } from '../../selectors'; |
||||
import { useCopyToClipboard } from '../../hooks/useCopyToClipboard'; |
||||
import { isEqualCaseInsensitive } from '../../helpers/utils/util'; |
||||
import Identicon from '../../components/ui/identicon/identicon.component'; |
||||
import { I18nContext } from '../../contexts/i18n'; |
||||
import { useTokenTracker } from '../../hooks/useTokenTracker'; |
||||
import { useTokenFiatAmount } from '../../hooks/useTokenFiatAmount'; |
||||
import { showModal } from '../../store/actions'; |
||||
import { NETWORK_TYPE_RPC } from '../../../shared/constants/network'; |
||||
import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../helpers/constants/routes'; |
||||
import Tooltip from '../../components/ui/tooltip'; |
||||
import Button from '../../components/ui/button'; |
||||
import CopyIcon from '../../components/ui/icon/copy-icon.component'; |
||||
import Box from '../../components/ui/box'; |
||||
import Typography from '../../components/ui/typography'; |
||||
import { |
||||
COLORS, |
||||
TYPOGRAPHY, |
||||
FONT_WEIGHT, |
||||
DISPLAY, |
||||
TEXT_ALIGN, |
||||
OVERFLOW_WRAP, |
||||
} from '../../helpers/constants/design-system'; |
||||
|
||||
export default function TokenDetailsPage() { |
||||
const dispatch = useDispatch(); |
||||
const history = useHistory(); |
||||
const t = useContext(I18nContext); |
||||
|
||||
const tokens = useSelector(getTokens); |
||||
const tokenList = useSelector(getTokenList); |
||||
const useTokenDetection = useSelector(getUseTokenDetection); |
||||
|
||||
const assetAddress = useSelector((state) => ({ |
||||
asset: getSendAssetAddress(state), |
||||
})); |
||||
|
||||
const { asset: tokenAddress } = assetAddress; |
||||
|
||||
const tokenMetadata = tokenList[tokenAddress]; |
||||
const fileName = tokenMetadata?.iconUrl; |
||||
const imagePath = useTokenDetection |
||||
? fileName |
||||
: `images/contract/${fileName}`; |
||||
|
||||
const token = tokens.find(({ address }) => |
||||
isEqualCaseInsensitive(address, tokenAddress), |
||||
); |
||||
|
||||
const { tokensWithBalances } = useTokenTracker([token]); |
||||
const tokenBalance = tokensWithBalances[0]?.string; |
||||
const tokenCurrencyBalance = useTokenFiatAmount( |
||||
token?.address, |
||||
tokenBalance, |
||||
token?.symbol, |
||||
); |
||||
|
||||
const currentNetwork = useSelector((state) => ({ |
||||
nickname: state.metamask.provider.nickname, |
||||
type: state.metamask.provider.type, |
||||
})); |
||||
|
||||
const { nickname: networkNickname, type: networkType } = currentNetwork; |
||||
|
||||
const [copied, handleCopy] = useCopyToClipboard(); |
||||
|
||||
if (!token) { |
||||
return <Redirect to={{ pathname: DEFAULT_ROUTE }} />; |
||||
} |
||||
return ( |
||||
<Box className="page-container token-details"> |
||||
<Box marginLeft={5} marginRight={6}> |
||||
<Typography |
||||
fontWeight={FONT_WEIGHT.BOLD} |
||||
margin={[4, 0, 0, 0]} |
||||
variant={TYPOGRAPHY.H6} |
||||
color={COLORS.BLACK} |
||||
className="token-details__title" |
||||
> |
||||
{t('tokenDetails')} |
||||
<Button |
||||
type="link" |
||||
onClick={() => history.push(`${ASSET_ROUTE}/${token.address}`)} |
||||
className="token-details__closeButton" |
||||
/> |
||||
</Typography> |
||||
<Box display={DISPLAY.FLEX} marginTop={4}> |
||||
<Typography |
||||
align={TEXT_ALIGN.CENTER} |
||||
fontWeight={FONT_WEIGHT.BOLD} |
||||
margin={[0, 5, 0, 0]} |
||||
variant={TYPOGRAPHY.H4} |
||||
color={COLORS.BLACK} |
||||
className="token-details__token-value" |
||||
> |
||||
{tokenBalance} |
||||
</Typography> |
||||
<Box marginTop={1}> |
||||
<Identicon |
||||
diameter={32} |
||||
address={token.address} |
||||
image={tokenMetadata ? imagePath : token.image} |
||||
/> |
||||
</Box> |
||||
</Box> |
||||
<Typography |
||||
margin={[4, 0, 0, 0]} |
||||
variant={TYPOGRAPHY.H7} |
||||
color={COLORS.UI4} |
||||
> |
||||
{tokenCurrencyBalance || ''} |
||||
</Typography> |
||||
<Typography |
||||
margin={[6, 0, 0, 0]} |
||||
variant={TYPOGRAPHY.H9} |
||||
color={COLORS.UI4} |
||||
fontWeight={FONT_WEIGHT.BOLD} |
||||
> |
||||
{t('tokenContractAddress')} |
||||
</Typography> |
||||
<Box display={DISPLAY.FLEX}> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H7} |
||||
margin={[2, 0, 0, 0]} |
||||
color={COLORS.BLACK} |
||||
overflowWrap={OVERFLOW_WRAP.BREAK_WORD} |
||||
className="token-details__token-address" |
||||
> |
||||
{token.address} |
||||
</Typography> |
||||
<Tooltip |
||||
position="bottom" |
||||
title={copied ? t('copiedExclamation') : t('copyToClipboard')} |
||||
containerClassName="token-details__copy-icon" |
||||
> |
||||
<Button |
||||
type="link" |
||||
className="token-details__copyIcon" |
||||
onClick={() => { |
||||
handleCopy(token.address); |
||||
}} |
||||
> |
||||
<CopyIcon size={11} color="#037DD6" /> |
||||
</Button> |
||||
</Tooltip> |
||||
</Box> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H9} |
||||
margin={[4, 0, 0, 0]} |
||||
color={COLORS.UI4} |
||||
fontWeight={FONT_WEIGHT.BOLD} |
||||
> |
||||
{t('tokenDecimalTitle')} |
||||
</Typography> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H7} |
||||
margin={[1, 0, 0, 0]} |
||||
color={COLORS.BLACK} |
||||
> |
||||
{token.decimals} |
||||
</Typography> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H9} |
||||
margin={[4, 0, 0, 0]} |
||||
color={COLORS.UI4} |
||||
fontWeight={FONT_WEIGHT.BOLD} |
||||
> |
||||
{t('network')} |
||||
</Typography> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H7} |
||||
margin={[1, 0, 0, 0]} |
||||
color={COLORS.BLACK} |
||||
> |
||||
{networkType === NETWORK_TYPE_RPC |
||||
? networkNickname ?? t('privateNetwork') |
||||
: t(networkType)} |
||||
</Typography> |
||||
<Button |
||||
type="primary" |
||||
className="token-details__hide-token-button" |
||||
onClick={() => { |
||||
dispatch( |
||||
showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token, history }), |
||||
); |
||||
}} |
||||
> |
||||
<Typography variant={TYPOGRAPHY.H6} color={COLORS.PRIMARY1}> |
||||
{t('hideToken')} |
||||
</Typography> |
||||
</Button> |
||||
</Box> |
||||
</Box> |
||||
); |
||||
} |
@ -0,0 +1,211 @@ |
||||
import React from 'react'; |
||||
import configureMockStore from 'redux-mock-store'; |
||||
import { fireEvent } from '@testing-library/react'; |
||||
import { renderWithProvider } from '../../../test/lib/render-helpers'; |
||||
import Identicon from '../../components/ui/identicon/identicon.component'; |
||||
import TokenDetailsPage from './token-details-page'; |
||||
|
||||
const state = { |
||||
metamask: { |
||||
selectedAddress: '0xAddress', |
||||
contractExchangeRates: { |
||||
'0xAnotherToken': 0.015, |
||||
}, |
||||
useTokenDetection: true, |
||||
tokenList: { |
||||
'0x6b175474e89094c44da98b954eedeac495271d0f': { |
||||
address: '0x6b175474e89094c44da98b954eedeac495271d0f', |
||||
symbol: 'META', |
||||
decimals: 18, |
||||
image: 'metamark.svg', |
||||
unlisted: false, |
||||
}, |
||||
'0xB8c77482e45F1F44dE1745F52C74426C631bDD52': { |
||||
address: '0xB8c77482e45F1F44dE1745F52C74426C631bDD52', |
||||
symbol: '0X', |
||||
decimals: 18, |
||||
image: '0x.svg', |
||||
unlisted: false, |
||||
}, |
||||
'0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': { |
||||
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', |
||||
symbol: 'AST', |
||||
decimals: 18, |
||||
image: 'ast.png', |
||||
unlisted: false, |
||||
}, |
||||
'0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2': { |
||||
address: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', |
||||
symbol: 'BAT', |
||||
decimals: 18, |
||||
image: 'BAT_icon.svg', |
||||
unlisted: false, |
||||
}, |
||||
'0xe83cccfabd4ed148903bf36d4283ee7c8b3494d1': { |
||||
address: '0xe83cccfabd4ed148903bf36d4283ee7c8b3494d1', |
||||
symbol: 'CVL', |
||||
decimals: 18, |
||||
image: 'CVL_token.svg', |
||||
unlisted: false, |
||||
}, |
||||
'0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': { |
||||
address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e', |
||||
symbol: 'GLA', |
||||
decimals: 18, |
||||
image: 'gladius.svg', |
||||
unlisted: false, |
||||
}, |
||||
'0x467Bccd9d29f223BcE8043b84E8C8B282827790F': { |
||||
address: '0x467Bccd9d29f223BcE8043b84E8C8B282827790F', |
||||
symbol: 'GNO', |
||||
decimals: 18, |
||||
image: 'gnosis.svg', |
||||
unlisted: false, |
||||
}, |
||||
'0xff20817765cb7f73d4bde2e66e067e58d11095c2': { |
||||
address: '0xff20817765cb7f73d4bde2e66e067e58d11095c2', |
||||
symbol: 'OMG', |
||||
decimals: 18, |
||||
image: 'omg.jpg', |
||||
unlisted: false, |
||||
}, |
||||
'0x8e870d67f660d95d5be530380d0ec0bd388289e1': { |
||||
address: '0x8e870d67f660d95d5be530380d0ec0bd388289e1', |
||||
symbol: 'WED', |
||||
decimals: 18, |
||||
image: 'wed.png', |
||||
unlisted: false, |
||||
}, |
||||
}, |
||||
provider: { |
||||
type: 'mainnet', |
||||
nickname: '', |
||||
}, |
||||
preferences: { |
||||
showFiatInTestnets: true, |
||||
}, |
||||
tokens: [ |
||||
{ |
||||
address: '0xaD6D458402F60fD3Bd25163575031ACDce07538A', |
||||
symbol: 'DAA', |
||||
decimals: 18, |
||||
image: null, |
||||
isERC721: false, |
||||
}, |
||||
{ |
||||
address: '0xaD6D458402F60fD3Bd25163575031ACDce07538U', |
||||
symbol: 'DAU', |
||||
decimals: 18, |
||||
image: null, |
||||
isERC721: false, |
||||
}, |
||||
], |
||||
}, |
||||
send: { |
||||
asset: { |
||||
balance: '0x0', |
||||
type: 'TOKEN', |
||||
details: { |
||||
address: '0xaD6D458402F60fD3Bd25163575031ACDce07538A', |
||||
decimals: 18, |
||||
image: null, |
||||
isERC721: false, |
||||
symbol: 'DAI', |
||||
}, |
||||
}, |
||||
}, |
||||
token: { |
||||
address: '0x6b175474e89094c44da98b954eedeac495271d0f', |
||||
decimals: 18, |
||||
image: './images/eth_logo.svg', |
||||
isERC721: false, |
||||
symbol: 'ETH', |
||||
}, |
||||
}; |
||||
|
||||
describe('TokenDetailsPage', () => { |
||||
it('should render title "Token details" in token details page', () => { |
||||
const store = configureMockStore()(state); |
||||
const { getByText } = renderWithProvider(<TokenDetailsPage />, store); |
||||
expect(getByText('Token details')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should close token details page when close button is clicked', () => { |
||||
const store = configureMockStore()(state); |
||||
const { container } = renderWithProvider(<TokenDetailsPage />, store); |
||||
const onCloseBtn = container.querySelector('.token-details__closeButton'); |
||||
fireEvent.click(onCloseBtn); |
||||
expect(onCloseBtn).toBeDefined(); |
||||
}); |
||||
|
||||
it('should render an icon image', () => { |
||||
const image = ( |
||||
<Identicon |
||||
diameter={32} |
||||
address={state.send.asset.details.address} |
||||
image={state.token.image} |
||||
/> |
||||
); |
||||
expect(image).toBeDefined(); |
||||
}); |
||||
|
||||
it('should render token contract address title in token details page', () => { |
||||
const store = configureMockStore()(state); |
||||
const { getByText } = renderWithProvider(<TokenDetailsPage />, store); |
||||
expect(getByText('Token Contract Address')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should render token contract address in token details page', () => { |
||||
const store = configureMockStore()(state); |
||||
const { getByText } = renderWithProvider(<TokenDetailsPage />, store); |
||||
expect(getByText(state.send.asset.details.address)).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should call copy button when click is simulated', () => { |
||||
const store = configureMockStore()(state); |
||||
const { container } = renderWithProvider(<TokenDetailsPage />, store); |
||||
const handleCopyBtn = container.querySelector('.token-details__copyIcon'); |
||||
fireEvent.click(handleCopyBtn); |
||||
expect(handleCopyBtn).toBeDefined(); |
||||
}); |
||||
|
||||
it('should render token decimal title in token details page', () => { |
||||
const store = configureMockStore()(state); |
||||
const { getByText } = renderWithProvider(<TokenDetailsPage />, store); |
||||
expect(getByText('Token Decimal:')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should render number of token decimals in token details page', () => { |
||||
const store = configureMockStore()(state); |
||||
const { getByText } = renderWithProvider(<TokenDetailsPage />, store); |
||||
expect(getByText('18')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should render current network title in token details page', () => { |
||||
const store = configureMockStore()(state); |
||||
const { getByText } = renderWithProvider(<TokenDetailsPage />, store); |
||||
expect(getByText('Network:')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should render current network in token details page', () => { |
||||
const store = configureMockStore()(state); |
||||
const { getByText } = renderWithProvider(<TokenDetailsPage />, store); |
||||
expect(getByText('Ethereum Mainnet')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should call hide token button when button is clicked in token details page', () => { |
||||
const store = configureMockStore()(state); |
||||
const { container } = renderWithProvider(<TokenDetailsPage />, store); |
||||
const hideTokenBtn = container.querySelector( |
||||
'.token-details__hide-token-button', |
||||
); |
||||
fireEvent.click(hideTokenBtn); |
||||
expect(hideTokenBtn).toBeDefined(); |
||||
}); |
||||
|
||||
it('should render label of hide token button in token details page', () => { |
||||
const store = configureMockStore()(state); |
||||
const { getByText } = renderWithProvider(<TokenDetailsPage />, store); |
||||
expect(getByText('Hide token')).toBeInTheDocument(); |
||||
}); |
||||
}); |
Loading…
Reference in new issue