import React, { useCallback, useContext, useEffect, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { ethErrors, serializeError } from 'eth-rpc-errors'; import { ApprovalType } from '@metamask/controller-utils'; 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 { PageContainerFooter } from '../../components/ui/page-container'; import { I18nContext } from '../../contexts/i18n'; import { MetaMetricsContext } from '../../contexts/metametrics'; 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 { getApprovalRequestsByType } from '../../selectors'; import { resolvePendingApproval, rejectPendingApproval, } from '../../store/actions'; import { MetaMetricsEventCategory, MetaMetricsEventName, MetaMetricsTokenEventSource, } from '../../../shared/constants/metametrics'; import { AssetType, TokenStandard, } from '../../../shared/constants/transaction'; function getTokenName(name, symbol) { return name === undefined ? symbol : `${name} (${symbol})`; } /** * @param {Array} suggestedAssets - an array of assets suggested to add to the user's wallet * via the RPC method `wallet_watchAsset` * @param {Array} tokens - the list of tokens currently tracked in state * @returns {boolean} Returns true when the list of suggestedAssets contains an entry with * an address that matches an existing token. */ function hasDuplicateAddress(suggestedAssets, tokens) { const duplicate = suggestedAssets.find(({ asset }) => { const dupe = tokens.find(({ address }) => { return isEqualCaseInsensitive(address, asset.address); }); return Boolean(dupe); }); return Boolean(duplicate); } /** * @param {Array} suggestedAssets - a list of assets suggested to add to the user's wallet * via RPC method `wallet_watchAsset` * @param {Array} tokens - the list of tokens currently tracked in state * @returns {boolean} Returns true when the list of suggestedAssets contains an entry with both * 1. a symbol that matches an existing token * 2. an address that does not match an existing token */ function hasDuplicateSymbolAndDiffAddress(suggestedAssets, tokens) { const duplicate = suggestedAssets.find(({ asset }) => { const dupe = tokens.find((token) => { return ( isEqualCaseInsensitive(token.symbol, asset.symbol) && !isEqualCaseInsensitive(token.address, asset.address) ); }); return Boolean(dupe); }); return Boolean(duplicate); } const ConfirmAddSuggestedToken = () => { const t = useContext(I18nContext); const dispatch = useDispatch(); const history = useHistory(); const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage); const suggestedAssets = useSelector((metamaskState) => getApprovalRequestsByType(metamaskState, ApprovalType.WatchAsset).map( ({ requestData }) => requestData, ), ); const tokens = useSelector(getTokens); const trackEvent = useContext(MetaMetricsContext); const knownTokenActionableMessage = useMemo(() => { return ( hasDuplicateAddress(suggestedAssets, tokens) && ( {t('learnScamRisk')} , ])} type="warning" withRightButton useIcon iconFillColor="#f8c000" /> ) ); }, [suggestedAssets, tokens, t]); const reusedTokenNameActionableMessage = useMemo(() => { return ( hasDuplicateSymbolAndDiffAddress(suggestedAssets, tokens) && ( ) ); }, [suggestedAssets, tokens, t]); const handleAddTokensClick = useCallback(async () => { await Promise.all( suggestedAssets.map(async ({ asset, id }) => { await dispatch(resolvePendingApproval(id, null)); trackEvent({ event: MetaMetricsEventName.TokenAdded, category: MetaMetricsEventCategory.Wallet, sensitiveProperties: { token_symbol: asset.symbol, token_contract_address: asset.address, token_decimal_precision: asset.decimals, unlisted: asset.unlisted, source_connection_method: MetaMetricsTokenEventSource.Dapp, token_standard: TokenStandard.ERC20, asset_type: AssetType.token, }, }); }), ); history.push(mostRecentOverviewPage); }, [dispatch, history, trackEvent, mostRecentOverviewPage, suggestedAssets]); const handleCancelClick = useCallback(async () => { await Promise.all( suggestedAssets.map(({ id }) => dispatch( rejectPendingApproval( id, serializeError(ethErrors.provider.userRejectedRequest()), ), ), ), ); history.push(mostRecentOverviewPage); }, [dispatch, history, mostRecentOverviewPage, suggestedAssets]); const goBackIfNoSuggestedAssetsOnFirstRender = () => { if (!suggestedAssets.length) { history.push(mostRecentOverviewPage); } }; useEffect(() => { goBackIfNoSuggestedAssetsOnFirstRender(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (
{t('addSuggestedTokens')}
{t('likeToImportTokens')}
{knownTokenActionableMessage} {reusedTokenNameActionableMessage}
{t('token')}
{t('balance')}
{suggestedAssets.map(({ asset }) => { return (
{getTokenName(asset.name, asset.symbol)}
); })}
); }; export default ConfirmAddSuggestedToken;