import React, { useState, useMemo, useCallback, useEffect, useContext, } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { v4 as uuidv4 } from 'uuid'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { mmiActionsFactory } from '../../../store/institutional/institution-background'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { ButtonIcon, Button, Text, Label, IconName, IconSize, BUTTON_SIZES, BUTTON_VARIANT, Box, } from '../../../components/component-library'; import { AlignItems, Display, FlexDirection, FontWeight, Color, JustifyContent, BorderRadius, BorderColor, BlockSize, TextColor, TextAlign, TextVariant, } from '../../../helpers/constants/design-system'; import { CUSTODY_ACCOUNT_DONE_ROUTE, DEFAULT_ROUTE, } from '../../../helpers/constants/routes'; import { getCurrentChainId } from '../../../selectors'; import { getMMIConfiguration } from '../../../selectors/institutional/selectors'; import CustodyAccountList from '../connect-custody/account-list'; import JwtUrlForm from '../../../components/institutional/jwt-url-form'; const CustodyPage = () => { const t = useI18nContext(); const history = useHistory(); const trackEvent = useContext(MetaMetricsContext); const dispatch = useDispatch(); const mmiActions = mmiActionsFactory(); const currentChainId = useSelector(getCurrentChainId); const { custodians } = useSelector(getMMIConfiguration); const [selectedAccounts, setSelectedAccounts] = useState({}); const [selectedCustodianName, setSelectedCustodianName] = useState(''); const [selectedCustodianImage, setSelectedCustodianImage] = useState(null); const [selectedCustodianDisplayName, setSelectedCustodianDisplayName] = useState(''); const [selectedCustodianType, setSelectedCustodianType] = useState(''); const [connectError, setConnectError] = useState(''); const [currentJwt, setCurrentJwt] = useState(''); const [selectError, setSelectError] = useState(''); const [jwtList, setJwtList] = useState([]); const [apiUrl, setApiUrl] = useState(''); const [addNewTokenClicked, setAddNewTokenClicked] = useState(false); const [chainId, setChainId] = useState(0); const [connectRequest, setConnectRequest] = useState(undefined); const [accounts, setAccounts] = useState(); const custodianButtons = useMemo(() => { const custodianItems = []; const sortedCustodians = custodians.sort(function (a, b) { const nameA = a.name.toLowerCase(); const nameB = b.name.toLowerCase(); if (nameA < nameB) { return -1; } if (nameA > nameB) { return 1; } return 0; }); sortedCustodians.forEach((custodian) => { if ( (!custodian.production && process.env.METAMASK_ENVIRONMENT === 'production') || custodian.hidden || (connectRequest && Object.keys(connectRequest).length && custodian.name !== selectedCustodianName) ) { return; } custodianItems.push( {custodian.iconUrl && ( {custodian.displayName} )} {custodian.displayName} , ); }); return custodianItems; }, [ connectRequest, custodians, dispatch, mmiActions, selectedCustodianName, t, trackEvent, ]); const handleConnectError = useCallback( (e) => { let errorMessage; const detailedError = e.message.split(':'); if (detailedError.length > 1 && !isNaN(parseInt(detailedError[0], 10))) { if (parseInt(detailedError[0], 10) === 401) { // Authentication Error errorMessage = 'Authentication error. Please ensure you have entered the correct token'; } } if (/Network Error/u.test(e.message)) { errorMessage = 'Network error. Please ensure you have entered the correct API URL'; } if (!errorMessage) { errorMessage = e.message; } setConnectError( `Something went wrong connecting your custodian account. Error details: ${errorMessage}`, ); trackEvent({ category: 'MMI', event: 'Connect to custodian error', properties: { custodian: selectedCustodianName, }, }); }, [selectedCustodianName, trackEvent], ); const getCustodianAccounts = useCallback( async (token, custody, getNonImportedAccounts) => { return await dispatch( mmiActions.getCustodianAccounts( token, apiUrl, custody || selectedCustodianType, getNonImportedAccounts, ), ); }, [dispatch, mmiActions, apiUrl, selectedCustodianType], ); const connect = useCallback(async () => { try { // If you have one JWT already, but no dropdown yet, currentJwt is null! const jwt = currentJwt || jwtList[0]; setConnectError(''); const accountsValue = await getCustodianAccounts( jwt, apiUrl, selectedCustodianType, true, ); setAccounts(accountsValue); trackEvent({ category: 'MMI', event: 'Connect to custodian', properties: { custodian: selectedCustodianName, apiUrl, rpc: Boolean(connectRequest), }, }); } catch (e) { handleConnectError(e); } }, [ apiUrl, connectRequest, currentJwt, getCustodianAccounts, handleConnectError, jwtList, selectedCustodianName, selectedCustodianType, trackEvent, ]); useEffect(() => { const fetchConnectRequest = async () => { const connectRequestValue = await dispatch( mmiActions.getCustodianConnectRequest(), ); setChainId(parseInt(currentChainId, 16)); // check if it's empty object if (Object.keys(connectRequestValue).length) { setConnectRequest(connectRequestValue); setCurrentJwt( connectRequestValue.token || (await dispatch(mmiActions.getCustodianToken())), ); setSelectedCustodianType(connectRequestValue.custodianType); setSelectedCustodianName(connectRequestValue.custodianName); setApiUrl(connectRequestValue.apiUrl); connect(); } }; // call the function fetchConnectRequest() // make sure to catch any error .catch(console.error); }, [dispatch, connect, currentChainId, mmiActions]); useEffect(() => { const handleNetworkChange = async () => { if (!isNaN(chainId)) { const jwt = currentJwt || jwtList[0]; if (jwt && jwt.length) { setAccounts( await getCustodianAccounts( jwt, apiUrl, selectedCustodianType, true, ), ); } } }; if (parseInt(chainId, 16) !== chainId) { setChainId(parseInt(currentChainId, 16)); handleNetworkChange(); } }, [ getCustodianAccounts, apiUrl, currentJwt, jwtList, selectedCustodianType, currentChainId, chainId, ]); const cancelConnectCustodianToken = () => { setSelectedCustodianName(''); setSelectedCustodianType(''); setSelectedCustodianImage(null); setSelectedCustodianDisplayName(''); setApiUrl(''); setCurrentJwt(''); setConnectError(''); setSelectError(''); }; const setSelectAllAccounts = (e) => { const allAccounts = {}; if (e.currentTarget.checked) { accounts.forEach((account) => { allAccounts[account.address] = { name: account.name, custodianDetails: account.custodianDetails, labels: account.labels, token: currentJwt, apiUrl, chainId: account.chainId, custodyType: selectedCustodianType, custodyName: selectedCustodianName, }; }); setSelectedAccounts(allAccounts); } else { setSelectedAccounts({}); } }; return ( <> {connectError && ( {connectError} )} {selectError && ( {selectError} )} {!accounts && !selectedCustodianType ? ( history.push(DEFAULT_ROUTE)} display={Display.Flex} /> {t('back')} {t('connectCustodialAccountTitle')} {t('connectCustodialAccountMsg')} ) : null} {!accounts && selectedCustodianType && ( <> cancelConnectCustodianToken()} display={[Display.Flex]} /> {t('back')} {selectedCustodianImage && ( {selectedCustodianDisplayName} )} {selectedCustodianDisplayName} {t('enterCustodianToken', [selectedCustodianDisplayName])} setCurrentJwt(jwt)} jwtInputText={t('pasteJWTToken')} apiUrl={apiUrl} urlInputText={t('custodyApiUrl', [ selectedCustodianDisplayName, ])} onUrlChange={(url) => setApiUrl(url)} /> )} {accounts && accounts.length > 0 && ( <> {t('selectAnAccount')} {t('selectAnAccountHelp')} setSelectAllAccounts(e)} checked={Object.keys(selectedAccounts).length === accounts.length} /> { if (selectedAccounts[account.address]) { delete selectedAccounts[account.address]; } else { selectedAccounts[account.address] = { name: account.name, custodianDetails: account.custodianDetails, labels: account.labels, token: currentJwt, apiUrl, chainId: account.chainId, custodyType: selectedCustodianType, custodyName: selectedCustodianName, }; } setSelectedAccounts(selectedAccounts); }} selectedAccounts={selectedAccounts} onAddAccounts={async () => { try { await dispatch( mmiActions.connectCustodyAddresses( selectedCustodianType, selectedCustodianName, selectedAccounts, ), ); const selectedCustodian = custodians.find( (custodian) => custodian.name === selectedCustodianName, ); history.push({ pathname: CUSTODY_ACCOUNT_DONE_ROUTE, state: { imgSrc: selectedCustodian.iconUrl, title: t('custodianAccountAddedTitle'), description: t('custodianAccountAddedDesc'), }, }); trackEvent({ category: 'MMI', event: 'Custodial accounts connected', properties: { custodian: selectedCustodianName, numberOfAccounts: Object.keys(selectedAccounts).length, chainId, }, }); } catch (e) { setSelectError(e.message); } }} onCancel={() => { setAccounts(null); setSelectedCustodianName(null); setSelectedCustodianType(null); setSelectedAccounts({}); setCurrentJwt(''); setApiUrl(''); setAddNewTokenClicked(false); if (Object.keys(connectRequest).length) { history.push(DEFAULT_ROUTE); } trackEvent({ category: 'MMI', event: 'Connect to custodian cancel', properties: { custodian: selectedCustodianName, numberOfAccounts: Object.keys(selectedAccounts).length, chainId, }, }); }} /> )} {accounts && accounts.length === 0 && ( {t('allCustodianAccountsConnectedTitle')} {t('allCustodianAccountsConnectedSubtitle')} )} ); }; export default CustodyPage;