ConfirmImportToken: Convert to Functional Component; Add storybook controls (#13594)

* ConfirmImportToken: convert to FC

* ConfirmImportToken: reorganize

* ConfirmImportToken: rm component from path

* replace mapStateToProps w/ useSelector
ConfirmImportToken

* replace mapDispatchToProps w/ useDispatch
ConfirmImportToken

* ConfirmAddSuggestedToken: useHistory

* ConfirmImportToken: add storybook controls

* ConfirmImportToken: rm 1st entry overwrite

* ConfirmImportToken: add tests

* ConfirmImportToken: use real Redux store in tests

* ConfirmImportToken: do not use mock-state.json
https://github.com/MetaMask/metamask-extension/pull/13594#discussion_r805022647

* ConfirmImportToken: tokenAddedEvent -> trackTokenAddedEvent

* Update ui/pages/confirm-import-token/confirm-import-token.js

Co-authored-by: Elliot Winkler <elliot.winkler@gmail.com>

* ConfirmImportToken: update useSelector logic

* ConfirmImportToken: replace typeof check w/ === undefined

* ConfirmImportToken: rm unnecessary /rendering from path

* ConfirmImportToken: fix add token redirect

* ConfirmImportToken: use useNewMetricEvent

* ConfirmImportToken: rename vars using "addedToken"

* ConfirmImportToken: setAddedToken to obj copy

Co-authored-by: Elliot Winkler <elliot.winkler@gmail.com>
feature/default_network_editable
Ariella Vu 3 years ago committed by GitHub
parent b954ca447d
commit fadad601b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      ui/ducks/metamask/metamask.js
  2. 141
      ui/pages/confirm-import-token/confirm-import-token.component.js
  3. 24
      ui/pages/confirm-import-token/confirm-import-token.container.js
  4. 142
      ui/pages/confirm-import-token/confirm-import-token.js
  5. 42
      ui/pages/confirm-import-token/confirm-import-token.stories.js
  6. 128
      ui/pages/confirm-import-token/confirm-import-token.test.js
  7. 2
      ui/pages/confirm-import-token/index.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) {

@ -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 (
<div className="page-container">
<div className="page-container__header">
<div className="page-container__title">
{this.context.t('importTokensCamelCase')}
</div>
<div className="page-container__subtitle">
{this.context.t('likeToImportTokens')}
</div>
</div>
<div className="page-container__content">
<div className="confirm-import-token">
<div className="confirm-import-token__header">
<div className="confirm-import-token__token">
{this.context.t('token')}
</div>
<div className="confirm-import-token__balance">
{this.context.t('balance')}
</div>
</div>
<div className="confirm-import-token__token-list">
{Object.entries(pendingTokens).map(([address, token]) => {
const { name, symbol } = token;
return (
<div
className="confirm-import-token__token-list-item"
key={address}
>
<div className="confirm-import-token__token confirm-import-token__data">
<Identicon
className="confirm-import-token__token-icon"
diameter={48}
address={address}
/>
<div className="confirm-import-token__name">
{this.getTokenName(name, symbol)}
</div>
</div>
<div className="confirm-import-token__balance">
<TokenBalance token={token} />
</div>
</div>
);
})}
</div>
</div>
</div>
<div className="page-container__footer">
<footer>
<Button
type="secondary"
large
className="page-container__footer-button"
onClick={() => history.push(IMPORT_TOKEN_ROUTE)}
>
{this.context.t('back')}
</Button>
<Button
type="primary"
large
className="page-container__footer-button"
onClick={() => {
addTokens(pendingTokens).then(() => {
const pendingTokenValues = Object.values(pendingTokens);
pendingTokenValues.forEach((pendingToken) => {
this.context.trackEvent({
event: 'Token Added',
category: 'Wallet',
sensitiveProperties: {
token_symbol: pendingToken.symbol,
token_contract_address: pendingToken.address,
token_decimal_precision: pendingToken.decimals,
unlisted: pendingToken.unlisted,
source: pendingToken.isCustom ? 'custom' : 'list',
},
});
});
clearPendingTokens();
const firstTokenAddress = pendingTokenValues?.[0].address?.toLowerCase();
if (firstTokenAddress) {
history.push(`${ASSET_ROUTE}/${firstTokenAddress}`);
} else {
history.push(mostRecentOverviewPage);
}
});
}}
>
{this.context.t('importTokensCamelCase')}
</Button>
</footer>
</div>
</div>
);
}
}

@ -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);

@ -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 (
<div className="page-container">
<div className="page-container__header">
<div className="page-container__title">
{t('importTokensCamelCase')}
</div>
<div className="page-container__subtitle">
{t('likeToImportTokens')}
</div>
</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>
<div className="confirm-import-token__token-list">
{Object.entries(pendingTokens).map(([address, token]) => {
const { name, symbol } = token;
return (
<div
className="confirm-import-token__token-list-item"
key={address}
>
<div className="confirm-import-token__token confirm-import-token__data">
<Identicon
className="confirm-import-token__token-icon"
diameter={48}
address={address}
/>
<div className="confirm-import-token__name">
{getTokenName(name, symbol)}
</div>
</div>
<div className="confirm-import-token__balance">
<TokenBalance token={token} />
</div>
</div>
);
})}
</div>
</div>
</div>
<div className="page-container__footer">
<footer>
<Button
type="secondary"
large
className="page-container__footer-button"
onClick={() => history.push(IMPORT_TOKEN_ROUTE)}
>
{t('back')}
</Button>
<Button
type="primary"
large
className="page-container__footer-button"
onClick={handleAddTokens}
>
{t('importTokensCamelCase')}
</Button>
</footer>
</div>
</div>
);
};
export default ConfirmImportToken;

@ -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 (
<PageSet>
<ConfirmAddToken history={history} />
<PageSet pendingTokens={pendingTokens}>
<ConfirmAddToken />
</PageSet>
);
};
DefaultStory.args = {
pendingTokens: { ...tokens },
};
DefaultStory.storyName = 'Default';

@ -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(<ConfirmImportToken />, 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}`,
);
});
});

@ -1,3 +1,3 @@
import ConfirmImportToken from './confirm-import-token.container';
import ConfirmImportToken from './confirm-import-token';
export default ConfirmImportToken;

Loading…
Cancel
Save