import React, { useEffect, useRef, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { getMetaMaskAccounts } from '../../../selectors'; import CustodyLabels from '../../../components/institutional/custody-labels/custody-labels'; import PulseLoader from '../../../components/ui/pulse-loader'; import { INSTITUTIONAL_FEATURES_DONE_ROUTE } from '../../../helpers/constants/routes'; import { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP } from '../../../../shared/constants/swaps'; import { CHAIN_IDS } from '../../../../shared/constants/network'; import { shortenAddress } from '../../../helpers/utils/util'; import Tooltip from '../../../components/ui/tooltip'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { mmiActionsFactory, showInteractiveReplacementTokenBanner, } from '../../../store/institutional/institution-background'; import { Label, Icon, ButtonLink, IconName, IconSize, Box, Button, BUTTON_VARIANT, BUTTON_SIZES, } from '../../../components/component-library'; import { Text } from '../../../components/component-library/text/deprecated'; import { OverflowWrap, TextColor, JustifyContent, BlockSize, Display, FlexDirection, IconColor, } from '../../../helpers/constants/design-system'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; const getButtonLinkHref = ({ address }) => { const url = SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[CHAIN_IDS.MAINNET]; return `${url}address/${address}`; }; export default function InteractiveReplacementTokenPage({ history }) { const dispatch = useDispatch(); const isMountedRef = useRef(false); const mmiActions = mmiActionsFactory(); const address = useSelector( (state) => state.appState.modal.modalState.props?.address, ); const { selectedAddress, custodyAccountDetails, interactiveReplacementToken, mmiConfiguration, } = useSelector((state) => state.metamask); const { custodianName } = custodyAccountDetails[toChecksumHexAddress(address || selectedAddress)] || {}; const { url } = interactiveReplacementToken || {}; const { custodians } = mmiConfiguration; const custodian = custodians.find((item) => item.name === custodianName) || {}; const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage); const metaMaskAccounts = useSelector(getMetaMaskAccounts); const connectRequests = useSelector( (state) => state.metamask.institutionalFeatures?.connectRequests, ); const [isLoading, setIsLoading] = useState(false); const [tokenAccounts, setTokenAccounts] = useState([]); const [error, setError] = useState(false); const t = useI18nContext(); const [copied, handleCopy] = useCopyToClipboard(); const { removeAddTokenConnectRequest, setCustodianNewRefreshToken, getCustodianAccounts, } = mmiActions; const connectRequest = connectRequests ? connectRequests[0] : undefined; useEffect(() => { isMountedRef.current = true; return () => (isMountedRef.current = false); }, []); useEffect(() => { let isMounted = true; const getTokenAccounts = async () => { if (!connectRequest) { history.push(mostRecentOverviewPage); setIsLoading(false); return; } try { const custodianAccounts = await dispatch( getCustodianAccounts( connectRequest.token, connectRequest.apiUrl, connectRequest.service, false, ), ); const filteredAccounts = custodianAccounts.filter( (account) => metaMaskAccounts[account.address.toLowerCase()], ); const mappedAccounts = filteredAccounts.map((account) => ({ address: account.address, name: account.name, labels: account.labels, balance: metaMaskAccounts[account.address.toLowerCase()]?.balance || 0, })); if (isMounted) { setTokenAccounts(mappedAccounts); setIsLoading(false); } } catch (e) { setError(true); setIsLoading(false); } finally { if (isMounted) { setIsLoading(false); } } }; getTokenAccounts(); return () => { isMounted = false; }; // We just want to get the accounts in the render of the component // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (!connectRequest) { history.push(mostRecentOverviewPage); setIsLoading(false); } }, [connectRequest, history, mostRecentOverviewPage]); if (!connectRequest) { return null; } const onRemoveAddTokenConnectRequest = ({ origin, apiUrl, token }) => { dispatch( removeAddTokenConnectRequest({ origin, apiUrl, token, }), ); }; const handleReject = () => { setIsLoading(true); onRemoveAddTokenConnectRequest(connectRequest); }; const handleApprove = async () => { if (error) { global.platform.openTab({ url, }); handleReject(); return; } setIsLoading(true); try { await Promise.all( tokenAccounts.map(async (account) => { await dispatch( setCustodianNewRefreshToken({ address: account.address, newAuthDetails: { refreshToken: connectRequest.token, refreshTokenUrl: connectRequest.apiUrl, }, }), ); }), ); dispatch(showInteractiveReplacementTokenBanner({})); onRemoveAddTokenConnectRequest(connectRequest); history.push({ pathname: INSTITUTIONAL_FEATURES_DONE_ROUTE, state: { imgSrc: custodian?.iconUrl, title: t('custodianReplaceRefreshTokenChangedTitle'), description: t('custodianReplaceRefreshTokenChangedSubtitle'), }, }); if (isMountedRef.current) { setIsLoading(false); } } catch (e) { console.error(e); } }; return ( {t('custodianReplaceRefreshTokenTitle')}{' '} {error ? t('failed').toLowerCase() : ''} {!error && ( {t('custodianReplaceRefreshTokenSubtitle')} )} {error ? ( {t('custodianReplaceRefreshTokenChangedFailed', [ custodian.displayName || 'Custodian', ])} ) : ( {tokenAccounts.map((account, idx) => { return ( {account.labels && ( )} ); })} )} {isLoading ? ( ) : ( )} ); } InteractiveReplacementTokenPage.propTypes = { history: PropTypes.object, };