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}
)}
>
);
}