import { Box } from '@biom3/react'; import { useCallback, useContext, useMemo, useState, } from 'react'; import { CheckoutError, CheckoutErrorType, TokenInfo } from '@imtbl/checkout-sdk'; import { useTranslation } from 'react-i18next'; import { TransactionRequest } from 'ethers'; import { SimpleLayout } from '../../../components/SimpleLayout/SimpleLayout'; import { HeaderNavigation } from '../../../components/Header/HeaderNavigation'; import { sendSwapWidgetCloseEvent } from '../SwapWidgetEvents'; import { FooterButton } from '../../../components/Footer/FooterButton'; import { ApproveERC20SwapData, PrefilledSwapForm, SwapWidgetViews, } from '../../../context/view-context/SwapViewContextTypes'; import { SimpleTextBody } from '../../../components/Body/SimpleTextBody'; import { SwapContext } from '../context/SwapContext'; import { SharedViews, ViewActions, ViewContext } from '../../../context/view-context/ViewContext'; import { LoadingView } from '../../../views/loading/LoadingView'; import { ConnectLoaderContext } from '../../../context/connect-loader-context/ConnectLoaderContext'; import { SpendingCapHero } from '../../../components/Hero/SpendingCapHero'; import { WalletApproveHero } from '../../../components/Hero/WalletApproveHero'; import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext'; import { isPassportProvider } from '../../../lib/provider'; export interface ApproveERC20Props { data: ApproveERC20SwapData; } export function ApproveERC20Onboarding({ data }: ApproveERC20Props) { const { t } = useTranslation(); const { swapState: { allowedTokens } } = useContext(SwapContext); const { connectLoaderState } = useContext(ConnectLoaderContext); const { checkout, provider } = connectLoaderState; const { viewDispatch } = useContext(ViewContext); const { eventTargetState: { eventTarget } } = useContext(EventTargetContext); const isPassport = isPassportProvider(provider); const noApprovalTransaction = data.approveTransaction === undefined; // Local state const [actionDisabled, setActionDisabled] = useState(false); const [approvalTxnLoading, setApprovalTxnLoading] = useState(false); const [showSwapTxnStep, setShowSwapTxnStep] = useState(noApprovalTransaction); const [loading, setLoading] = useState(false); // reject transaction flags const [rejectedSpending, setRejectedSpending] = useState(false); const [rejectedSwap, setRejectedSwap] = useState(false); // Get symbol from swap info for approve amount text const fromToken = useMemo( () => allowedTokens.find( (token: TokenInfo) => token.address === data.swapFormInfo.fromTokenAddress, ), [allowedTokens, data.swapFormInfo.fromTokenAddress], ); // Common error view function const showErrorView = useCallback(() => { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SharedViews.ERROR_VIEW, error: new Error('No checkout object or no provider object found'), }, }, }); }, [viewDispatch]); const goBackWithSwapData = useCallback(() => { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SwapWidgetViews.SWAP, data: data.swapFormInfo as PrefilledSwapForm, }, }, }); }, [viewDispatch]); const handleExceptions = (err: any, swapFormData: PrefilledSwapForm) => { if (err instanceof CheckoutError && err.type === CheckoutErrorType.TRANSACTION_FAILED && err.message.startsWith('execution reverted: "STF"')) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SwapWidgetViews.PRICE_SURGE, data: swapFormData, }, }, }); return; } if (err.type === CheckoutErrorType.TRANSACTION_FAILED || err.type === CheckoutErrorType.INSUFFICIENT_FUNDS || (err.receipt && err.receipt.status === 0)) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SwapWidgetViews.FAIL, reason: 'Transaction failed', data: swapFormData, }, }, }); return; } // eslint-disable-next-line no-console console.error('Approve ERC20 failed', err); viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SharedViews.ERROR_VIEW, error: err, }, }, }); }; const prepareTransaction = (transaction: TransactionRequest, isGasFree = false) => ({ ...transaction, gasPrice: (isGasFree ? BigInt(0) : undefined), }); /* --------------------- */ // Approve spending step // /* --------------------- */ const handleApproveSpendingClick = useCallback(async () => { if (loading) return; setLoading(true); if (!checkout || !provider) { showErrorView(); return; } if (actionDisabled || !data.approveTransaction) return; setActionDisabled(true); try { const txnResult = await checkout.sendTransaction({ provider, transaction: prepareTransaction(data.approveTransaction, isPassport), }); setApprovalTxnLoading(true); const approvalReceipt = await txnResult.transactionResponse.wait(); if (approvalReceipt?.status !== 1) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SwapWidgetViews.FAIL, data: data.swapFormInfo as unknown as PrefilledSwapForm, }, }, }); return; } setApprovalTxnLoading(false); setActionDisabled(false); setShowSwapTxnStep(true); } catch (err: any) { setApprovalTxnLoading(false); setActionDisabled(false); if (err.type === CheckoutErrorType.USER_REJECTED_REQUEST_ERROR) { setRejectedSpending(true); return; } handleExceptions(err, data.swapFormInfo); } finally { setLoading(false); } }, [ checkout, provider, showErrorView, viewDispatch, setRejectedSwap, data.approveTransaction, data.swapFormInfo, actionDisabled, setActionDisabled, setApprovalTxnLoading, ]); const approveSpendingContent = useMemo( () => ( {isPassport && ({t('views.APPROVE_ERC20.approveSpending.content.passport.body')})} {!isPassport // eslint-disable-next-line max-len && ( {t( 'views.APPROVE_ERC20.approveSpending.content.metamask.body', { amount: `${data.swapFormInfo.fromAmount} ${fromToken?.symbol || ''}` }, )} )} ), [data.swapFormInfo, fromToken, isPassport], ); const approveSpendingFooter = useMemo(() => ( ), [rejectedSpending, handleApproveSpendingClick, loading]); /* ----------------- */ // Approve swap step // /* ----------------- */ const handleApproveSwapClick = useCallback(async () => { if (loading) return; setLoading(true); if (!checkout || !provider) { showErrorView(); return; } if (actionDisabled) return; setActionDisabled(true); try { const txn = await checkout.sendTransaction({ provider, transaction: prepareTransaction(data.transaction, isPassport), }); setActionDisabled(false); // user approves swap // go to the Swap In Progress View viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: SwapWidgetViews.IN_PROGRESS, data: { transactionResponse: txn.transactionResponse, swapForm: data.swapFormInfo as PrefilledSwapForm, }, }, }, }); } catch (err: any) { setActionDisabled(false); if (err.type === CheckoutErrorType.USER_REJECTED_REQUEST_ERROR) { setRejectedSwap(true); return; } handleExceptions(err, data.swapFormInfo); } finally { setLoading(false); } }, [ checkout, provider, showErrorView, viewDispatch, setRejectedSwap, data.transaction, data.swapFormInfo, actionDisabled, setActionDisabled, ]); const approveSwapContent = ( {t('views.APPROVE_ERC20.approveSwap.content.body')} ); const approveSwapFooter = useMemo(() => ( ), [rejectedSwap, handleApproveSwapClick, loading]); return ( <> {approvalTxnLoading && ( )} {!approvalTxnLoading && ( sendSwapWidgetCloseEvent(eventTarget)} onBackButtonClick={goBackWithSwapData} /> )} floatHeader heroContent={showSwapTxnStep ? : } footer={showSwapTxnStep ? approveSwapFooter : approveSpendingFooter} > {showSwapTxnStep ? approveSwapContent : approveSpendingContent} )} ); }