diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js
index bb2634288..7aa2e1220 100644
--- a/ui/ducks/metamask/metamask.js
+++ b/ui/ducks/metamask/metamask.js
@@ -258,6 +258,8 @@ export const getWeb3ShimUsageAlertEnabledness = (state) =>
export const getUnconnectedAccountAlertShown = (state) =>
state.metamask.unconnectedAccountAlertShownOrigins;
+export const getPendingTokens = (state) => state.metamask.pendingTokens;
+
export const getTokens = (state) => state.metamask.tokens;
export function getCollectiblesDetectionNoticeDismissed(state) {
diff --git a/ui/pages/confirm-import-token/confirm-import-token.component.js b/ui/pages/confirm-import-token/confirm-import-token.component.js
deleted file mode 100644
index b64baacec..000000000
--- a/ui/pages/confirm-import-token/confirm-import-token.component.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import {
- ASSET_ROUTE,
- IMPORT_TOKEN_ROUTE,
-} from '../../helpers/constants/routes';
-import Button from '../../components/ui/button';
-import Identicon from '../../components/ui/identicon';
-import TokenBalance from '../../components/ui/token-balance';
-
-export default class ConfirmImportToken extends Component {
- static contextTypes = {
- t: PropTypes.func,
- trackEvent: PropTypes.func,
- };
-
- static propTypes = {
- history: PropTypes.object,
- clearPendingTokens: PropTypes.func,
- addTokens: PropTypes.func,
- mostRecentOverviewPage: PropTypes.string.isRequired,
- pendingTokens: PropTypes.object,
- };
-
- componentDidMount() {
- const { mostRecentOverviewPage, pendingTokens = {}, history } = this.props;
-
- if (Object.keys(pendingTokens).length === 0) {
- history.push(mostRecentOverviewPage);
- }
- }
-
- getTokenName(name, symbol) {
- return typeof name === 'undefined' ? symbol : `${name} (${symbol})`;
- }
-
- render() {
- const {
- history,
- addTokens,
- clearPendingTokens,
- mostRecentOverviewPage,
- pendingTokens,
- } = this.props;
-
- return (
-
-
-
- {this.context.t('importTokensCamelCase')}
-
-
- {this.context.t('likeToImportTokens')}
-
-
-
-
-
-
- {this.context.t('token')}
-
-
- {this.context.t('balance')}
-
-
-
- {Object.entries(pendingTokens).map(([address, token]) => {
- const { name, symbol } = token;
-
- return (
-
-
-
-
- {this.getTokenName(name, symbol)}
-
-
-
-
-
-
- );
- })}
-
-
-
-
-
-
-
- );
- }
-}
diff --git a/ui/pages/confirm-import-token/confirm-import-token.container.js b/ui/pages/confirm-import-token/confirm-import-token.container.js
deleted file mode 100644
index ef80986cb..000000000
--- a/ui/pages/confirm-import-token/confirm-import-token.container.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { connect } from 'react-redux';
-
-import { addTokens, clearPendingTokens } from '../../store/actions';
-import { getMostRecentOverviewPage } from '../../ducks/history/history';
-import ConfirmImportToken from './confirm-import-token.component';
-
-const mapStateToProps = (state) => {
- const {
- metamask: { pendingTokens },
- } = state;
- return {
- mostRecentOverviewPage: getMostRecentOverviewPage(state),
- pendingTokens,
- };
-};
-
-const mapDispatchToProps = (dispatch) => {
- return {
- addTokens: (tokens) => dispatch(addTokens(tokens)),
- clearPendingTokens: () => dispatch(clearPendingTokens()),
- };
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(ConfirmImportToken);
diff --git a/ui/pages/confirm-import-token/confirm-import-token.js b/ui/pages/confirm-import-token/confirm-import-token.js
new file mode 100644
index 000000000..dcba139b2
--- /dev/null
+++ b/ui/pages/confirm-import-token/confirm-import-token.js
@@ -0,0 +1,142 @@
+import React, { useCallback, useContext, useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useHistory } from 'react-router-dom';
+import {
+ ASSET_ROUTE,
+ IMPORT_TOKEN_ROUTE,
+} from '../../helpers/constants/routes';
+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 { getMostRecentOverviewPage } from '../../ducks/history/history';
+import { getPendingTokens } from '../../ducks/metamask/metamask';
+import { useNewMetricEvent } from '../../hooks/useMetricEvent';
+import { addTokens, clearPendingTokens } from '../../store/actions';
+
+const getTokenName = (name, symbol) => {
+ return name === undefined ? symbol : `${name} (${symbol})`;
+};
+
+const ConfirmImportToken = () => {
+ const t = useContext(I18nContext);
+ const dispatch = useDispatch();
+ const history = useHistory();
+
+ const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
+ const pendingTokens = useSelector(getPendingTokens);
+
+ const [addedToken, setAddedToken] = useState({});
+
+ const trackTokenAddedEvent = useNewMetricEvent({
+ event: 'Token Added',
+ category: 'Wallet',
+ sensitiveProperties: {
+ token_symbol: addedToken.symbol,
+ token_contract_address: addedToken.address,
+ token_decimal_precision: addedToken.decimals,
+ unlisted: addedToken.unlisted,
+ source: addedToken.isCustom ? 'custom' : 'list',
+ },
+ });
+
+ const handleAddTokens = useCallback(async () => {
+ await dispatch(addTokens(pendingTokens));
+
+ const addedTokenValues = Object.values(pendingTokens);
+ const firstTokenAddress = addedTokenValues?.[0].address?.toLowerCase();
+
+ addedTokenValues.forEach((pendingToken) => {
+ setAddedToken({ ...pendingToken });
+ });
+ dispatch(clearPendingTokens());
+
+ if (firstTokenAddress) {
+ history.push(`${ASSET_ROUTE}/${firstTokenAddress}`);
+ } else {
+ history.push(mostRecentOverviewPage);
+ }
+ }, [dispatch, history, mostRecentOverviewPage, pendingTokens]);
+
+ useEffect(() => {
+ if (Object.keys(addedToken).length) {
+ trackTokenAddedEvent();
+ }
+ }, [addedToken, trackTokenAddedEvent]);
+
+ useEffect(() => {
+ if (Object.keys(pendingTokens).length === 0) {
+ history.push(mostRecentOverviewPage);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+
+ {t('importTokensCamelCase')}
+
+
+ {t('likeToImportTokens')}
+
+
+
+
+
+
{t('token')}
+
{t('balance')}
+
+
+ {Object.entries(pendingTokens).map(([address, token]) => {
+ const { name, symbol } = token;
+
+ return (
+
+
+
+
+ {getTokenName(name, symbol)}
+
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+};
+
+export default ConfirmImportToken;
diff --git a/ui/pages/confirm-import-token/confirm-import-token.stories.js b/ui/pages/confirm-import-token/confirm-import-token.stories.js
index 2ec34bf8d..413327382 100644
--- a/ui/pages/confirm-import-token/confirm-import-token.stories.js
+++ b/ui/pages/confirm-import-token/confirm-import-token.stories.js
@@ -1,8 +1,6 @@
/* eslint-disable react/prop-types */
import React, { useEffect } from 'react';
-import { createBrowserHistory } from 'history';
-import { text } from '@storybook/addon-knobs';
import { store, getNewState } from '../../../.storybook/preview';
import { tokens } from '../../../.storybook/initial-states/approval-screens/add-token';
import { updateMetamaskState } from '../../store/actions';
@@ -11,45 +9,39 @@ import ConfirmAddToken from '.';
export default {
title: 'Pages/ConfirmImportToken',
id: __filename,
+
+ argTypes: {
+ pendingTokens: {
+ control: 'object',
+ table: { category: 'Data' },
+ },
+ },
};
-const history = createBrowserHistory();
+const PageSet = ({ children, pendingTokens }) => {
+ const { metamask: state } = store.getState();
-const PageSet = ({ children }) => {
- const symbol = text('symbol', 'TRDT');
- const state = store.getState();
- const pendingTokensState = state.metamask.pendingTokens;
- // only change the first token in the list
useEffect(() => {
- const pendingTokens = { ...pendingTokensState };
- pendingTokens['0x33f90dee07c6e8b9682dd20f73e6c358b2ed0f03'].symbol = symbol;
store.dispatch(
updateMetamaskState(
- getNewState(state.metamask, {
+ getNewState(state, {
pendingTokens,
}),
),
);
- }, [symbol, pendingTokensState, state.metamask]);
+ }, [state, pendingTokens]);
return children;
};
-export const DefaultStory = () => {
- const { metamask: state } = store.getState();
- store.dispatch(
- updateMetamaskState(
- getNewState(state, {
- pendingTokens: tokens,
- }),
- ),
- );
-
+export const DefaultStory = ({ pendingTokens }) => {
return (
-
-
+
+
);
};
-
+DefaultStory.args = {
+ pendingTokens: { ...tokens },
+};
DefaultStory.storyName = 'Default';
diff --git a/ui/pages/confirm-import-token/confirm-import-token.test.js b/ui/pages/confirm-import-token/confirm-import-token.test.js
new file mode 100644
index 000000000..5d9cb28a2
--- /dev/null
+++ b/ui/pages/confirm-import-token/confirm-import-token.test.js
@@ -0,0 +1,128 @@
+import React from 'react';
+import reactRouterDom from 'react-router-dom';
+import { fireEvent, screen } from '@testing-library/react';
+import {
+ ASSET_ROUTE,
+ IMPORT_TOKEN_ROUTE,
+} from '../../helpers/constants/routes';
+import { addTokens, clearPendingTokens } from '../../store/actions';
+import configureStore from '../../store/store';
+import { renderWithProvider } from '../../../test/jest';
+import ConfirmImportToken from '.';
+
+const MOCK_PENDING_TOKENS = {
+ '0x6b175474e89094c44da98b954eedeac495271d0f': {
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ symbol: 'META',
+ decimals: 18,
+ image: 'metamark.svg',
+ },
+ '0xB8c77482e45F1F44dE1745F52C74426C631bDD52': {
+ address: '0xB8c77482e45F1F44dE1745F52C74426C631bDD52',
+ symbol: '0X',
+ decimals: 18,
+ image: '0x.svg',
+ },
+};
+
+jest.mock('../../store/actions', () => ({
+ addTokens: jest.fn().mockReturnValue({ type: 'test' }),
+ clearPendingTokens: jest
+ .fn()
+ .mockReturnValue({ type: 'CLEAR_PENDING_TOKENS' }),
+}));
+
+const renderComponent = (mockPendingTokens = MOCK_PENDING_TOKENS) => {
+ const store = configureStore({
+ metamask: {
+ pendingTokens: { ...mockPendingTokens },
+ provider: { chainId: '0x1' },
+ },
+ history: {
+ mostRecentOverviewPage: '/',
+ },
+ });
+
+ return renderWithProvider(, store);
+};
+
+describe('ConfirmImportToken Component', () => {
+ const mockHistoryPush = jest.fn();
+
+ beforeEach(() => {
+ jest
+ .spyOn(reactRouterDom, 'useHistory')
+ .mockImplementation()
+ .mockReturnValue({ push: mockHistoryPush });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render', () => {
+ renderComponent();
+
+ const [title, importTokensBtn] = screen.queryAllByText('Import Tokens');
+
+ expect(title).toBeInTheDocument(title);
+ 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: 'Back' })).toBeInTheDocument();
+ expect(importTokensBtn).toBeInTheDocument();
+ });
+
+ it('should render the list of tokens', () => {
+ renderComponent();
+
+ Object.values(MOCK_PENDING_TOKENS).forEach((token) => {
+ expect(screen.getByText(token.symbol)).toBeInTheDocument();
+ });
+ });
+
+ it('should go to "IMPORT_TOKEN_ROUTE" route when clicking the "Back" button', async () => {
+ renderComponent();
+
+ const backBtn = screen.getByRole('button', { name: 'Back' });
+
+ await fireEvent.click(backBtn);
+ expect(mockHistoryPush).toHaveBeenCalledTimes(1);
+ expect(mockHistoryPush).toHaveBeenCalledWith(IMPORT_TOKEN_ROUTE);
+ });
+
+ it('should dispatch clearPendingTokens and redirect to the first token page when clicking the "Import Tokens" button', async () => {
+ const mockFirstPendingTokenAddress =
+ '0xe83cccfabd4ed148903bf36d4283ee7c8b3494d1';
+ const mockPendingTokens = {
+ [mockFirstPendingTokenAddress]: {
+ address: mockFirstPendingTokenAddress,
+ symbol: 'CVL',
+ decimals: 18,
+ image: 'CVL_token.svg',
+ },
+ '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e': {
+ address: '0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e',
+ symbol: 'GLA',
+ decimals: 18,
+ image: 'gladius.svg',
+ },
+ };
+ renderComponent(mockPendingTokens);
+
+ const importTokensBtn = screen.getByRole('button', {
+ name: 'Import Tokens',
+ });
+
+ await fireEvent.click(importTokensBtn);
+
+ expect(addTokens).toHaveBeenCalled();
+ expect(clearPendingTokens).toHaveBeenCalled();
+ expect(mockHistoryPush).toHaveBeenCalledTimes(1);
+ expect(mockHistoryPush).toHaveBeenCalledWith(
+ `${ASSET_ROUTE}/${mockFirstPendingTokenAddress}`,
+ );
+ });
+});
diff --git a/ui/pages/confirm-import-token/index.js b/ui/pages/confirm-import-token/index.js
index cddbd5032..4443efa6b 100644
--- a/ui/pages/confirm-import-token/index.js
+++ b/ui/pages/confirm-import-token/index.js
@@ -1,3 +1,3 @@
-import ConfirmImportToken from './confirm-import-token.container';
+import ConfirmImportToken from './confirm-import-token';
export default ConfirmImportToken;