import { useEffect, useReducer, useMemo, useContext, useCallback, useState, } from 'react'; import { TokenFilterTypes, IMTBLWidgetEvents, SwapWidgetParams, SwapDirection, WalletProviderName, SwapWidgetConfiguration, } from '@imtbl/checkout-sdk'; import { useTranslation } from 'react-i18next'; import { SwapCoins } from './views/SwapCoins'; import { LoadingView } from '../../views/loading/LoadingView'; import { SwapActions, SwapContext, initialSwapState, swapReducer, } from './context/SwapContext'; import { ErrorView as ErrorViewType, SharedViews, ViewActions, ViewContext, initialViewState, viewReducer, } from '../../context/view-context/ViewContext'; import { SwapSuccessView, SwapWidgetViews, } from '../../context/view-context/SwapViewContextTypes'; import { StrongCheckoutWidgetsConfig } from '../../lib/withDefaultWidgetConfig'; import { DEFAULT_BALANCE_RETRY_POLICY } from '../../lib'; import { StatusView } from '../../components/Status/StatusView'; import { StatusType } from '../../components/Status/StatusType'; import { ErrorView } from '../../views/error/ErrorView'; import { sendSwapFailedEvent, sendSwapRejectedEvent, sendSwapSuccessEvent, sendSwapWidgetCloseEvent, } from './SwapWidgetEvents'; import { SwapInProgress } from './views/SwapInProgress'; import { ApproveERC20Onboarding } from './views/ApproveERC20Onboarding'; import { TopUpView } from '../../views/top-up/TopUpView'; import { ConnectLoaderContext } from '../../context/connect-loader-context/ConnectLoaderContext'; import { EventTargetContext } from '../../context/event-target-context/EventTargetContext'; import { getAllowedBalances } from '../../lib/balance'; import { ServiceUnavailableErrorView } from '../../views/error/ServiceUnavailableErrorView'; import { ServiceUnavailableToRegionErrorView } from '../../views/error/ServiceUnavailableToRegionErrorView'; import { ServiceType } from '../../views/error/serviceTypes'; import { isPassportProvider } from '../../lib/provider'; import { topUpBridgeOption, topUpOnRampOption } from './helpers'; import { GeoblockLoader } from './GeoblockLoader'; export type SwapWidgetInputs = SwapWidgetParams & { config: StrongCheckoutWidgetsConfig; swapConfig: SwapWidgetConfiguration; }; export default function SwapWidget({ amount, fromTokenAddress, toTokenAddress, config, autoProceed, direction, showBackButton, swapConfig: { customTitle, customSubTitle, showHeader }, walletProviderName, }: SwapWidgetInputs) { const { t } = useTranslation(); const { eventTargetState: { eventTarget }, } = useContext(EventTargetContext); const { theme, themeOverrides, isOnRampEnabled, isSwapEnabled, isBridgeEnabled, } = config; const { connectLoaderState: { checkout, provider }, } = useContext(ConnectLoaderContext); const [viewState, viewDispatch] = useReducer(viewReducer, { ...initialViewState, history: [], }); const [swapState, swapDispatch] = useReducer(swapReducer, initialSwapState); const [errorViewLoading, setErrorViewLoading] = useState(false); const swapReducerValues = useMemo( () => ({ swapState, swapDispatch }), [swapState, swapDispatch], ); const viewReducerValues = useMemo( () => ({ viewState, viewDispatch }), [viewState, viewDispatch], ); const showErrorView = useCallback( (error: any, tryAgain?: () => Promise) => { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SharedViews.ERROR_VIEW, tryAgain, error, }, }, }); }, [viewDispatch], ); const showSwapView = useCallback(() => { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SwapWidgetViews.SWAP }, }, }); }, [viewDispatch]); const loadBalances = useCallback(async (): Promise => { if (!checkout) throw new Error('loadBalances: missing checkout'); if (!provider) throw new Error('loadBalances: missing provider'); try { const tokensAndBalances = await getAllowedBalances({ checkout, provider, allowTokenListType: TokenFilterTypes.SWAP, }); // Why? Check getAllowedBalances if (tokensAndBalances === undefined) return false; swapDispatch({ payload: { type: SwapActions.SET_ALLOWED_TOKENS, allowedTokens: tokensAndBalances.allowList.tokens, }, }); swapDispatch({ payload: { type: SwapActions.SET_TOKEN_BALANCES, tokenBalances: tokensAndBalances.allowedBalances, }, }); } catch (err: any) { if (DEFAULT_BALANCE_RETRY_POLICY.nonRetryable!(err)) { showErrorView(err, loadBalances); return false; } } return true; }, [checkout, provider]); useEffect(() => { (async () => { if (!checkout || !provider) return; const network = await checkout.getNetworkInfo({ provider }); // If the provider's network is not the correct network, return out of this and let the // connect loader handle the switch network functionality if (Number(network.chainId) !== checkout.config.l2ChainId) return; swapDispatch({ payload: { type: SwapActions.SET_NETWORK, network, }, }); if (!(await loadBalances())) return; if (viewState.view.type === SharedViews.LOADING_VIEW) { showSwapView(); } })(); }, [checkout, provider]); useEffect(() => { swapDispatch({ payload: { type: SwapActions.SET_AUTO_PROCEED, autoProceed: autoProceed ?? false, direction: direction ?? SwapDirection.FROM, }, }); }, [autoProceed, direction]); const cancelAutoProceed = useCallback(() => { if (autoProceed) { swapDispatch({ payload: { type: SwapActions.SET_AUTO_PROCEED, autoProceed: false, direction: SwapDirection.FROM, }, }); } }, [autoProceed, swapDispatch]); const fromAmount = direction === SwapDirection.FROM || direction == null ? amount : undefined; const toAmount = direction === SwapDirection.TO ? amount : undefined; const topUpOptions: { textKey: string; action: () => void }[] | undefined = useMemo(() => { const optionsArray: { textKey: string; action: () => void }[] = []; const isNotPassport = !isPassportProvider(provider) || walletProviderName !== WalletProviderName.PASSPORT; const isOnramp = topUpOnRampOption(isOnRampEnabled); if (isOnramp) { optionsArray.push({ ...isOnramp }); } const isBridge = topUpBridgeOption( isBridgeEnabled, isNotPassport, ); if (isBridge) { optionsArray.push({ ...isBridge }); } return optionsArray; }, []); return ( {viewState.view.type === SharedViews.LOADING_VIEW && ( )} {viewState.view.type === SwapWidgetViews.SWAP && ( )} {viewState.view.type === SwapWidgetViews.IN_PROGRESS && ( )} {viewState.view.type === SwapWidgetViews.APPROVE_ERC20 && ( )} {viewState.view.type === SwapWidgetViews.SUCCESS && ( { sendSwapSuccessEvent( eventTarget, (viewState.view as SwapSuccessView).data.transactionHash, ); }} onActionClick={() => sendSwapWidgetCloseEvent(eventTarget)} statusType={StatusType.SUCCESS} testId="success-view" /> )} {viewState.view.type === SwapWidgetViews.FAIL && ( { sendSwapFailedEvent(eventTarget, 'Transaction failed'); }} onActionClick={() => { if (viewState.view.type === SwapWidgetViews.FAIL) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SwapWidgetViews.SWAP, data: viewState.view.data, }, }, }); } }} statusType={StatusType.FAILURE} onCloseClick={() => sendSwapWidgetCloseEvent(eventTarget)} testId="fail-view" /> )} {viewState.view.type === SwapWidgetViews.PRICE_SURGE && ( { sendSwapRejectedEvent(eventTarget, 'Price surge'); }} onActionClick={() => { if (viewState.view.type === SwapWidgetViews.PRICE_SURGE) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SwapWidgetViews.SWAP, data: viewState.view.data, }, }, }); } }} statusType={StatusType.WARNING} onCloseClick={() => sendSwapWidgetCloseEvent(eventTarget)} testId="price-surge-view" /> )} {viewState.view.type === SharedViews.ERROR_VIEW && ( { setErrorViewLoading(true); const data = viewState.view as ErrorViewType; if (!data.tryAgain) { showSwapView(); setErrorViewLoading(false); return; } if (await data.tryAgain()) showSwapView(); setErrorViewLoading(false); }} onCloseClick={() => sendSwapWidgetCloseEvent(eventTarget)} errorEventActionLoading={errorViewLoading} /> )} {viewState.view.type === SwapWidgetViews.SERVICE_UNAVAILABLE && ( sendSwapWidgetCloseEvent(eventTarget)} onBackButtonClick={() => { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SwapWidgetViews.SWAP } }, }); }} /> )} {viewState.view.type === SharedViews.TOP_UP_VIEW && ( sendSwapWidgetCloseEvent(eventTarget)} /> )} ) } serviceUnavailableView={ ( sendSwapWidgetCloseEvent(window)} showHeader={showHeader} primaryActionText={ topUpOptions && topUpOptions?.length > 0 ? t(topUpOptions[0].textKey) : undefined } onPrimaryButtonClick={ topUpOptions && topUpOptions?.length > 0 ? topUpOptions[0].action : undefined } secondaryActionText={ topUpOptions?.length === 2 ? t(topUpOptions[1].textKey) : undefined } onSecondaryButtonClick={ topUpOptions?.length === 2 ? topUpOptions[1].action : undefined } /> ) } /> ); }