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
parent
b954ca447d
commit
fadad601b8
@ -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; |
@ -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…
Reference in new issue