import React, { useEffect, useRef, useContext, useState, useCallback, } from 'react'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { Switch, Route, useLocation, useHistory, Redirect, } from 'react-router-dom'; import { shuffle, isEqual } from 'lodash'; import classnames from 'classnames'; import { I18nContext } from '../../contexts/i18n'; import { getSelectedAccount, getCurrentChainId, getIsSwapsChain, isHardwareWallet, getHardwareWalletType, getTokenList, } from '../../selectors/selectors'; import { getQuotes, clearSwapsState, getTradeTxId, getApproveTxId, getFetchingQuotes, setTopAssets, getFetchParams, setAggregatorMetadata, getAggregatorMetadata, getBackgroundSwapRouteState, getSwapsErrorKey, getSwapsFeatureIsLive, prepareToLeaveSwaps, fetchAndSetSwapsGasPriceInfo, fetchSwapsLivenessAndFeatureFlags, getReviewSwapClickedTimestamp, getPendingSmartTransactions, getSmartTransactionsOptInStatus, getSmartTransactionsEnabled, getCurrentSmartTransactionsEnabled, getCurrentSmartTransactionsError, navigateBackToBuildQuote, getSwapRedesignEnabled, setTransactionSettingsOpened, } from '../../ducks/swaps/swaps'; import { checkNetworkAndAccountSupports1559, currentNetworkTxListSelector, } from '../../selectors'; import { AWAITING_SIGNATURES_ROUTE, AWAITING_SWAP_ROUTE, SMART_TRANSACTION_STATUS_ROUTE, BUILD_QUOTE_ROUTE, VIEW_QUOTE_ROUTE, LOADING_QUOTES_ROUTE, SWAPS_ERROR_ROUTE, DEFAULT_ROUTE, SWAPS_MAINTENANCE_ROUTE, PREPARE_SWAP_ROUTE, SWAPS_NOTIFICATION_ROUTE, } from '../../helpers/constants/routes'; import { ERROR_FETCHING_QUOTES, QUOTES_NOT_AVAILABLE_ERROR, SWAP_FAILED_ERROR, CONTRACT_DATA_DISABLED_ERROR, OFFLINE_FOR_MAINTENANCE, } from '../../../shared/constants/swaps'; import { resetBackgroundSwapsState, setSwapsTokens, setBackgroundSwapRouteState, setSwapsErrorKey, } from '../../store/actions'; import { useGasFeeEstimates } from '../../hooks/useGasFeeEstimates'; import FeatureToggledRoute from '../../helpers/higher-order-components/feature-toggled-route'; import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics'; import { TransactionStatus } from '../../../shared/constants/transaction'; import { MetaMetricsContext } from '../../contexts/metametrics'; import { getSwapsTokensReceivedFromTxMeta } from '../../../shared/lib/transactions-controller-utils'; import { Icon, IconName, IconSize } from '../../components/component-library'; import Box from '../../components/ui/box'; import { DISPLAY, JustifyContent, IconColor, FRACTIONS, } from '../../helpers/constants/design-system'; import { fetchTokens, fetchTopAssets, fetchAggregatorMetadata, } from './swaps.util'; import AwaitingSignatures from './awaiting-signatures'; import SmartTransactionStatus from './smart-transaction-status'; import AwaitingSwap from './awaiting-swap'; import LoadingQuote from './loading-swaps-quotes'; import BuildQuote from './build-quote'; import PrepareSwapPage from './prepare-swap-page/prepare-swap-page'; import NotificationPage from './notification-page/notification-page'; import ViewQuote from './view-quote'; export default function Swap() { const t = useContext(I18nContext); const history = useHistory(); const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); const { pathname } = useLocation(); const isAwaitingSwapRoute = pathname === AWAITING_SWAP_ROUTE; const isAwaitingSignaturesRoute = pathname === AWAITING_SIGNATURES_ROUTE; const isSwapsErrorRoute = pathname === SWAPS_ERROR_ROUTE; const isLoadingQuotesRoute = pathname === LOADING_QUOTES_ROUTE; const isSmartTransactionStatusRoute = pathname === SMART_TRANSACTION_STATUS_ROUTE; const isViewQuoteRoute = pathname === VIEW_QUOTE_ROUTE; const isPrepareSwapRoute = pathname === PREPARE_SWAP_ROUTE; const [currentStxErrorTracked, setCurrentStxErrorTracked] = useState(false); const fetchParams = useSelector(getFetchParams, isEqual); const { destinationTokenInfo = {} } = fetchParams?.metaData || {}; const routeState = useSelector(getBackgroundSwapRouteState); const selectedAccount = useSelector(getSelectedAccount, shallowEqual); const quotes = useSelector(getQuotes, isEqual); const txList = useSelector(currentNetworkTxListSelector, shallowEqual); const tradeTxId = useSelector(getTradeTxId); const approveTxId = useSelector(getApproveTxId); const aggregatorMetadata = useSelector(getAggregatorMetadata, shallowEqual); const fetchingQuotes = useSelector(getFetchingQuotes); let swapsErrorKey = useSelector(getSwapsErrorKey); const swapsEnabled = useSelector(getSwapsFeatureIsLive); const chainId = useSelector(getCurrentChainId); const isSwapsChain = useSelector(getIsSwapsChain); const networkAndAccountSupports1559 = useSelector( checkNetworkAndAccountSupports1559, ); const tokenList = useSelector(getTokenList, isEqual); const shuffledTokensList = shuffle(Object.values(tokenList)); const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); const pendingSmartTransactions = useSelector(getPendingSmartTransactions); const reviewSwapClicked = Boolean(reviewSwapClickedTimestamp); const smartTransactionsOptInStatus = useSelector( getSmartTransactionsOptInStatus, ); const smartTransactionsEnabled = useSelector(getSmartTransactionsEnabled); const currentSmartTransactionsEnabled = useSelector( getCurrentSmartTransactionsEnabled, ); const swapRedesignEnabled = useSelector(getSwapRedesignEnabled); const currentSmartTransactionsError = useSelector( getCurrentSmartTransactionsError, ); useEffect(() => { const leaveSwaps = async () => { await dispatch(prepareToLeaveSwaps()); // We need to wait until "prepareToLeaveSwaps" is done, because otherwise // a user would be redirected from DEFAULT_ROUTE back to Swaps. history.push(DEFAULT_ROUTE); }; if (!isSwapsChain) { leaveSwaps(); } }, [isSwapsChain, dispatch, history]); // This will pre-load gas fees before going to the View Quote page. useGasFeeEstimates(); const { balance: ethBalance, address: selectedAccountAddress } = selectedAccount; const approveTxData = approveTxId && txList.find(({ id }) => approveTxId === id); const tradeTxData = tradeTxId && txList.find(({ id }) => tradeTxId === id); const tokensReceived = tradeTxData?.txReceipt && getSwapsTokensReceivedFromTxMeta( destinationTokenInfo?.symbol, tradeTxData, destinationTokenInfo?.address, selectedAccountAddress, destinationTokenInfo?.decimals, approveTxData, chainId, ); const tradeConfirmed = tradeTxData?.status === TransactionStatus.confirmed; const approveError = approveTxData?.status === TransactionStatus.failed || approveTxData?.txReceipt?.status === '0x0'; const tradeError = tradeTxData?.status === TransactionStatus.failed || tradeTxData?.txReceipt?.status === '0x0'; const conversionError = approveError || tradeError; if (conversionError && swapsErrorKey !== CONTRACT_DATA_DISABLED_ERROR) { swapsErrorKey = SWAP_FAILED_ERROR; } // eslint-disable-next-line useEffect(() => { if (!isSwapsChain) { return undefined; } fetchTokens(chainId) .then((tokens) => { dispatch(setSwapsTokens(tokens)); }) .catch((error) => console.error(error)); fetchTopAssets(chainId).then((topAssets) => { dispatch(setTopAssets(topAssets)); }); fetchAggregatorMetadata(chainId).then((newAggregatorMetadata) => { dispatch(setAggregatorMetadata(newAggregatorMetadata)); }); if (!networkAndAccountSupports1559) { dispatch(fetchAndSetSwapsGasPriceInfo(chainId)); } return () => { dispatch(prepareToLeaveSwaps()); }; }, [dispatch, chainId, networkAndAccountSupports1559, isSwapsChain]); const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); const trackExitedSwapsEvent = () => { trackEvent({ event: 'Exited Swaps', category: MetaMetricsEventCategory.Swaps, sensitiveProperties: { token_from: fetchParams?.sourceTokenInfo?.symbol, token_from_amount: fetchParams?.value, request_type: fetchParams?.balanceError, token_to: fetchParams?.destinationTokenInfo?.symbol, slippage: fetchParams?.slippage, custom_slippage: fetchParams?.slippage !== 2, current_screen: pathname.match(/\/swaps\/(.+)/u)[1], is_hardware_wallet: hardwareWalletUsed, hardware_wallet_type: hardwareWalletType, stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, }, }); }; const exitEventRef = useRef(); useEffect(() => { exitEventRef.current = () => { trackExitedSwapsEvent(); }; }); useEffect(() => { const fetchSwapsLivenessAndFeatureFlagsWrapper = async () => { await dispatch(fetchSwapsLivenessAndFeatureFlags()); }; fetchSwapsLivenessAndFeatureFlagsWrapper(); return () => { exitEventRef.current(); }; }, [dispatch]); useEffect(() => { // If there is a swapsErrorKey and reviewSwapClicked is false, there was an error in silent quotes prefetching // and we don't want to show the error page in that case, because another API call for quotes can be successful. if (swapsErrorKey && !isSwapsErrorRoute && reviewSwapClicked) { history.push(SWAPS_ERROR_ROUTE); } }, [history, swapsErrorKey, isSwapsErrorRoute, reviewSwapClicked]); const beforeUnloadEventAddedRef = useRef(); useEffect(() => { const fn = () => { if (isLoadingQuotesRoute) { dispatch(prepareToLeaveSwaps()); } return null; }; if (isLoadingQuotesRoute && !beforeUnloadEventAddedRef.current) { beforeUnloadEventAddedRef.current = true; window.addEventListener('beforeunload', fn); } return () => window.removeEventListener('beforeunload', fn); }, [dispatch, isLoadingQuotesRoute]); const trackErrorStxEvent = useCallback(() => { trackEvent({ event: 'Error Smart Transactions', category: MetaMetricsEventCategory.Swaps, sensitiveProperties: { token_from: fetchParams?.sourceTokenInfo?.symbol, token_from_amount: fetchParams?.value, request_type: fetchParams?.balanceError, token_to: fetchParams?.destinationTokenInfo?.symbol, slippage: fetchParams?.slippage, custom_slippage: fetchParams?.slippage !== 2, current_screen: pathname.match(/\/swaps\/(.+)/u)[1], is_hardware_wallet: hardwareWalletUsed, hardware_wallet_type: hardwareWalletType, stx_enabled: smartTransactionsEnabled, current_stx_enabled: currentSmartTransactionsEnabled, stx_user_opt_in: smartTransactionsOptInStatus, stx_error: currentSmartTransactionsError, }, }); }, [ currentSmartTransactionsError, currentSmartTransactionsEnabled, trackEvent, fetchParams?.balanceError, fetchParams?.destinationTokenInfo?.symbol, fetchParams?.slippage, fetchParams?.sourceTokenInfo?.symbol, fetchParams?.value, hardwareWalletType, hardwareWalletUsed, pathname, smartTransactionsEnabled, smartTransactionsOptInStatus, ]); useEffect(() => { if (currentSmartTransactionsError && !currentStxErrorTracked) { setCurrentStxErrorTracked(true); trackErrorStxEvent(); } }, [ currentSmartTransactionsError, trackErrorStxEvent, currentStxErrorTracked, ]); if (!isSwapsChain) { // A user is being redirected outside of Swaps via the async "leaveSwaps" function above. In the meantime // we have to prevent the code below this condition, which wouldn't work on an unsupported chain. return <>; } const redirectToDefaultRoute = async () => { history.push({ pathname: DEFAULT_ROUTE, state: { stayOnHomePage: true }, }); dispatch(clearSwapsState()); await dispatch(resetBackgroundSwapsState()); }; return (
{!swapRedesignEnabled && (
{ await dispatch(navigateBackToBuildQuote(history)); }} > {isViewQuoteRoute && t('edit')}
)} {swapRedesignEnabled && ( { if (e.key === 'Enter') { redirectToDefaultRoute(); } }} > {!isAwaitingSwapRoute && !isAwaitingSignaturesRoute && !isSmartTransactionStatusRoute && ( )} )}
{t('swap')}
{!swapRedesignEnabled && (
{ dispatch(clearSwapsState()); await dispatch(resetBackgroundSwapsState()); history.push(DEFAULT_ROUTE); }} > {!isAwaitingSwapRoute && !isAwaitingSignaturesRoute && !isSmartTransactionStatusRoute && t('cancel')}
)} {swapRedesignEnabled && ( { if (e.key === 'Enter') { dispatch(setTransactionSettingsOpened(true)); } }} > {isPrepareSwapRoute && ( { dispatch(setTransactionSettingsOpened(true)); }} style={{ cursor: 'pointer' }} title={t('transactionSettings')} /> )} )}
{ if (swapRedesignEnabled) { return ; } if (tradeTxData && !conversionError) { return ; } else if (tradeTxData && routeState) { return ; } else if (routeState === 'loading' && aggregatorMetadata) { return ; } return ( ); }} /> { if (!swapRedesignEnabled) { return ; } return ( ); }} /> { if ( pendingSmartTransactions.length > 0 && routeState === 'smartTransactionStatus' ) { return ( ); } if (swapRedesignEnabled) { return ; } if (Object.values(quotes).length) { return ( ); } else if (fetchParams) { return ; } return ; }} /> { if (swapsErrorKey) { return ( ); } return ; }} /> { if (!swapsErrorKey) { return ; } return ; }} /> { return aggregatorMetadata ? ( { await dispatch(setBackgroundSwapRouteState('')); if ( swapsErrorKey === ERROR_FETCHING_QUOTES || swapsErrorKey === QUOTES_NOT_AVAILABLE_ERROR ) { dispatch(setSwapsErrorKey(QUOTES_NOT_AVAILABLE_ERROR)); history.push(SWAPS_ERROR_ROUTE); } else { history.push(VIEW_QUOTE_ROUTE); } }} aggregatorMetadata={aggregatorMetadata} /> ) : ( ); }} /> { return swapsEnabled === false ? ( ) : ( ); }} /> { return ; }} /> { return ; }} /> { return routeState === 'awaiting' || tradeTxData ? ( ) : ( ); }} />
); }