import { Body, ButtCon, Button, FramedIcon, HeroFormControl, HeroTextInput, MenuItem, Stack, } from '@biom3/react'; import debounce from 'lodash.debounce'; import { ChainId, type Checkout, EIP6963ProviderInfo, IMTBLWidgetEvents, TokenFilterTypes, type TokenInfo, WalletProviderRdns, WrappedBrowserProvider, } from '@imtbl/checkout-sdk'; import { type ChangeEvent, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import { useTranslation } from 'react-i18next'; import { parseUnits } from 'ethers/utils'; import { SimpleLayout } from '../../../components/SimpleLayout/SimpleLayout'; import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext'; import { SharedViews, ViewActions, ViewContext, } from '../../../context/view-context/ViewContext'; import { orchestrationEvents } from '../../../lib/orchestrationEvents'; import { AddTokensActions, AddTokensContext, } from '../context/AddTokensContext'; import type { StrongCheckoutWidgetsConfig } from '../../../lib/withDefaultWidgetConfig'; import { useRoutes } from '../../../lib/squid/hooks/useRoutes'; import { AddTokensWidgetViews } from '../../../context/view-context/AddTokensViewContextTypes'; import { AddTokensErrorTypes } from '../types'; import { DeliverToWalletDrawer } from '../../../components/WalletDrawer/DeliverToWalletDrawer'; import { PayWithWalletDrawer } from '../../../components/WalletDrawer/PayWithWalletDrawer'; import { useInjectedProviders } from '../../../lib/hooks/useInjectedProviders'; import { getProviderSlugFromRdns } from '../../../lib/provider'; import { useProvidersContext } from '../../../context/providers-context/ProvidersContext'; import { sendConnectProviderSuccessEvent } from '../AddTokensWidgetEvents'; import { convertToUsd } from '../../../lib/squid/functions/convertToUsd'; import { validateToAmount } from '../functions/amountValidation'; import { OnboardingDrawer } from '../components/OnboardingDrawer'; import { useError } from '../hooks/useError'; import { SquidFooter } from '../../../lib/squid/components/SquidFooter'; import { TokenDrawerMenu } from '../components/TokenDrawerMenu'; import { PULSE_SHADOW } from '../utils/animation'; import { RouteData } from '../../../lib/squid/types'; import { SQUID_NATIVE_TOKEN, TOOLKIT_SQUID_URL } from '../../../lib/squid/config'; import { NotEnoughGasDrawer } from '../../../components/NotEnoughGasDrawer/NotEnoughGasDrawer'; import { SelectedWallet } from '../../../components/SelectedWallet/SelectedWallet'; import { RouteOptionsDrawer } from '../../../components/RouteOptionsDrawer/RouteOptionsDrawer'; import { SelectedRouteOption } from '../../../components/SelectedRouteOption/SelectedRouteOption'; import { getFormattedAmounts } from '../../../functions/getFormattedNumber'; import { checkSanctionedAddresses } from '../../../functions/checkSanctionedAddresses'; interface AddTokensProps { checkout: Checkout; showBackButton?: boolean; showOnrampOption?: boolean; showSwapOption?: boolean; showBridgeOption?: boolean; toTokenAddress?: string; toAmount?: string; onCloseButtonClick?: () => void; onBackButtonClick?: () => void; config: StrongCheckoutWidgetsConfig; } export function AddTokens({ checkout, toAmount, config, toTokenAddress, showOnrampOption = true, showSwapOption = true, showBridgeOption = true, onCloseButtonClick, showBackButton, onBackButtonClick, }: AddTokensProps) { const inputRef = useRef(null); const { fetchRoutes } = useRoutes(); const { showErrorHandover } = useError(config.environment); const { addTokensState, addTokensDispatch, } = useContext(AddTokensContext); const { id, squid, chains, balances, tokens, selectedAmount, routes, selectedRouteData, selectedToken, isSwapAvailable, } = addTokensState; const { viewDispatch } = useContext(ViewContext); const { t } = useTranslation(); const { eventTargetState: { eventTarget }, } = useContext(EventTargetContext); const [payWithCardClicked, setPayWithCardClicked] = useState(false); const [showOptionsDrawer, setShowOptionsDrawer] = useState(false); const [showPayWithDrawer, setShowPayWithDrawer] = useState(false); const [showDeliverToDrawer, setShowDeliverToDrawer] = useState(false); const [onRampAllowedTokens, setOnRampAllowedTokens] = useState( [], ); const [inputValue, setInputValue] = useState( selectedAmount || toAmount || '', ); const [fetchingRoutes, setFetchingRoutes] = useState(false); const [insufficientBalance, setInsufficientBalance] = useState(false); const [isAmountInputSynced, setIsAmountInputSynced] = useState(false); const [showNotEnoughGasDrawer, setShowNotEnoughGasDrawer] = useState(false); const debouncedSetSelectedAmount = useRef( debounce((value: string) => { addTokensDispatch({ payload: { type: AddTokensActions.SET_SELECTED_AMOUNT, selectedAmount: value, }, }); }, 2500), ); const selectedAmountUsd = useMemo( () => convertToUsd(tokens, inputValue, selectedToken), [tokens, inputValue, selectedToken], ); const setSelectedAmount = (value: string) => { setIsAmountInputSynced(false); debouncedSetSelectedAmount.current(value); }; const resetRoutes = () => { addTokensDispatch({ payload: { type: AddTokensActions.SET_ROUTES, routes: [], }, }); }; useEffect(() => { if (selectedAmount === inputValue) { setIsAmountInputSynced(true); } }, [selectedAmount, inputValue]); const setSelectedRouteData = (route: RouteData | undefined) => { addTokensDispatch({ payload: { type: AddTokensActions.SET_SELECTED_ROUTE_DATA, selectedRouteData: route, }, }); }; const handleOnAmountInputChange = (event: ChangeEvent) => { const { value, amount, isValid } = validateToAmount(event.target.value); if (isValid || amount === 0 || value === '') { setInputValue(value); if (amount > 0) { setSelectedAmount(value); } else { setSelectedAmount(''); } } }; const { providersState: { fromProviderInfo, toProviderInfo, toProvider, fromAddress, toAddress, lockedToProvider, }, } = useProvidersContext(); const { providers } = useInjectedProviders({ checkout }); const walletOptions = useMemo( () => providers // TODO: Check if must filter passport on L1 .map((detail) => { if (detail.info.rdns === WalletProviderRdns.PASSPORT) { return { ...detail, info: { ...detail.info, name: getProviderSlugFromRdns(detail.info.rdns).replace( /^\w/, (c) => c.toUpperCase(), ), }, }; } return detail; }), [providers], ); useEffect(() => { if (!lockedToProvider) { return; } (async () => { })(); }, [toProvider, lockedToProvider]); const toChain = useMemo( () => chains?.find((chain) => chain.id === ChainId.IMTBL_ZKEVM_MAINNET.toString()), [chains], ); useEffect(() => { }, [id, isSwapAvailable]); useEffect(() => { if (!toAmount) return; setSelectedAmount(toAmount); }, [toAmount]); useEffect(() => { resetRoutes(); setInsufficientBalance(false); setSelectedRouteData(undefined); }, [fromAddress]); useEffect(() => { resetRoutes(); setInsufficientBalance(false); setSelectedRouteData(undefined); (async () => { const isValidAmount = validateToAmount(selectedAmount).isValid; if ( balances && squid && tokens && selectedToken?.address && isValidAmount ) { setFetchingRoutes(true); try { const availableRoutes = await fetchRoutes( squid, tokens, balances, ChainId.IMTBL_ZKEVM_MAINNET.toString(), selectedToken.address === 'native' ? SQUID_NATIVE_TOKEN : selectedToken.address, selectedAmount, 5, 1000, isSwapAvailable, ); addTokensDispatch({ payload: { type: AddTokensActions.SET_ROUTES, routes: availableRoutes, }, }); if (availableRoutes.length === 0) { setInsufficientBalance(true); } } catch { // Route errors are reflected by the empty routes state. } setFetchingRoutes(false); } })(); }, [balances, squid, selectedToken, selectedAmount]); useEffect(() => { if (!selectedRouteData && routes.length > 0) { setSelectedRouteData(routes[0]); } }, [routes]); useEffect(() => { if (!checkout) return; const fetchOnRampTokens = async () => { try { const tokenResponse = await checkout.getTokenAllowList({ type: TokenFilterTypes.ONRAMP, chainId: checkout.config.l2ChainId, }); if (tokenResponse?.tokens.length > 0) { setOnRampAllowedTokens(tokenResponse.tokens); } } catch (error) { showErrorHandover(AddTokensErrorTypes.SERVICE_BREAKDOWN); } }; fetchOnRampTokens(); }, [checkout, id]); const sendRequestOnRampEvent = async () => { const data = { tokenAddress: selectedToken?.address ?? '', amount: selectedAmount ?? '', showBackButton: true, provider: toProvider, }; orchestrationEvents.sendRequestOnrampEvent( eventTarget, IMTBLWidgetEvents.IMTBL_ADD_TOKENS_WIDGET_EVENT, data, ); }; const handleCardClick = () => { setPayWithCardClicked(true); if (!toProvider) { setShowDeliverToDrawer(true); return; } sendRequestOnRampEvent(); }; useEffect(() => { if (toProvider && payWithCardClicked) { sendRequestOnRampEvent(); } }, [toProvider, payWithCardClicked]); const handleRouteClick = (route: RouteData) => { setShowOptionsDrawer(false); setShowPayWithDrawer(false); setShowDeliverToDrawer(false); setSelectedRouteData(route); }; const handleDeliverToClose = (connectedToAddress?: string) => { if (!connectedToAddress) { setPayWithCardClicked(false); } setShowDeliverToDrawer(false); }; const handleReviewClick = async () => { if (!selectedRouteData || !selectedToken?.address) return; if ( fromAddress && toAddress && (await checkSanctionedAddresses( [fromAddress, toAddress], { address: selectedToken.address, amount: parseUnits(selectedAmount, selectedToken.decimals) }, checkout.config, )) ) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SharedViews.SERVICE_UNAVAILABLE_ERROR_VIEW, error: new Error('Sanctioned address'), }, }, }); return; } viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: AddTokensWidgetViews.REVIEW, data: { balance: selectedRouteData.amountData.balance, toChainId: ChainId.IMTBL_ZKEVM_MAINNET.toString(), toTokenAddress: selectedToken.address, toAmount: selectedAmount, additionalBuffer: selectedRouteData.amountData.additionalBuffer, }, }, }, }); }; const shouldShowOnRampOption = useMemo(() => { if (showOnrampOption && selectedToken) { const isAllowedToken = onRampAllowedTokens.find( (token) => token.address?.toLowerCase() === selectedToken.address?.toLowerCase(), ); return !!isAllowedToken; } return false; }, [selectedToken, onRampAllowedTokens, showOnrampOption]); const showInitialEmptyState = !selectedToken; useEffect(() => { if (inputRef.current && !showInitialEmptyState) { inputRef.current.focus(); } }, [showInitialEmptyState]); const shouldShowBackButton = showBackButton && onBackButtonClick; const routeInputsReady = !!selectedToken && !!fromAddress && validateToAmount(selectedAmount).isValid && validateToAmount(inputValue).isValid && isAmountInputSynced; const loading = (routeInputsReady || fetchingRoutes) && !(selectedRouteData || insufficientBalance); const readyToReview = routeInputsReady && !!toAddress && !!selectedRouteData && !selectedRouteData.isInsufficientGas && !loading; const handleWalletConnected = ( providerType: 'from' | 'to', provider: WrappedBrowserProvider, providerInfo: EIP6963ProviderInfo, ) => { sendConnectProviderSuccessEvent( eventTarget, providerType, provider, providerInfo, ); }; const getChainInfo = () => { if (toChain) { return { iconUrl: toChain.iconUrl, name: toChain.name, }; } return undefined; }; useEffect(() => { if (selectedRouteData?.isInsufficientGas) { setShowNotEnoughGasDrawer(true); } else { setShowNotEnoughGasDrawer(false); } }, [selectedRouteData]); const handleToolkitClick = () => { setShowNotEnoughGasDrawer(false); window.open(TOOLKIT_SQUID_URL, '_blank'); }; return ( {shouldShowBackButton && ( )} )} > {showInitialEmptyState ? ( {t('views.ADD_TOKENS.tokenSelection.buttonText')} ) : ( {t('views.ADD_TOKENS.tokenSelection.tokenLabel')} {' '} {selectedToken.symbol} handleOnAmountInputChange(event)} placeholder="0" maxTextSize="xLarge" /> {`${t('views.ADD_TOKENS.fees.fiatPricePrefix')} $${getFormattedAmounts(selectedAmountUsd)}`} )} { event.stopPropagation(); setShowPayWithDrawer(true); }} > {selectedToken && fromAddress && selectedAmount && isAmountInputSynced && ( <> setShowOptionsDrawer(true)} withSelectedWallet={!!fromAddress} insufficientBalance={insufficientBalance} showOnrampOption={shouldShowOnRampOption} /> )} setShowDeliverToDrawer(true)} disabled={lockedToProvider} /> setShowPayWithDrawer(false)} onPayWithCard={handleCardClick} onConnect={handleWalletConnected} insufficientBalance={insufficientBalance} showOnRampOption={shouldShowOnRampOption || !selectedToken} drawerBackground={config.themeOverrides?.drawerBackground} /> setShowOptionsDrawer(false)} onCardClick={handleCardClick} onRouteClick={handleRouteClick} insufficientBalance={insufficientBalance} /> setShowNotEnoughGasDrawer(false)} onToolkitClick={handleToolkitClick} /> ); }