mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 17:33:23 +01:00
UX: Multichain: Added TokenList Component (#17859)
* added redesign storybook * updated token list * updated css * fixed lint error * updated the new token list component * fixed redesign folder error * reverted changes in settings.json * updated redesign to multichain * added feature flag * reverted settings.json * added detect token banner * added button componeny * fixed lint errors * removed settings * fixed lint errors * added stories for multichain * updated no token found string * updated lint error * updated padding values * updated padding values * updated tabs with role button * updated hover state * updated components with multichain * fixed lint errors * updated multichain import token link * updated a tag * updated fixes * updated onClick to handleClick * updated setShowDetectedTokens proptype * updated multichain tokenlist with item suffix * updated tests * updated tests * updated token list css * updated snapshot * updated text * reverted story * added story for multichain token list * updated story * updated tooltip * updated the new token list component * fixed redesign folder error * added feature flag * reverted unused setting change * removed token list * fixed lint error * updated status * updated tooltip * updated token-list-item changes * updated actionbutton click for detect token banner * updated snapshot * updated symbol * updated styles * updated eth decimal and token url * updated snapshot * updated scripts * updated snapshots
This commit is contained in:
parent
68f928c8a2
commit
fcfb8a8938
@ -12,21 +12,26 @@ import {
|
||||
getNativeCurrencyImage,
|
||||
getDetectedTokensInCurrentNetwork,
|
||||
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
|
||||
getTokenList,
|
||||
} from '../../../selectors';
|
||||
import { getNativeCurrency } from '../../../ducks/metamask/metamask';
|
||||
import { useCurrencyDisplay } from '../../../hooks/useCurrencyDisplay';
|
||||
import Typography from '../../ui/typography/typography';
|
||||
import Box from '../../ui/box/box';
|
||||
import {
|
||||
Color,
|
||||
TypographyVariant,
|
||||
FONT_WEIGHT,
|
||||
JustifyContent,
|
||||
TextVariant,
|
||||
TEXT_ALIGN,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
||||
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
|
||||
import DetectedToken from '../detected-token/detected-token';
|
||||
import {
|
||||
DetectedTokensBanner,
|
||||
MultichainTokenListItem,
|
||||
MultichainImportTokenLink,
|
||||
} from '../../multichain';
|
||||
import { Text } from '../../component-library';
|
||||
import DetectedTokensLink from './detetcted-tokens-link/detected-tokens-link';
|
||||
|
||||
const AssetList = ({ onClickAsset }) => {
|
||||
@ -69,20 +74,38 @@ const AssetList = ({ onClickAsset }) => {
|
||||
const istokenDetectionInactiveOnNonMainnetSupportedNetwork = useSelector(
|
||||
getIstokenDetectionInactiveOnNonMainnetSupportedNetwork,
|
||||
);
|
||||
|
||||
const tokenList = useSelector(getTokenList);
|
||||
const tokenData = Object.values(tokenList).find(
|
||||
(token) => token.symbol === primaryCurrencyProperties.suffix,
|
||||
);
|
||||
const title = tokenData?.name || primaryCurrencyProperties.suffix;
|
||||
return (
|
||||
<>
|
||||
<AssetListItem
|
||||
onClick={() => onClickAsset(nativeCurrency)}
|
||||
data-testid="wallet-balance"
|
||||
primary={
|
||||
primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value
|
||||
}
|
||||
tokenSymbol={primaryCurrencyProperties.suffix}
|
||||
secondary={showFiat ? secondaryCurrencyDisplay : undefined}
|
||||
tokenImage={balanceIsLoading ? null : primaryTokenImage}
|
||||
identiconBorder
|
||||
/>
|
||||
{process.env.MULTICHAIN ? (
|
||||
<MultichainTokenListItem
|
||||
onClick={() => onClickAsset(nativeCurrency)}
|
||||
title={title}
|
||||
primary={
|
||||
primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value
|
||||
}
|
||||
tokenSymbol={primaryCurrencyProperties.suffix}
|
||||
secondary={showFiat ? secondaryCurrencyDisplay : undefined}
|
||||
tokenImage={balanceIsLoading ? null : primaryTokenImage}
|
||||
/>
|
||||
) : (
|
||||
<AssetListItem
|
||||
onClick={() => onClickAsset(nativeCurrency)}
|
||||
data-testid="wallet-balance"
|
||||
primary={
|
||||
primaryCurrencyProperties.value ?? secondaryCurrencyProperties.value
|
||||
}
|
||||
tokenSymbol={primaryCurrencyProperties.suffix}
|
||||
secondary={showFiat ? secondaryCurrencyDisplay : undefined}
|
||||
tokenImage={balanceIsLoading ? null : primaryTokenImage}
|
||||
identiconBorder
|
||||
/>
|
||||
)}
|
||||
|
||||
<TokenList
|
||||
onTokenClick={(tokenAddress) => {
|
||||
onClickAsset(tokenAddress);
|
||||
@ -98,19 +121,36 @@ const AssetList = ({ onClickAsset }) => {
|
||||
/>
|
||||
{detectedTokens.length > 0 &&
|
||||
!istokenDetectionInactiveOnNonMainnetSupportedNetwork && (
|
||||
<DetectedTokensLink setShowDetectedTokens={setShowDetectedTokens} />
|
||||
<>
|
||||
{process.env.MULTICHAIN ? (
|
||||
<DetectedTokensBanner
|
||||
actionButtonOnClick={() => setShowDetectedTokens(true)}
|
||||
margin={4}
|
||||
/>
|
||||
) : (
|
||||
<DetectedTokensLink
|
||||
setShowDetectedTokens={setShowDetectedTokens}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Box marginTop={detectedTokens.length > 0 ? 0 : 4}>
|
||||
<Box justifyContent={JustifyContent.center}>
|
||||
<Typography
|
||||
color={Color.textAlternative}
|
||||
variant={TypographyVariant.H6}
|
||||
fontWeight={FONT_WEIGHT.NORMAL}
|
||||
>
|
||||
{t('missingToken')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<ImportTokenLink />
|
||||
{process.env.MULTICHAIN ? (
|
||||
<MultichainImportTokenLink margin={4} />
|
||||
) : (
|
||||
<>
|
||||
<Text
|
||||
color={Color.textAlternative}
|
||||
variant={TextVariant.bodySm}
|
||||
as="h6"
|
||||
textAlign={TEXT_ALIGN.CENTER}
|
||||
>
|
||||
{t('missingToken')}
|
||||
</Text>
|
||||
|
||||
<ImportTokenLink />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
{showDetectedTokens && (
|
||||
<DetectedToken setShowDetectedTokens={setShowDetectedTokens} />
|
||||
|
@ -3,9 +3,12 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import AssetListItem from '../asset-list-item';
|
||||
import { getSelectedAddress } from '../../../selectors';
|
||||
import { getSelectedAddress, getTokenList } from '../../../selectors';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
|
||||
import { MultichainTokenListItem } from '../../multichain';
|
||||
import { ButtonLink, Text } from '../../component-library';
|
||||
import { TextColor } from '../../../helpers/constants/design-system';
|
||||
|
||||
export default function TokenCell({
|
||||
address,
|
||||
@ -19,39 +22,58 @@ export default function TokenCell({
|
||||
}) {
|
||||
const userAddress = useSelector(getSelectedAddress);
|
||||
const t = useI18nContext();
|
||||
|
||||
const tokenList = useSelector(getTokenList);
|
||||
const tokenData = Object.values(tokenList).find(
|
||||
(token) => token.symbol === symbol,
|
||||
);
|
||||
const title = tokenData?.name || symbol;
|
||||
const tokenImage = tokenData?.iconUrl || image;
|
||||
const formattedFiat = useTokenFiatAmount(address, string, symbol);
|
||||
const warning = balanceError ? (
|
||||
<span>
|
||||
<Text as="span">
|
||||
{t('troubleTokenBalances')}
|
||||
<a
|
||||
<ButtonLink
|
||||
href={`https://ethplorer.io/address/${userAddress}`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
externalLink
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
style={{ color: 'var(--color-warning-default)' }}
|
||||
textProps={{
|
||||
color: TextColor.warningDefault,
|
||||
}}
|
||||
>
|
||||
{t('here')}
|
||||
</a>
|
||||
</span>
|
||||
</ButtonLink>
|
||||
</Text>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<AssetListItem
|
||||
className={classnames('token-cell', {
|
||||
'token-cell--outdated': Boolean(balanceError),
|
||||
})}
|
||||
iconClassName="token-cell__icon"
|
||||
onClick={onClick.bind(null, address)}
|
||||
tokenAddress={address}
|
||||
tokenSymbol={symbol}
|
||||
tokenDecimals={decimals}
|
||||
tokenImage={image}
|
||||
warning={warning}
|
||||
primary={`${string || 0}`}
|
||||
secondary={formattedFiat}
|
||||
isERC721={isERC721}
|
||||
/>
|
||||
<>
|
||||
{process.env.MULTICHAIN ? (
|
||||
<MultichainTokenListItem
|
||||
onClick={() => onClick(address)}
|
||||
tokenSymbol={symbol}
|
||||
tokenImage={tokenImage}
|
||||
primary={`${string || 0}`}
|
||||
secondary={formattedFiat}
|
||||
title={title}
|
||||
/>
|
||||
) : (
|
||||
<AssetListItem
|
||||
className={classnames('token-cell', {
|
||||
'token-cell--outdated': Boolean(balanceError),
|
||||
})}
|
||||
iconClassName="token-cell__icon"
|
||||
onClick={() => onClick(address)}
|
||||
tokenAddress={address}
|
||||
tokenSymbol={symbol}
|
||||
tokenDecimals={decimals}
|
||||
tokenImage={image}
|
||||
warning={warning}
|
||||
primary={`${string || 0}`}
|
||||
secondary={formattedFiat}
|
||||
isERC721={isERC721}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,27 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DetectedTokensBanner should render correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="box mm-banner-base mm-banner-alert mm-banner-alert--severity-info multichain-detected-token-banner box--padding-3 box--padding-left-2 box--display-flex box--gap-2 box--flex-direction-row box--background-color-primary-muted box--rounded-sm"
|
||||
data-testid="detected-token-banner"
|
||||
>
|
||||
<span
|
||||
class="box mm-icon mm-icon--size-lg box--display-inline-block box--flex-direction-row box--color-primary-default"
|
||||
style="mask-image: url('./images/icons/info.svg');"
|
||||
/>
|
||||
<div>
|
||||
<p
|
||||
class="box mm-text mm-text--body-md mm-text--color-text-default box--flex-direction-row"
|
||||
>
|
||||
3 new tokens found in this account
|
||||
</p>
|
||||
<button
|
||||
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md mm-text--color-primary-default box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-transparent"
|
||||
>
|
||||
Import tokens
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,60 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import { getDetectedTokensInCurrentNetwork } from '../../../selectors';
|
||||
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
||||
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
|
||||
import { BannerAlert } from '../../component-library';
|
||||
|
||||
export const DetectedTokensBanner = ({
|
||||
className,
|
||||
actionButtonOnClick,
|
||||
...props
|
||||
}) => {
|
||||
const t = useI18nContext();
|
||||
const trackEvent = useContext(MetaMetricsContext);
|
||||
|
||||
const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork);
|
||||
const detectedTokensDetails = detectedTokens.map(
|
||||
({ address, symbol }) => `${symbol} - ${address}`,
|
||||
);
|
||||
|
||||
const handleOnClick = () => {
|
||||
actionButtonOnClick();
|
||||
trackEvent({
|
||||
event: EVENT_NAMES.TOKEN_IMPORT_CLICKED,
|
||||
category: EVENT.CATEGORIES.WALLET,
|
||||
properties: {
|
||||
source: EVENT.SOURCE.TOKEN.DETECTED,
|
||||
tokens: detectedTokensDetails,
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<BannerAlert
|
||||
className={classNames('multichain-detected-token-banner', className)}
|
||||
actionButtonLabel={t('importTokensCamelCase')}
|
||||
actionButtonOnClick={handleOnClick}
|
||||
data-testid="detected-token-banner"
|
||||
{...props}
|
||||
>
|
||||
{detectedTokens.length === 1
|
||||
? t('numberOfNewTokensDetectedSingular')
|
||||
: t('numberOfNewTokensDetectedPlural', [detectedTokens.length])}
|
||||
</BannerAlert>
|
||||
);
|
||||
};
|
||||
|
||||
DetectedTokensBanner.propTypes = {
|
||||
/**
|
||||
* Handler to be passed to the DetectedTokenBanner component
|
||||
*/
|
||||
actionButtonOnClick: PropTypes.func.isRequired,
|
||||
/**
|
||||
* An additional className to the DetectedTokenBanner component
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { DetectedTokensBanner } from './detected-token-banner';
|
||||
|
||||
export default {
|
||||
title: 'Components/Multichain/DetectedTokensBanner',
|
||||
component: DetectedTokensBanner,
|
||||
argTypes: {
|
||||
actionButtonOnClick: { action: 'setShowDetectedTokens' },
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => <DetectedTokensBanner {...args} />;
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { renderWithProvider, screen, fireEvent } from '../../../../test/jest';
|
||||
import configureStore from '../../../store/store';
|
||||
import testData from '../../../../.storybook/test-data';
|
||||
|
||||
import { DetectedTokensBanner } from './detected-token-banner';
|
||||
|
||||
describe('DetectedTokensBanner', () => {
|
||||
let setShowDetectedTokensSpy;
|
||||
|
||||
const args = {};
|
||||
|
||||
beforeEach(() => {
|
||||
setShowDetectedTokensSpy = jest.fn();
|
||||
args.actionButtonOnClick = setShowDetectedTokensSpy;
|
||||
});
|
||||
|
||||
it('should render correctly', () => {
|
||||
const store = configureStore(testData);
|
||||
const { getByTestId, container } = renderWithProvider(
|
||||
<DetectedTokensBanner {...args} />,
|
||||
store,
|
||||
);
|
||||
|
||||
expect(getByTestId('detected-token-banner')).toBeDefined();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it('should render number of tokens detected link', () => {
|
||||
const store = configureStore(testData);
|
||||
renderWithProvider(<DetectedTokensBanner {...args} />, store);
|
||||
|
||||
expect(
|
||||
screen.getByText('3 new tokens found in this account'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Import tokens'));
|
||||
expect(setShowDetectedTokensSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
1
ui/components/multichain/detected-token-banner/index.js
Normal file
1
ui/components/multichain/detected-token-banner/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { DetectedTokensBanner } from './detected-token-banner';
|
@ -1,3 +1,6 @@
|
||||
export { AccountListItem } from './account-list-item';
|
||||
export { AccountListItemMenu } from './account-list-item-menu';
|
||||
export { AccountListMenu } from './account-list-menu';
|
||||
export { DetectedTokensBanner } from './detected-token-banner';
|
||||
export { MultichainImportTokenLink } from './multichain-import-token-link';
|
||||
export { MultichainTokenListItem } from './multichain-token-list-item';
|
||||
|
@ -1,2 +0,0 @@
|
||||
@import 'account-list-item/index';
|
||||
@import 'account-list-menu/index';
|
9
ui/components/multichain/multichain-components.scss
Normal file
9
ui/components/multichain/multichain-components.scss
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Please import your styles in order of atomicity.
|
||||
* The most atomic styles should be imported first.
|
||||
* This will help improve specificity and reduce the chance of
|
||||
* unintended overrides.
|
||||
**/
|
||||
@import 'account-list-item/index';
|
||||
@import 'account-list-menu/index';
|
||||
@import 'multichain-token-list-item/multichain-token-list-item';
|
@ -0,0 +1,75 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Import Token Link should match snapshot for goerli chainId 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="box multichain-import-token-link box--flex-direction-row"
|
||||
>
|
||||
<div
|
||||
class="box box--display-flex box--flex-direction-row box--align-items-center"
|
||||
>
|
||||
<button
|
||||
class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md mm-text--color-primary-default box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-transparent"
|
||||
data-testid="import-token-button"
|
||||
>
|
||||
<span
|
||||
class="box mm-icon mm-icon--size-sm box--margin-inline-end-1 box--display-inline-block box--flex-direction-row box--color-inherit"
|
||||
style="mask-image: url('./images/icons/add.svg');"
|
||||
/>
|
||||
Import tokens
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="box box--padding-top-4 box--padding-bottom-4 box--display-flex box--flex-direction-row box--align-items-center"
|
||||
>
|
||||
<button
|
||||
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md mm-text--color-primary-default box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-transparent"
|
||||
data-testid="refresh-list-button"
|
||||
>
|
||||
<span
|
||||
class="box mm-icon mm-icon--size-sm box--margin-inline-end-1 box--display-inline-block box--flex-direction-row box--color-inherit"
|
||||
style="mask-image: url('./images/icons/refresh.svg');"
|
||||
/>
|
||||
Refresh list
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Import Token Link should match snapshot for mainnet chainId 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="box multichain-import-token-link box--flex-direction-row"
|
||||
>
|
||||
<div
|
||||
class="box box--display-flex box--flex-direction-row box--align-items-center"
|
||||
>
|
||||
<button
|
||||
class="box mm-text mm-button-base mm-button-base--size-md mm-button-link mm-text--body-md mm-text--color-primary-default box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-transparent"
|
||||
data-testid="import-token-button"
|
||||
>
|
||||
<span
|
||||
class="box mm-icon mm-icon--size-sm box--margin-inline-end-1 box--display-inline-block box--flex-direction-row box--color-inherit"
|
||||
style="mask-image: url('./images/icons/add.svg');"
|
||||
/>
|
||||
Import tokens
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="box box--padding-top-4 box--padding-bottom-4 box--display-flex box--flex-direction-row box--align-items-center"
|
||||
>
|
||||
<button
|
||||
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md mm-text--color-primary-default box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-transparent"
|
||||
data-testid="refresh-list-button"
|
||||
>
|
||||
<span
|
||||
class="box mm-icon mm-icon--size-sm box--margin-inline-end-1 box--display-inline-block box--flex-direction-row box--color-inherit"
|
||||
style="mask-image: url('./images/icons/refresh.svg');"
|
||||
/>
|
||||
Refresh list
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1 @@
|
||||
export { MultichainImportTokenLink } from './multichain-import-token-link';
|
@ -0,0 +1,87 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import Box from '../../ui/box/box';
|
||||
import { ButtonLink, ICON_NAMES } from '../../component-library';
|
||||
import {
|
||||
AlignItems,
|
||||
DISPLAY,
|
||||
Size,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import { IMPORT_TOKEN_ROUTE } from '../../../helpers/constants/routes';
|
||||
import { detectNewTokens } from '../../../store/actions';
|
||||
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
||||
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
|
||||
import {
|
||||
getIsTokenDetectionSupported,
|
||||
getIsTokenDetectionInactiveOnMainnet,
|
||||
} from '../../../selectors';
|
||||
|
||||
export const MultichainImportTokenLink = ({ className, ...props }) => {
|
||||
const trackEvent = useContext(MetaMetricsContext);
|
||||
const t = useI18nContext();
|
||||
const history = useHistory();
|
||||
|
||||
const isTokenDetectionSupported = useSelector(getIsTokenDetectionSupported);
|
||||
const isTokenDetectionInactiveOnMainnet = useSelector(
|
||||
getIsTokenDetectionInactiveOnMainnet,
|
||||
);
|
||||
|
||||
const isTokenDetectionAvailable =
|
||||
isTokenDetectionSupported ||
|
||||
isTokenDetectionInactiveOnMainnet ||
|
||||
Boolean(process.env.IN_TEST);
|
||||
return (
|
||||
<Box
|
||||
className={classnames('multichain-import-token-link', className)}
|
||||
{...props}
|
||||
>
|
||||
<Box display={DISPLAY.FLEX} alignItems={AlignItems.center}>
|
||||
<ButtonLink
|
||||
size={Size.MD}
|
||||
data-testid="import-token-button"
|
||||
startIconName={ICON_NAMES.ADD}
|
||||
onClick={() => {
|
||||
history.push(IMPORT_TOKEN_ROUTE);
|
||||
trackEvent({
|
||||
event: EVENT_NAMES.TOKEN_IMPORT_BUTTON_CLICKED,
|
||||
category: EVENT.CATEGORIES.NAVIGATION,
|
||||
properties: {
|
||||
location: 'Home',
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isTokenDetectionAvailable
|
||||
? t('importTokensCamelCase')
|
||||
: t('importTokensCamelCase').charAt(0).toUpperCase() +
|
||||
t('importTokensCamelCase').slice(1)}
|
||||
</ButtonLink>
|
||||
</Box>
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
alignItems={AlignItems.center}
|
||||
paddingBottom={4}
|
||||
paddingTop={4}
|
||||
>
|
||||
<ButtonLink
|
||||
startIconName={ICON_NAMES.REFRESH}
|
||||
data-testid="refresh-list-button"
|
||||
onClick={() => detectNewTokens()}
|
||||
>
|
||||
{t('refreshList')}
|
||||
</ButtonLink>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
MultichainImportTokenLink.propTypes = {
|
||||
/**
|
||||
* An additional className to apply to the TokenList.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { MultichainImportTokenLink } from './multichain-import-token-link';
|
||||
|
||||
export default {
|
||||
title: 'Components/Multichain/MultichainImportTokenLink',
|
||||
component: MultichainImportTokenLink,
|
||||
};
|
||||
|
||||
export const DefaultStory = () => <MultichainImportTokenLink />;
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { detectNewTokens } from '../../../store/actions';
|
||||
import { renderWithProvider } from '../../../../test/lib/render-helpers';
|
||||
import { MultichainImportTokenLink } from './multichain-import-token-link';
|
||||
|
||||
const mockPushHistory = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const original = jest.requireActual('react-router-dom');
|
||||
return {
|
||||
...original,
|
||||
useLocation: jest.fn(() => ({ search: '' })),
|
||||
useHistory: () => ({
|
||||
push: mockPushHistory,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../store/actions.ts', () => ({
|
||||
detectNewTokens: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Import Token Link', () => {
|
||||
it('should match snapshot for goerli chainId', () => {
|
||||
const mockState = {
|
||||
metamask: {
|
||||
provider: {
|
||||
chainId: '0x5',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const store = configureMockStore()(mockState);
|
||||
|
||||
const { container } = renderWithProvider(
|
||||
<MultichainImportTokenLink />,
|
||||
store,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot for mainnet chainId', () => {
|
||||
const mockState = {
|
||||
metamask: {
|
||||
provider: {
|
||||
chainId: '0x1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const store = configureMockStore()(mockState);
|
||||
|
||||
const { container } = renderWithProvider(
|
||||
<MultichainImportTokenLink />,
|
||||
store,
|
||||
);
|
||||
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should detectNewTokens when clicking refresh', () => {
|
||||
const mockState = {
|
||||
metamask: {
|
||||
provider: {
|
||||
chainId: '0x5',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const store = configureMockStore()(mockState);
|
||||
|
||||
renderWithProvider(<MultichainImportTokenLink />, store);
|
||||
|
||||
const refreshList = screen.getByTestId('refresh-list-button');
|
||||
fireEvent.click(refreshList);
|
||||
|
||||
expect(detectNewTokens).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should push import token route', () => {
|
||||
const mockState = {
|
||||
metamask: {
|
||||
provider: {
|
||||
chainId: '0x5',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const store = configureMockStore()(mockState);
|
||||
|
||||
renderWithProvider(<MultichainImportTokenLink />, store);
|
||||
|
||||
const importToken = screen.getByTestId('import-token-button');
|
||||
fireEvent.click(importToken);
|
||||
|
||||
expect(mockPushHistory).toHaveBeenCalledWith('/import-token');
|
||||
});
|
||||
});
|
@ -0,0 +1,67 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MultichainTokenListItem should render correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="box multichain-token-list-item box--display-flex box--gap-4 box--flex-direction-column"
|
||||
data-testid="multichain-token-list-item"
|
||||
>
|
||||
<a
|
||||
class="box multichain-token-list-item__container-cell box--padding-4 box--display-flex box--flex-direction-row"
|
||||
data-testid="multichain-token-list-button"
|
||||
href="#"
|
||||
>
|
||||
<div
|
||||
class="box mm-badge-wrapper box--margin-right-3 box--display-inline-block box--flex-direction-row"
|
||||
>
|
||||
<div
|
||||
class="box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-token mm-avatar-token--with-halo mm-text--body-sm mm-text--text-transform-uppercase mm-text--color-text-default box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-background-alternative box--rounded-full box--border-color-border-default box--border-style-solid box--border-width-1"
|
||||
>
|
||||
?
|
||||
</div>
|
||||
<div
|
||||
class="box mm-badge-wrapper__badge-container mm-badge-wrapper__badge-container--circular-top-right box--flex-direction-row"
|
||||
>
|
||||
<div
|
||||
class="box mm-text mm-avatar-base mm-avatar-base--size-xs mm-avatar-network mm-text--body-xs mm-text--text-transform-uppercase mm-text--color-text-default box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--background-color-background-alternative box--rounded-full box--border-color-border-muted box--border-style-solid box--border-width-1"
|
||||
>
|
||||
<img
|
||||
alt="undefined logo"
|
||||
class="mm-avatar-network__network-image"
|
||||
src="./images/eth_logo.svg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box multichain-token-list-item__container-cell--text-container box--display-flex box--flex-direction-column box--width-full"
|
||||
style="flex-grow: 1; overflow: hidden;"
|
||||
>
|
||||
<div
|
||||
class="box box--display-flex box--gap-1 box--flex-direction-row box--justify-content-space-between"
|
||||
>
|
||||
<div
|
||||
class="box box--flex-direction-row box--width-1/3"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
class="box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--ellipsis mm-text--color-text-default box--flex-direction-row"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
class="box mm-text mm-text--body-md mm-text--font-weight-medium mm-text--text-align-end mm-text--color-text-default box--flex-direction-row"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="box mm-text mm-text--body-md mm-text--color-text-alternative box--flex-direction-row"
|
||||
>
|
||||
NaN
|
||||
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1 @@
|
||||
export { MultichainTokenListItem } from './multichain-token-list-item';
|
@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
BLOCK_SIZES,
|
||||
BorderColor,
|
||||
DISPLAY,
|
||||
FLEX_DIRECTION,
|
||||
FONT_WEIGHT,
|
||||
JustifyContent,
|
||||
Size,
|
||||
TextColor,
|
||||
TextVariant,
|
||||
TEXT_ALIGN,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import {
|
||||
AvatarNetwork,
|
||||
AvatarToken,
|
||||
BadgeWrapper,
|
||||
Text,
|
||||
} from '../../component-library';
|
||||
import Box from '../../ui/box/box';
|
||||
import { getNativeCurrencyImage } from '../../../selectors';
|
||||
import Tooltip from '../../ui/tooltip';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
|
||||
export const MultichainTokenListItem = ({
|
||||
className,
|
||||
onClick,
|
||||
tokenSymbol,
|
||||
tokenImage,
|
||||
primary,
|
||||
secondary,
|
||||
title,
|
||||
}) => {
|
||||
const t = useI18nContext();
|
||||
const primaryTokenImage = useSelector(getNativeCurrencyImage);
|
||||
const dataTheme = document.documentElement.getAttribute('data-theme');
|
||||
return (
|
||||
<Box
|
||||
className={classnames('multichain-token-list-item', className)}
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
data-testid="multichain-token-list-item"
|
||||
>
|
||||
<Box
|
||||
className="multichain-token-list-item__container-cell"
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.ROW}
|
||||
padding={4}
|
||||
as="a"
|
||||
data-testid="multichain-token-list-button"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
<BadgeWrapper
|
||||
badge={
|
||||
<AvatarNetwork
|
||||
size={Size.XS}
|
||||
name={tokenSymbol}
|
||||
src={primaryTokenImage}
|
||||
borderColor={
|
||||
primaryTokenImage
|
||||
? BorderColor.borderMuted
|
||||
: BorderColor.borderDefault
|
||||
}
|
||||
/>
|
||||
}
|
||||
marginRight={3}
|
||||
>
|
||||
<AvatarToken
|
||||
name={tokenSymbol}
|
||||
src={tokenImage}
|
||||
showHalo
|
||||
borderColor={
|
||||
tokenImage ? BorderColor.transparent : BorderColor.borderDefault
|
||||
}
|
||||
/>
|
||||
</BadgeWrapper>
|
||||
<Box
|
||||
className="multichain-token-list-item__container-cell--text-container"
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
width={BLOCK_SIZES.FULL}
|
||||
style={{ flexGrow: 1, overflow: 'hidden' }}
|
||||
>
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
justifyContent={JustifyContent.spaceBetween}
|
||||
gap={1}
|
||||
>
|
||||
<Box width={[BLOCK_SIZES.ONE_THIRD]}>
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
interactive
|
||||
html={title}
|
||||
disabled={title?.length < 12}
|
||||
tooltipInnerClassName="multichain-token-list-item__tooltip"
|
||||
theme={dataTheme === 'light' ? 'dark' : 'light'}
|
||||
>
|
||||
<Text
|
||||
fontWeight={FONT_WEIGHT.MEDIUM}
|
||||
variant={TextVariant.bodyMd}
|
||||
ellipsis
|
||||
>
|
||||
{title === 'ETH' ? t('networkNameEthereum') : title}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Text
|
||||
fontWeight={FONT_WEIGHT.MEDIUM}
|
||||
variant={TextVariant.bodyMd}
|
||||
width={[BLOCK_SIZES.TWO_THIRD]}
|
||||
textAlign={TEXT_ALIGN.END}
|
||||
>
|
||||
{secondary}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text color={TextColor.textAlternative}>
|
||||
{Number(primary).toFixed(3)} {tokenSymbol}{' '}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
MultichainTokenListItem.propTypes = {
|
||||
/**
|
||||
* An additional className to apply to the TokenList.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* The onClick handler to be passed to the MultichainTokenListItem component
|
||||
*/
|
||||
onClick: PropTypes.func,
|
||||
/**
|
||||
* tokenSymbol represents the symbol of the Token
|
||||
*/
|
||||
tokenSymbol: PropTypes.string,
|
||||
/**
|
||||
* title represents the name of the token and if name is not available then Symbol
|
||||
*/
|
||||
title: PropTypes.string,
|
||||
/**
|
||||
* tokenImage represnts the image of the token icon
|
||||
*/
|
||||
tokenImage: PropTypes.string,
|
||||
/**
|
||||
* primary represents the balance
|
||||
*/
|
||||
primary: PropTypes.string,
|
||||
/**
|
||||
* secondary represents the balance in dollars
|
||||
*/
|
||||
secondary: PropTypes.string,
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
.multichain-token-list-item {
|
||||
&__container-cell {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
background-color: var(--color-background-default-hover);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import testData from '../../../../.storybook/test-data';
|
||||
import configureStore from '../../../store/store';
|
||||
import { MultichainTokenListItem } from './multichain-token-list-item';
|
||||
|
||||
export default {
|
||||
title: 'Components/Multichain/MultichainTokenListItem',
|
||||
component: MultichainTokenListItem,
|
||||
argTypes: {
|
||||
tokenSymbol: {
|
||||
control: 'text',
|
||||
},
|
||||
tokenImage: {
|
||||
control: 'text',
|
||||
},
|
||||
primary: {
|
||||
control: 'text',
|
||||
},
|
||||
secondary: {
|
||||
control: 'text',
|
||||
},
|
||||
title: {
|
||||
control: 'text',
|
||||
},
|
||||
onClick: {
|
||||
action: 'onClick',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
secondary: '$9.80 USD',
|
||||
primary: '88.00687889',
|
||||
tokenImage: './images/eth_logo.svg',
|
||||
tokenSymbol: 'ETH',
|
||||
title: 'Ethereum',
|
||||
},
|
||||
};
|
||||
|
||||
const customNetworkData = {
|
||||
...testData,
|
||||
metamask: { ...testData.metamask, nativeCurrency: '' },
|
||||
};
|
||||
const customNetworkStore = configureStore(customNetworkData);
|
||||
|
||||
const Template = (args) => {
|
||||
return <MultichainTokenListItem {...args} />;
|
||||
};
|
||||
|
||||
export const DefaultStory = Template.bind({});
|
||||
|
||||
export const ChaosStory = (args) => (
|
||||
<div
|
||||
style={{ width: '336px', border: '1px solid var(--color-border-muted)' }}
|
||||
>
|
||||
<MultichainTokenListItem {...args} />
|
||||
</div>
|
||||
);
|
||||
ChaosStory.storyName = 'ChaosStory';
|
||||
|
||||
ChaosStory.args = {
|
||||
title: 'Really long, long name',
|
||||
secondary: '$94556756776.80 USD',
|
||||
primary: '34449765768526.00',
|
||||
};
|
||||
|
||||
export const NoImagesStory = Template.bind({});
|
||||
|
||||
NoImagesStory.decorators = [
|
||||
(Story) => (
|
||||
<Provider store={customNetworkStore}>
|
||||
<Story />
|
||||
</Provider>
|
||||
),
|
||||
];
|
||||
|
||||
NoImagesStory.args = {
|
||||
tokenImage: '',
|
||||
};
|
@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { renderWithProvider } from '../../../../test/lib/render-helpers';
|
||||
import { MultichainTokenListItem } from './multichain-token-list-item';
|
||||
|
||||
const state = {
|
||||
metamask: {
|
||||
provider: {
|
||||
ticker: 'ETH',
|
||||
nickname: '',
|
||||
chainId: '0x1',
|
||||
type: 'mainnet',
|
||||
},
|
||||
useTokenDetection: false,
|
||||
nativeCurrency: 'ETH',
|
||||
},
|
||||
};
|
||||
|
||||
describe('MultichainTokenListItem', () => {
|
||||
const props = {
|
||||
onClick: jest.fn(),
|
||||
};
|
||||
it('should render correctly', () => {
|
||||
const store = configureMockStore()(state);
|
||||
const { getByTestId, container } = renderWithProvider(
|
||||
<MultichainTokenListItem />,
|
||||
store,
|
||||
);
|
||||
expect(getByTestId('multichain-token-list-item')).toBeDefined();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with custom className', () => {
|
||||
const store = configureMockStore()(state);
|
||||
const { getByTestId } = renderWithProvider(
|
||||
<MultichainTokenListItem className="multichain-token-list-item-test" />,
|
||||
store,
|
||||
);
|
||||
expect(getByTestId('multichain-token-list-item')).toHaveClass(
|
||||
'multichain-token-list-item-test',
|
||||
);
|
||||
});
|
||||
|
||||
it('handles click action and fires onClick', () => {
|
||||
const store = configureMockStore()(state);
|
||||
const { queryByTestId } = renderWithProvider(
|
||||
<MultichainTokenListItem {...props} />,
|
||||
store,
|
||||
);
|
||||
|
||||
fireEvent.click(queryByTestId('multichain-token-list-button'));
|
||||
|
||||
expect(props.onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -26,19 +26,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
&[x-placement^=top] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
|
||||
&[x-placement^='top'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
|
||||
border-top-color: var(--color-background-default);
|
||||
}
|
||||
|
||||
&[x-placement^=right] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
|
||||
&[x-placement^='right'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
|
||||
border-right-color: var(--color-background-default);
|
||||
}
|
||||
|
||||
&[x-placement^=left] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
|
||||
&[x-placement^='left'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
|
||||
border-left-color: var(--color-background-default);
|
||||
}
|
||||
|
||||
&[x-placement^=bottom] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
|
||||
&[x-placement^='bottom'] .tippy-tooltip.tippy-tooltip--mm-custom-theme [x-arrow] {
|
||||
border-bottom-color: var(--color-background-default);
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,8 @@
|
||||
@import './base-styles.scss';
|
||||
@import '../components/component-library/component-library-components.scss';
|
||||
@import '../components/app/app-components';
|
||||
@import '../components/multichain/index.scss';
|
||||
@import '../components/ui/ui-components';
|
||||
@import '../components/multichain/multichain-components.scss';
|
||||
@import '../pages/pages';
|
||||
@import './errors.scss';
|
||||
@import './loading.scss';
|
||||
|
Loading…
Reference in New Issue
Block a user