mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 17:33:23 +01:00
ConfirmAddSuggestToken: replace mapDispatchToProps & mapStateToProps; Add Tests (#13526)
* ConfirmAddSuggestedToken: component.js -> .js * replace mapStateToProps w/ useSelector ConfirmAddSuggestedToken * replace mapDispatchToProps w/ useDispatch ConfirmAddSuggestedToken * ConfirmAddSuggestedToken: useHistory * ConfirmAddSuggestedToken: revert scss reuse ConfirmImportToken styles for now * ConfirmAddSuggestToken: add tests * ConfirmAddSuggestedToken: rm unused controls * ConfirmAddSuggestedToken: rm snapshot * use `metamark.svg` instead of `metamask.svg` * ConfirmAddSuggestedToken: update useSelectors * Revert "ConfirmAddSuggestedToken: revert scss" This reverts commit 07aed4576caaf247cb38ad481d79a339ba3d3947. * ConfirmAddSuggestedToken: === undefined w/ strict mode * ConfirmAddSuggestedToken: use useNewMetricEvent * ConfirmAddSuggestedToken: run useEffect once * ConfirmAddSuggestedToken: rm unused async * ConfirmAddSuggestedToken: rm mock redux store * ConfirmAddSuggestedToken: duplicate ConfirmImportToken styles * ConfirmAddSuggestedToken: update metrics logic * ConfirmAddSuggestedToken: metricEvent-> trackEvent * ConfirmAddSuggestedToken:clean useEffect w/ new fn - PR suggestion: https://github.com/MetaMask/metamask-extension/pull/13526#discussion_r818007393 * ConfirmAddSuggestedToken: rm unused .token-balance styles
This commit is contained in:
parent
bcf59ab09b
commit
fa15b32fab
@ -1,32 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { rejectWatchAsset, acceptWatchAsset } from '../../store/actions';
|
||||
import { getMostRecentOverviewPage } from '../../ducks/history/history';
|
||||
import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const {
|
||||
metamask: { suggestedAssets, tokens },
|
||||
} = state;
|
||||
|
||||
return {
|
||||
mostRecentOverviewPage: getMostRecentOverviewPage(state),
|
||||
suggestedAssets,
|
||||
tokens,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
rejectWatchAsset: (suggestedAssetID) =>
|
||||
dispatch(rejectWatchAsset(suggestedAssetID)),
|
||||
acceptWatchAsset: (suggestedAssetID) =>
|
||||
dispatch(acceptWatchAsset(suggestedAssetID)),
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(ConfirmAddSuggestedToken);
|
@ -1,16 +1,21 @@
|
||||
import React, { useContext, useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import ActionableMessage from '../../components/ui/actionable-message/actionable-message';
|
||||
import Button from '../../components/ui/button';
|
||||
import Identicon from '../../components/ui/identicon';
|
||||
import TokenBalance from '../../components/ui/token-balance';
|
||||
import { I18nContext } from '../../contexts/i18n';
|
||||
import { MetaMetricsContext } from '../../contexts/metametrics';
|
||||
import { MetaMetricsContext as NewMetaMetricsContext } from '../../contexts/metametrics.new';
|
||||
import { getMostRecentOverviewPage } from '../../ducks/history/history';
|
||||
import { getTokens } from '../../ducks/metamask/metamask';
|
||||
import ZENDESK_URLS from '../../helpers/constants/zendesk-url';
|
||||
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
|
||||
import { getSuggestedAssets } from '../../selectors';
|
||||
import { rejectWatchAsset, acceptWatchAsset } from '../../store/actions';
|
||||
|
||||
function getTokenName(name, symbol) {
|
||||
return typeof name === 'undefined' ? symbol : `${name} (${symbol})`;
|
||||
return name === undefined ? symbol : `${name} (${symbol})`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,32 +56,16 @@ function hasDuplicateSymbolAndDiffAddress(suggestedAssets, tokens) {
|
||||
return Boolean(duplicate);
|
||||
}
|
||||
|
||||
const ConfirmAddSuggestedToken = (props) => {
|
||||
const {
|
||||
acceptWatchAsset,
|
||||
history,
|
||||
mostRecentOverviewPage,
|
||||
rejectWatchAsset,
|
||||
suggestedAssets,
|
||||
tokens,
|
||||
} = props;
|
||||
|
||||
const metricsEvent = useContext(MetaMetricsContext);
|
||||
const ConfirmAddSuggestedToken = () => {
|
||||
const t = useContext(I18nContext);
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const tokenAddedEvent = (asset) => {
|
||||
metricsEvent({
|
||||
event: 'Token Added',
|
||||
category: 'Wallet',
|
||||
sensitiveProperties: {
|
||||
token_symbol: asset.symbol,
|
||||
token_contract_address: asset.address,
|
||||
token_decimal_precision: asset.decimals,
|
||||
unlisted: asset.unlisted,
|
||||
source: 'dapp',
|
||||
},
|
||||
});
|
||||
};
|
||||
const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
|
||||
const suggestedAssets = useSelector(getSuggestedAssets);
|
||||
const tokens = useSelector(getTokens);
|
||||
|
||||
const trackEvent = useContext(NewMetaMetricsContext);
|
||||
|
||||
const knownTokenActionableMessage = useMemo(() => {
|
||||
return (
|
||||
@ -117,11 +106,38 @@ const ConfirmAddSuggestedToken = (props) => {
|
||||
);
|
||||
}, [suggestedAssets, tokens, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAddTokensClick = useCallback(async () => {
|
||||
await Promise.all(
|
||||
suggestedAssets.map(async ({ asset, id }) => {
|
||||
await dispatch(acceptWatchAsset(id));
|
||||
|
||||
trackEvent({
|
||||
event: 'Token Added',
|
||||
category: 'Wallet',
|
||||
sensitiveProperties: {
|
||||
token_symbol: asset.symbol,
|
||||
token_contract_address: asset.address,
|
||||
token_decimal_precision: asset.decimals,
|
||||
unlisted: asset.unlisted,
|
||||
source: 'dapp',
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
history.push(mostRecentOverviewPage);
|
||||
}, [dispatch, history, trackEvent, mostRecentOverviewPage, suggestedAssets]);
|
||||
|
||||
const goBackIfNoSuggestedAssetsOnFirstRender = () => {
|
||||
if (!suggestedAssets.length) {
|
||||
history.push(mostRecentOverviewPage);
|
||||
}
|
||||
}, [history, suggestedAssets, mostRecentOverviewPage]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
goBackIfNoSuggestedAssetsOnFirstRender();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page-container">
|
||||
@ -134,30 +150,34 @@ const ConfirmAddSuggestedToken = (props) => {
|
||||
{reusedTokenNameActionableMessage}
|
||||
</div>
|
||||
<div className="page-container__content">
|
||||
<div className="confirm-import-token">
|
||||
<div className="confirm-import-token__header">
|
||||
<div className="confirm-import-token__token">{t('token')}</div>
|
||||
<div className="confirm-import-token__balance">{t('balance')}</div>
|
||||
<div className="confirm-add-suggested-token">
|
||||
<div className="confirm-add-suggested-token__header">
|
||||
<div className="confirm-add-suggested-token__token">
|
||||
{t('token')}
|
||||
</div>
|
||||
<div className="confirm-add-suggested-token__balance">
|
||||
{t('balance')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirm-import-token__token-list">
|
||||
<div className="confirm-add-suggested-token__token-list">
|
||||
{suggestedAssets.map(({ asset }) => {
|
||||
return (
|
||||
<div
|
||||
className="confirm-import-token__token-list-item"
|
||||
className="confirm-add-suggested-token__token-list-item"
|
||||
key={asset.address}
|
||||
>
|
||||
<div className="confirm-import-token__token confirm-import-token__data">
|
||||
<div className="confirm-add-suggested-token__token confirm-add-suggested-token__data">
|
||||
<Identicon
|
||||
className="confirm-import-token__token-icon"
|
||||
className="confirm-add-suggested-token__token-icon"
|
||||
diameter={48}
|
||||
address={asset.address}
|
||||
image={asset.image}
|
||||
/>
|
||||
<div className="confirm-import-token__name">
|
||||
<div className="confirm-add-suggested-token__name">
|
||||
{getTokenName(asset.name, asset.symbol)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirm-import-token__balance">
|
||||
<div className="confirm-add-suggested-token__balance">
|
||||
<TokenBalance token={asset} />
|
||||
</div>
|
||||
</div>
|
||||
@ -174,7 +194,7 @@ const ConfirmAddSuggestedToken = (props) => {
|
||||
className="page-container__footer-button"
|
||||
onClick={async () => {
|
||||
await Promise.all(
|
||||
suggestedAssets.map(async ({ id }) => rejectWatchAsset(id)),
|
||||
suggestedAssets.map(({ id }) => dispatch(rejectWatchAsset(id))),
|
||||
);
|
||||
history.push(mostRecentOverviewPage);
|
||||
}}
|
||||
@ -186,15 +206,7 @@ const ConfirmAddSuggestedToken = (props) => {
|
||||
large
|
||||
className="page-container__footer-button"
|
||||
disabled={suggestedAssets.length === 0}
|
||||
onClick={async () => {
|
||||
await Promise.all(
|
||||
suggestedAssets.map(async ({ asset, id }) => {
|
||||
await acceptWatchAsset(id);
|
||||
tokenAddedEvent(asset);
|
||||
}),
|
||||
);
|
||||
history.push(mostRecentOverviewPage);
|
||||
}}
|
||||
onClick={handleAddTokensClick}
|
||||
>
|
||||
{t('addToken')}
|
||||
</Button>
|
||||
@ -204,13 +216,4 @@ const ConfirmAddSuggestedToken = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
ConfirmAddSuggestedToken.propTypes = {
|
||||
acceptWatchAsset: PropTypes.func,
|
||||
history: PropTypes.object,
|
||||
mostRecentOverviewPage: PropTypes.string.isRequired,
|
||||
rejectWatchAsset: PropTypes.func,
|
||||
suggestedAssets: PropTypes.array,
|
||||
tokens: PropTypes.array,
|
||||
};
|
||||
|
||||
export default ConfirmAddSuggestedToken;
|
@ -10,7 +10,6 @@ export default {
|
||||
title: 'Pages/ConfirmAddSuggestedToken',
|
||||
id: __filename,
|
||||
argTypes: {
|
||||
// Data
|
||||
tokens: {
|
||||
control: 'array',
|
||||
table: { category: 'Data' },
|
||||
@ -19,26 +18,6 @@ export default {
|
||||
control: 'array',
|
||||
table: { category: 'Data' },
|
||||
},
|
||||
|
||||
// Text
|
||||
mostRecentOverviewPage: {
|
||||
control: { type: 'text', disable: true },
|
||||
table: { category: 'Text' },
|
||||
},
|
||||
|
||||
// Events
|
||||
acceptWatchAsset: {
|
||||
action: 'acceptWatchAsset',
|
||||
table: { category: 'Events' },
|
||||
},
|
||||
history: {
|
||||
action: 'history',
|
||||
table: { category: 'Events' },
|
||||
},
|
||||
rejectWatchAsset: {
|
||||
action: 'rejectWatchAsset',
|
||||
table: { category: 'Events' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,142 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { acceptWatchAsset, rejectWatchAsset } from '../../store/actions';
|
||||
import configureStore from '../../store/store';
|
||||
import { renderWithProvider } from '../../../test/jest/rendering';
|
||||
import ConfirmAddSuggestedToken from '.';
|
||||
|
||||
const MOCK_SUGGESTED_ASSETS = [
|
||||
{
|
||||
id: 1,
|
||||
asset: {
|
||||
address: '0x8b175474e89094c44da98b954eedeac495271d0a',
|
||||
symbol: 'NEW',
|
||||
decimals: 18,
|
||||
image: 'metamark.svg',
|
||||
unlisted: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
asset: {
|
||||
address: '0xC8c77482e45F1F44dE1745F52C74426C631bDD51',
|
||||
symbol: '0XYX',
|
||||
decimals: 18,
|
||||
image: '0x.svg',
|
||||
unlisted: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const MOCK_TOKEN = {
|
||||
address: '0x108cf70c7d384c552f42c07c41c0e1e46d77ea0d',
|
||||
symbol: 'TEST',
|
||||
decimals: '0',
|
||||
};
|
||||
|
||||
jest.mock('../../store/actions', () => ({
|
||||
acceptWatchAsset: jest.fn().mockReturnValue({ type: 'test' }),
|
||||
rejectWatchAsset: jest.fn().mockReturnValue({ type: 'test' }),
|
||||
}));
|
||||
|
||||
const renderComponent = (tokens = []) => {
|
||||
const store = configureStore({
|
||||
metamask: {
|
||||
suggestedAssets: [...MOCK_SUGGESTED_ASSETS],
|
||||
tokens,
|
||||
provider: { chainId: '0x1' },
|
||||
},
|
||||
history: {
|
||||
mostRecentOverviewPage: '/',
|
||||
},
|
||||
});
|
||||
return renderWithProvider(<ConfirmAddSuggestedToken />, store);
|
||||
};
|
||||
|
||||
describe('ConfirmAddSuggestedToken Component', () => {
|
||||
it('should render', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByText('Add Suggested Tokens')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Would you like to import these tokens?'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Token')).toBeInTheDocument();
|
||||
expect(screen.getByText('Balance')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Add Token' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the list of suggested tokens', () => {
|
||||
renderComponent();
|
||||
|
||||
for (const { asset } of MOCK_SUGGESTED_ASSETS) {
|
||||
expect(screen.getByText(asset.symbol)).toBeInTheDocument();
|
||||
}
|
||||
expect(screen.getAllByRole('img')).toHaveLength(
|
||||
MOCK_SUGGESTED_ASSETS.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch acceptWatchAsset when clicking the "Add Token" button', () => {
|
||||
renderComponent();
|
||||
const addTokenBtn = screen.getByRole('button', { name: 'Add Token' });
|
||||
|
||||
fireEvent.click(addTokenBtn);
|
||||
expect(acceptWatchAsset).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should dispatch rejectWatchAsset when clicking the "Cancel" button', () => {
|
||||
renderComponent();
|
||||
const cancelBtn = screen.getByRole('button', { name: 'Cancel' });
|
||||
|
||||
expect(rejectWatchAsset).toHaveBeenCalledTimes(0);
|
||||
fireEvent.click(cancelBtn);
|
||||
expect(rejectWatchAsset).toHaveBeenCalledTimes(
|
||||
MOCK_SUGGESTED_ASSETS.length,
|
||||
);
|
||||
});
|
||||
|
||||
describe('when the suggested token address matches an existing token address', () => {
|
||||
it('should show "already listed" warning', () => {
|
||||
const mockTokens = [
|
||||
{
|
||||
...MOCK_TOKEN,
|
||||
address: MOCK_SUGGESTED_ASSETS[0].asset.address,
|
||||
},
|
||||
];
|
||||
renderComponent(mockTokens);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'This action will edit tokens that are already listed in your wallet, which can be used' +
|
||||
' to phish you. Only approve if you are certain that you mean to change what these' +
|
||||
' tokens represent. Learn more about',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'scams and security risks.' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the suggested token symbol matches an existing token symbol and has a different address', () => {
|
||||
it('should show "reuses a symbol" warning', () => {
|
||||
const mockTokens = [
|
||||
{
|
||||
...MOCK_TOKEN,
|
||||
symbol: MOCK_SUGGESTED_ASSETS[0].asset.symbol,
|
||||
},
|
||||
];
|
||||
renderComponent(mockTokens);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'A token here reuses a symbol from another token you watch, this can be confusing or deceptive.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,3 +1 @@
|
||||
import ConfirmAddSuggestedToken from './confirm-add-suggested-token.container';
|
||||
|
||||
export default ConfirmAddSuggestedToken;
|
||||
export { default } from './confirm-add-suggested-token';
|
||||
|
@ -1,4 +1,6 @@
|
||||
.confirm-add-suggested-token {
|
||||
padding: 16px;
|
||||
|
||||
&__link {
|
||||
@include H7;
|
||||
|
||||
@ -6,4 +8,51 @@
|
||||
color: var(--primary-blue);
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
@include H7;
|
||||
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__token {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__balance {
|
||||
flex: 0 0 30%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__token-list {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
&__token-list-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__token-icon {
|
||||
margin-right: 12px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
@ -481,6 +481,10 @@ function getSuggestedAssetCount(state) {
|
||||
return suggestedAssets.length;
|
||||
}
|
||||
|
||||
export function getSuggestedAssets(state) {
|
||||
return state.metamask.suggestedAssets;
|
||||
}
|
||||
|
||||
export function getIsMainnet(state) {
|
||||
const chainId = getCurrentChainId(state);
|
||||
return chainId === MAINNET_CHAIN_ID;
|
||||
|
Loading…
Reference in New Issue
Block a user