import type { ExchangeRateUpdateParams } from '@lifi/sdk'; import { Delete } from '@mui/icons-material'; import { Box, Button, Tooltip } from '@mui/material'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; import type { BottomSheetBase } from '../../components/BottomSheet/types.js'; import { ContractComponent } from '../../components/ContractComponent/ContractComponent.js'; import { GasMessage } from '../../components/GasMessage/GasMessage.js'; import { PageContainer } from '../../components/PageContainer.js'; import { getStepList } from '../../components/Step/StepList.js'; import { TransactionDetails } from '../../components/TransactionDetails.js'; import { useHeader } from '../../hooks/useHeader.js'; import { useNavigateBack } from '../../hooks/useNavigateBack.js'; import { useRouteExecution } from '../../hooks/useRouteExecution.js'; import { useWidgetEvents } from '../../hooks/useWidgetEvents.js'; import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'; import { useFieldActions } from '../../stores/form/useFieldActions.js'; import { RouteExecutionStatus } from '../../stores/routes/types.js'; import { WidgetEvent } from '../../types/events.js'; import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js'; import type { ExchangeRateBottomSheetBase } from './ExchangeRateBottomSheet.js'; import { ExchangeRateBottomSheet } from './ExchangeRateBottomSheet.js'; import { RouteTracker } from './RouteTracker.js'; import { StartTransactionButton } from './StartTransactionButton.js'; import { StatusBottomSheet } from './StatusBottomSheet.js'; import { TokenValueBottomSheet } from './TokenValueBottomSheet.js'; import { calculateValueLossPercentage, getTokenValueLossThreshold, } from './utils.js'; import mixpanel from 'mixpanel-browser'; export const TransactionPage: React.FC = () => { const { t } = useTranslation(); const { setFieldValue } = useFieldActions(); const emitter = useWidgetEvents(); const { navigateBack } = useNavigateBack(); const { subvariant, subvariantOptions, contractSecondaryComponent, mixpanelToken, } = useWidgetConfig(); const { state }: any = useLocation(); const [routeRefreshing, setRouteRefreshing] = useState(false); const [stateRouteId, setRouteId] = useState(state?.routeId ?? ''); const tokenValueBottomSheetRef = useRef(null); const exchangeRateBottomSheetRef = useRef(null); const onAcceptExchangeRateUpdate = ( resolver: (value: boolean) => void, data: ExchangeRateUpdateParams, ) => { exchangeRateBottomSheetRef.current?.open(resolver, data); }; const { route, status, executeRoute, restartRoute, deleteRoute } = useRouteExecution({ routeId: stateRouteId, onAcceptExchangeRateUpdate, }); const headerTitle = useMemo(() => { if (subvariant === 'custom') { return t(`header.${subvariantOptions?.custom ?? 'checkout'}`); } else { if (route) { const transactionType = route.fromChainId === route.toChainId ? 'swap' : 'bridge'; return status === RouteExecutionStatus.Idle ? t(`button.${transactionType}Review`) : t(`header.${transactionType}`); } } return t(`header.exchange`); }, [route, status, subvariant, subvariantOptions?.custom, t]); const headerAction = useMemo(() => { return status === RouteExecutionStatus.Idle ? ( ) : undefined; }, [stateRouteId, status]); useHeader(headerTitle, headerAction); useEffect(() => { if (status === RouteExecutionStatus.Idle) { emitter.emit(WidgetEvent.ReviewTransactionPageEntered, route); } // We want to emit event only when the page is mounted // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (!route) { return null; } const handleExecuteRoute = () => { if (tokenValueBottomSheetRef.current?.isOpen()) { const { gasCostUSD, feeCostUSD } = getAccumulatedFeeCostsBreakdown(route); const fromAmountUSD = parseFloat(route.fromAmountUSD); const toAmountUSD = parseFloat(route.toAmountUSD); emitter.emit(WidgetEvent.RouteHighValueLoss, { fromAmountUSD, toAmountUSD, gasCostUSD, feeCostUSD, valueLoss: calculateValueLossPercentage( fromAmountUSD, toAmountUSD, gasCostUSD, feeCostUSD, ), }); } tokenValueBottomSheetRef.current?.close(); executeRoute(); setFieldValue('fromAmount', ''); if (subvariant === 'custom') { setFieldValue('fromToken', ''); setFieldValue('toToken', ''); } }; const handleStartClick = async () => { if (mixpanelToken) { mixpanel.track('universal_swap_start_transaction_button', { status, route, }); } if (status === RouteExecutionStatus.Idle) { const { gasCostUSD, feeCostUSD } = getAccumulatedFeeCostsBreakdown(route); const fromAmountUSD = parseFloat(route.fromAmountUSD); const toAmountUSD = parseFloat(route.toAmountUSD); const tokenValueLossThresholdExceeded = getTokenValueLossThreshold( fromAmountUSD, toAmountUSD, gasCostUSD, feeCostUSD, ); if (tokenValueLossThresholdExceeded && subvariant !== 'custom') { tokenValueBottomSheetRef.current?.open(); } else { handleExecuteRoute(); } } if (status === RouteExecutionStatus.Failed) { restartRoute(); } }; const handleRemoveRoute = () => { navigateBack(); deleteRoute(); }; const getButtonText = (): string => { switch (status) { case RouteExecutionStatus.Idle: switch (subvariant) { case 'custom': return subvariantOptions?.custom === 'deposit' ? t(`button.deposit`) : t(`button.buy`); case 'refuel': return t('button.startBridging'); default: const transactionType = route.fromChainId === route.toChainId ? 'Swapping' : 'Bridging'; return t(`button.start${transactionType}`); } case RouteExecutionStatus.Failed: return t('button.tryAgain'); default: return ''; } }; return ( {getStepList(route, subvariant)} {subvariant === 'custom' && contractSecondaryComponent ? ( {contractSecondaryComponent} ) : null} {status === RouteExecutionStatus.Idle || status === RouteExecutionStatus.Failed ? ( <> {status === RouteExecutionStatus.Failed ? ( ) : null} ) : null} {status ? : null} {subvariant !== 'custom' ? ( ) : null} ); };