mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 09:23:21 +01:00
Show Bridge button in TokenOverview component (#18630)
* Show Bridge button in TokenOverview component * Hide Swap button in token overview page when network is not supported
This commit is contained in:
parent
8e6f4b8831
commit
a144b75fe8
@ -8,3 +8,46 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [
|
||||
CHAIN_IDS.OPTIMISM,
|
||||
CHAIN_IDS.ARBITRUM,
|
||||
];
|
||||
|
||||
export const ALLOWED_BRIDGE_TOKEN_ADDRESSES = {
|
||||
[CHAIN_IDS.MAINNET]: [
|
||||
'0xdac17f958d2ee523a2206206994597c13d831ec7',
|
||||
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
'0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
'0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0',
|
||||
'0x8965349fb649a33a30cbfda057d8ec2c48abe2a2',
|
||||
],
|
||||
[CHAIN_IDS.BSC]: [
|
||||
'0x55d398326f99059ff775485246999027b3197955',
|
||||
'0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d',
|
||||
'0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3',
|
||||
'0x2170ed0880ac9a755fd29b2688956bd959f933f8',
|
||||
'0xcc42724c6683b7e57334c4e856f4c9965ed682bd',
|
||||
'0x1ce0c2827e2ef14d5c4f29a091d735a204794041',
|
||||
],
|
||||
[CHAIN_IDS.POLYGON]: [
|
||||
'0xc2132d05d31c914a87c6611c10748aeb04b58e8f',
|
||||
'0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
|
||||
'0x8f3cf7ad23cd3cadbd9735aff958023239c6a063',
|
||||
'0x7ceb23fd6bc0add59e62ac25578270cff1b9f619',
|
||||
'0x2c89bbc92bd86f8075d1decc58c7f4e0107f286b',
|
||||
],
|
||||
[CHAIN_IDS.AVALANCHE]: [
|
||||
'0xc7198437980c041c805a1edcba50c1ce5db95118',
|
||||
'0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7',
|
||||
'0xa7d7079b0fead91f3e65f86e8915cb59c1a4c664',
|
||||
'0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e',
|
||||
'0xd586e7f844cea2f87f50152665bcbc2c279d8d70',
|
||||
'0x49d5c2bdffac6ce2bfdb6640f4f80f226bc10bab',
|
||||
],
|
||||
[CHAIN_IDS.OPTIMISM]: [
|
||||
'0x94b008aa00579c1307b0ef2c499ad98a8ce58e58',
|
||||
'0x7f5c764cbc14f9669b88837ca1490cca17c31607',
|
||||
'0xda10009cbd5d07dd0cecc66161fc93d7c9000da1',
|
||||
],
|
||||
[CHAIN_IDS.ARBITRUM]: [
|
||||
'0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9',
|
||||
'0xff970a61a04b1ca14834a43f5de4533ebddb5cc8',
|
||||
'0xda10009cbd5d07dd0cecc66161fc93d7c9000da1',
|
||||
],
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import classnames from 'classnames';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import Identicon from '../../ui/identicon';
|
||||
import { I18nContext } from '../../../contexts/i18n';
|
||||
@ -52,6 +52,7 @@ const EthOverview = ({ className }) => {
|
||||
const t = useContext(I18nContext);
|
||||
const trackEvent = useContext(MetaMetricsContext);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const keyring = useSelector(getCurrentKeyring);
|
||||
const usingHardwareWallet = isHardwareKeyring(keyring?.type);
|
||||
const balanceIsCached = useSelector(isBalanceCached);
|
||||
@ -246,7 +247,9 @@ const EthOverview = ({ className }) => {
|
||||
const portfolioUrl = process.env.PORTFOLIO_URL;
|
||||
const bridgeUrl = `${portfolioUrl}/bridge`;
|
||||
global.platform.openTab({
|
||||
url: `${bridgeUrl}?metamaskEntry=ext`,
|
||||
url: `${bridgeUrl}?metamaskEntry=ext_bridge_button${
|
||||
location.pathname.includes('asset') ? '&token=native' : ''
|
||||
}`,
|
||||
});
|
||||
trackEvent({
|
||||
category: MetaMetricsEventCategory.Navigation,
|
||||
|
@ -181,7 +181,9 @@ describe('EthOverview', () => {
|
||||
|
||||
await waitFor(() =>
|
||||
expect(openTabSpy).toHaveBeenCalledWith({
|
||||
url: expect.stringContaining(`/bridge?metamaskEntry=ext`),
|
||||
url: expect.stringContaining(
|
||||
'/bridge?metamaskEntry=ext_bridge_button',
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
@ -114,7 +114,14 @@
|
||||
align-items: center;
|
||||
margin: 16px 0;
|
||||
padding: 0 16px;
|
||||
max-width: 100%;
|
||||
max-width: 326px;
|
||||
}
|
||||
|
||||
&__primary-container {
|
||||
display: flex;
|
||||
max-width: inherit;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__primary-balance {
|
||||
@ -131,6 +138,11 @@
|
||||
color: var(--color-text-alternative);
|
||||
}
|
||||
|
||||
&__portfolio-button {
|
||||
height: inherit;
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
&__button:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import Identicon from '../../ui/identicon';
|
||||
import Tooltip from '../../ui/tooltip';
|
||||
import CurrencyDisplay from '../../ui/currency-display';
|
||||
import { I18nContext } from '../../../contexts/i18n';
|
||||
import { isHardwareKeyring } from '../../../helpers/utils/hardware';
|
||||
@ -20,6 +19,7 @@ import {
|
||||
getCurrentKeyring,
|
||||
getIsSwapsChain,
|
||||
getIsBuyableChain,
|
||||
getIsBridgeToken,
|
||||
} from '../../../selectors';
|
||||
|
||||
import IconButton from '../../ui/icon-button';
|
||||
@ -35,8 +35,10 @@ import {
|
||||
import { AssetType } from '../../../../shared/constants/transaction';
|
||||
import useRamps from '../../../hooks/experiences/useRamps';
|
||||
|
||||
import { Icon, IconName } from '../../component-library';
|
||||
import { ButtonIcon, Icon, IconName } from '../../component-library';
|
||||
import { IconColor } from '../../../helpers/constants/design-system';
|
||||
|
||||
import { BUTTON_ICON_SIZES } from '../../component-library/button-icon/deprecated';
|
||||
import WalletOverview from './wallet-overview';
|
||||
|
||||
const TokenOverview = ({ className, token }) => {
|
||||
@ -55,7 +57,7 @@ const TokenOverview = ({ className, token }) => {
|
||||
token.symbol,
|
||||
);
|
||||
const isSwapsChain = useSelector(getIsSwapsChain);
|
||||
|
||||
const isBridgeToken = useSelector(getIsBridgeToken(token.address));
|
||||
const isBuyableChain = useSelector(getIsBuyableChain);
|
||||
|
||||
const { openBuyCryptoInPdapp } = useRamps();
|
||||
@ -75,11 +77,42 @@ const TokenOverview = ({ className, token }) => {
|
||||
<WalletOverview
|
||||
balance={
|
||||
<div className="token-overview__balance">
|
||||
<CurrencyDisplay
|
||||
className="token-overview__primary-balance"
|
||||
displayValue={balanceToRender}
|
||||
suffix={token.symbol}
|
||||
/>
|
||||
<div className="token-overview__primary-container">
|
||||
<CurrencyDisplay
|
||||
style={{ display: 'contents' }}
|
||||
className="token-overview__primary-balance"
|
||||
displayValue={balanceToRender}
|
||||
suffix={token.symbol}
|
||||
/>
|
||||
<ButtonIcon
|
||||
className="token-overview__portfolio-button"
|
||||
data-testid="home__portfolio-site"
|
||||
color={IconColor.primaryDefault}
|
||||
iconName={IconName.Diagram}
|
||||
ariaLabel={t('portfolio')}
|
||||
size={BUTTON_ICON_SIZES.LG}
|
||||
onClick={() => {
|
||||
const portfolioUrl = process.env.PORTFOLIO_URL;
|
||||
global.platform.openTab({
|
||||
url: `${portfolioUrl}?metamaskEntry=ext`,
|
||||
});
|
||||
trackEvent(
|
||||
{
|
||||
category: MetaMetricsEventCategory.Home,
|
||||
event: MetaMetricsEventName.PortfolioLinkClicked,
|
||||
properties: {
|
||||
url: portfolioUrl,
|
||||
},
|
||||
},
|
||||
{
|
||||
contextPropsIntoEventProperties: [
|
||||
MetaMetricsContextProp.PageTitle,
|
||||
],
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{formattedFiatBalance ? (
|
||||
<CurrencyDisplay
|
||||
className="token-overview__secondary-balance"
|
||||
@ -145,17 +178,16 @@ const TokenOverview = ({ className, token }) => {
|
||||
data-testid="eth-overview-send"
|
||||
disabled={token.isERC721}
|
||||
/>
|
||||
<IconButton
|
||||
className="token-overview__button"
|
||||
disabled={!isSwapsChain}
|
||||
Icon={
|
||||
<Icon
|
||||
name={IconName.SwapHorizontal}
|
||||
color={IconColor.primaryInverse}
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSwapsChain) {
|
||||
{isSwapsChain && (
|
||||
<IconButton
|
||||
className="token-overview__button"
|
||||
Icon={
|
||||
<Icon
|
||||
name={IconName.SwapHorizontal}
|
||||
color={IconColor.primaryInverse}
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
event: MetaMetricsEventName.NavSwapButtonClicked,
|
||||
category: MetaMetricsEventCategory.Swaps,
|
||||
@ -179,51 +211,38 @@ const TokenOverview = ({ className, token }) => {
|
||||
} else {
|
||||
history.push(BUILD_QUOTE_ROUTE);
|
||||
}
|
||||
}}
|
||||
label={t('swap')}
|
||||
tooltipRender={null}
|
||||
/>
|
||||
)}
|
||||
{isBridgeToken && (
|
||||
<IconButton
|
||||
className="token-overview__button"
|
||||
data-testid="token-overview-bridge"
|
||||
Icon={
|
||||
<Icon name={IconName.Bridge} color={IconColor.primaryInverse} />
|
||||
}
|
||||
}}
|
||||
label={t('swap')}
|
||||
tooltipRender={
|
||||
isSwapsChain
|
||||
? null
|
||||
: (contents) => (
|
||||
<Tooltip
|
||||
title={t('currentlyUnavailable')}
|
||||
position="bottom"
|
||||
disabled={isSwapsChain}
|
||||
>
|
||||
{contents}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
className="eth-overview__button"
|
||||
Icon={
|
||||
<Icon name={IconName.Diagram} color={IconColor.primaryInverse} />
|
||||
}
|
||||
label={t('portfolio')}
|
||||
data-testid="home__portfolio-site"
|
||||
onClick={() => {
|
||||
const portfolioUrl = process.env.PORTFOLIO_URL;
|
||||
global.platform.openTab({
|
||||
url: `${portfolioUrl}?metamaskEntry=ext`,
|
||||
});
|
||||
trackEvent(
|
||||
{
|
||||
category: MetaMetricsEventCategory.Home,
|
||||
event: MetaMetricsEventName.PortfolioLinkClicked,
|
||||
label={t('bridge')}
|
||||
onClick={() => {
|
||||
const portfolioUrl = process.env.PORTFOLIO_URL;
|
||||
|
||||
const bridgeUrl = `${portfolioUrl}/bridge`;
|
||||
global.platform.openTab({
|
||||
url: `${bridgeUrl}?metamaskEntry=ext_bridge_button&token=${token.address}`,
|
||||
});
|
||||
trackEvent({
|
||||
category: MetaMetricsEventCategory.Navigation,
|
||||
event: MetaMetricsEventName.BridgeLinkClicked,
|
||||
properties: {
|
||||
url: portfolioUrl,
|
||||
location: 'Token Overview',
|
||||
text: 'Bridge',
|
||||
},
|
||||
},
|
||||
{
|
||||
contextPropsIntoEventProperties: [
|
||||
MetaMetricsContextProp.PageTitle,
|
||||
],
|
||||
},
|
||||
);
|
||||
}}
|
||||
/>
|
||||
});
|
||||
}}
|
||||
tooltipRender={null}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
className={className}
|
||||
|
@ -23,6 +23,7 @@ jest.mock('../../../../shared/constants/network', () => ({
|
||||
},
|
||||
},
|
||||
}));
|
||||
let openTabSpy;
|
||||
|
||||
describe('TokenOverview', () => {
|
||||
const mockStore = {
|
||||
@ -68,6 +69,11 @@ describe('TokenOverview', () => {
|
||||
openTab: jest.fn(),
|
||||
},
|
||||
});
|
||||
openTabSpy = jest.spyOn(global.platform, 'openTab');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
openTabSpy.mockClear();
|
||||
});
|
||||
|
||||
const token = {
|
||||
@ -209,8 +215,6 @@ describe('TokenOverview', () => {
|
||||
mockedStoreWithBuyableChainId,
|
||||
);
|
||||
|
||||
const openTabSpy = jest.spyOn(global.platform, 'openTab');
|
||||
|
||||
const { queryByTestId } = renderWithProvider(
|
||||
<TokenOverview token={token} />,
|
||||
mockedStore,
|
||||
@ -228,5 +232,137 @@ describe('TokenOverview', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should always show the Portfolio button', () => {
|
||||
const mockToken = {
|
||||
name: 'test',
|
||||
isERC721: false,
|
||||
address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f619',
|
||||
symbol: 'test',
|
||||
};
|
||||
const { queryByTestId } = renderWithProvider(
|
||||
<TokenOverview token={mockToken} />,
|
||||
store,
|
||||
);
|
||||
const portfolioButton = queryByTestId('home__portfolio-site');
|
||||
expect(portfolioButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open the Portfolio URI when clicking on Portfolio button', async () => {
|
||||
const mockToken = {
|
||||
name: 'test',
|
||||
isERC721: false,
|
||||
address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f619',
|
||||
symbol: 'test',
|
||||
};
|
||||
const { queryByTestId } = renderWithProvider(
|
||||
<TokenOverview token={mockToken} />,
|
||||
store,
|
||||
);
|
||||
|
||||
const portfolioButton = queryByTestId('home__portfolio-site');
|
||||
|
||||
expect(portfolioButton).toBeInTheDocument();
|
||||
expect(portfolioButton).not.toBeDisabled();
|
||||
|
||||
fireEvent.click(portfolioButton);
|
||||
expect(openTabSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(openTabSpy).toHaveBeenCalledWith({
|
||||
url: expect.stringContaining(`?metamaskEntry=ext`),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should show the Bridge button if chain id and token are supported', async () => {
|
||||
const mockToken = {
|
||||
name: 'test',
|
||||
isERC721: false,
|
||||
address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f619',
|
||||
symbol: 'test',
|
||||
};
|
||||
|
||||
const mockedStoreWithBridgeableChainId = {
|
||||
metamask: {
|
||||
...mockStore.metamask,
|
||||
provider: { type: 'test', chainId: CHAIN_IDS.POLYGON },
|
||||
},
|
||||
};
|
||||
const mockedStore = configureMockStore([thunk])(
|
||||
mockedStoreWithBridgeableChainId,
|
||||
);
|
||||
|
||||
const { queryByTestId } = renderWithProvider(
|
||||
<TokenOverview token={mockToken} />,
|
||||
mockedStore,
|
||||
);
|
||||
const bridgeButton = queryByTestId('token-overview-bridge');
|
||||
expect(bridgeButton).toBeInTheDocument();
|
||||
expect(bridgeButton).not.toBeDisabled();
|
||||
|
||||
fireEvent.click(bridgeButton);
|
||||
expect(openTabSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(openTabSpy).toHaveBeenCalledWith({
|
||||
url: expect.stringContaining(
|
||||
'/bridge?metamaskEntry=ext_bridge_button&token=0x7ceb23fd6bc0add59e62ac25578270cff1B9f619',
|
||||
),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not show the Bridge button if chain id is not supported', async () => {
|
||||
const mockToken = {
|
||||
name: 'test',
|
||||
isERC721: false,
|
||||
address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f619',
|
||||
symbol: 'test',
|
||||
};
|
||||
|
||||
const mockedStoreWithBridgeableChainId = {
|
||||
metamask: {
|
||||
...mockStore.metamask,
|
||||
provider: { type: 'test', chainId: CHAIN_IDS.FANTOM },
|
||||
},
|
||||
};
|
||||
const mockedStore = configureMockStore([thunk])(
|
||||
mockedStoreWithBridgeableChainId,
|
||||
);
|
||||
|
||||
const { queryByTestId } = renderWithProvider(
|
||||
<TokenOverview token={mockToken} />,
|
||||
mockedStore,
|
||||
);
|
||||
const bridgeButton = queryByTestId('token-overview-bridge');
|
||||
expect(bridgeButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show the Bridge button if token is not supported', async () => {
|
||||
const mockToken = {
|
||||
name: 'test',
|
||||
isERC721: false,
|
||||
address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f620',
|
||||
symbol: 'test',
|
||||
};
|
||||
|
||||
const mockedStoreWithBridgeableChainId = {
|
||||
metamask: {
|
||||
...mockStore.metamask,
|
||||
provider: { type: 'test', chainId: CHAIN_IDS.POLYGON },
|
||||
},
|
||||
};
|
||||
const mockedStore = configureMockStore([thunk])(
|
||||
mockedStoreWithBridgeableChainId,
|
||||
);
|
||||
|
||||
const { queryByTestId } = renderWithProvider(
|
||||
<TokenOverview token={mockToken} />,
|
||||
mockedStore,
|
||||
);
|
||||
const bridgeButton = queryByTestId('token-overview-bridge');
|
||||
expect(bridgeButton).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -46,7 +46,10 @@ import {
|
||||
ALLOWED_DEV_SWAPS_CHAIN_IDS,
|
||||
} from '../../shared/constants/swaps';
|
||||
|
||||
import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../shared/constants/bridge';
|
||||
import {
|
||||
ALLOWED_BRIDGE_CHAIN_IDS,
|
||||
ALLOWED_BRIDGE_TOKEN_ADDRESSES,
|
||||
} from '../../shared/constants/bridge';
|
||||
|
||||
import {
|
||||
shortenAddress,
|
||||
@ -747,6 +750,15 @@ export function getIsBridgeChain(state) {
|
||||
return ALLOWED_BRIDGE_CHAIN_IDS.includes(chainId);
|
||||
}
|
||||
|
||||
export const getIsBridgeToken = (tokenAddress) => (state) => {
|
||||
const chainId = getCurrentChainId(state);
|
||||
const isBridgeChain = getIsBridgeChain(state);
|
||||
return (
|
||||
isBridgeChain &&
|
||||
ALLOWED_BRIDGE_TOKEN_ADDRESSES[chainId].includes(tokenAddress.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
export function getIsBuyableChain(state) {
|
||||
const chainId = getCurrentChainId(state);
|
||||
return Object.keys(BUYABLE_CHAINS_MAP).includes(chainId);
|
||||
|
@ -450,4 +450,23 @@ describe('Selectors', () => {
|
||||
const isFantomSupported = selectors.getIsBridgeChain(mockState);
|
||||
expect(isFantomSupported).toBeFalsy();
|
||||
});
|
||||
|
||||
it('#getIsBridgeToken', () => {
|
||||
mockState.metamask.provider.chainId = '0xa';
|
||||
const isOptimismTokenSupported = selectors.getIsBridgeToken(
|
||||
'0x94B008aa00579c1307b0ef2c499ad98a8ce58e58',
|
||||
)(mockState);
|
||||
expect(isOptimismTokenSupported).toBeTruthy();
|
||||
|
||||
const isOptimismUnknownTokenSupported = selectors.getIsBridgeToken(
|
||||
'0x94B008aa00579c1307b0ef2c499ad98a8ce58e60',
|
||||
)(mockState);
|
||||
expect(isOptimismUnknownTokenSupported).toBeFalsy();
|
||||
|
||||
mockState.metamask.provider.chainId = '0xfa';
|
||||
const isFantomTokenSupported = selectors.getIsBridgeToken(
|
||||
'0x94B008aa00579c1307b0ef2c499ad98a8ce58e58',
|
||||
)(mockState);
|
||||
expect(isFantomTokenSupported).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user