import type { ExchangeRateUpdateParams } from '@lifi/sdk' import Delete from '@mui/icons-material/Delete' 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 { WarningMessages } from '../../components/Messages/WarningMessages.js' import { PageContainer } from '../../components/PageContainer.js' import { getStepList } from '../../components/Step/StepList.js' import { TransactionDetails } from '../../components/TransactionDetails.js' import { useAddressActivity } from '../../hooks/useAddressActivity.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 { HiddenUI } from '../../types/widget.js' import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js' import { ConfirmToAddressSheet } from './ConfirmToAddressSheet.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' export const TransactionPage: React.FC = () => { const { t } = useTranslation() const { setFieldValue } = useFieldActions() const emitter = useWidgetEvents() const { navigateBack } = useNavigateBack() const { subvariant, subvariantOptions, contractSecondaryComponent, hiddenUI, } = useWidgetConfig() const { state }: any = useLocation() const stateRouteId = state?.routeId const [routeId, setRouteId] = useState(stateRouteId) const [routeRefreshing, setRouteRefreshing] = useState(false) const tokenValueBottomSheetRef = useRef(null) const exchangeRateBottomSheetRef = useRef(null) const confirmToAddressSheetRef = useRef(null) const onAcceptExchangeRateUpdate = ( resolver: (value: boolean) => void, data: ExchangeRateUpdateParams ) => { exchangeRateBottomSheetRef.current?.open(resolver, data) } const { route, status, executeRoute, restartRoute, deleteRoute } = useRouteExecution({ routeId: routeId, onAcceptExchangeRateUpdate, }) const { toAddress, hasActivity, isLoading: isLoadingAddressActivity, isFetched: isActivityAddressFetched, } = useAddressActivity(route?.toChainId) const getHeaderTitle = () => { if (subvariant === 'custom') { return t(`header.${subvariantOptions?.custom ?? 'checkout'}`) } 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') } const headerAction = useMemo( () => status === RouteExecutionStatus.Idle ? ( ) : undefined, [stateRouteId, status] ) useHeader(getHeaderTitle(), headerAction) // biome-ignore lint/correctness/useExhaustiveDependencies: We want to emit event only when the page is mounted useEffect(() => { if (status === RouteExecutionStatus.Idle) { emitter.emit(WidgetEvent.ReviewTransactionPageEntered, route) } }, []) if (!route) { return null } const handleExecuteRoute = () => { if (tokenValueBottomSheetRef.current?.isOpen()) { const { gasCostUSD, feeCostUSD } = getAccumulatedFeeCostsBreakdown(route) const fromAmountUSD = Number.parseFloat(route.fromAmountUSD) const toAmountUSD = Number.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 (status === RouteExecutionStatus.Idle) { if ( toAddress && !hasActivity && !isLoadingAddressActivity && isActivityAddressFetched && !hiddenUI?.includes(HiddenUI.LowAddressActivityConfirmation) ) { confirmToAddressSheetRef.current?.open() return } const { gasCostUSD, feeCostUSD } = getAccumulatedFeeCostsBreakdown(route) const fromAmountUSD = Number.parseFloat(route.fromAmountUSD) const toAmountUSD = Number.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' || subvariantOptions?.custom === 'fund' ? 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} {!hiddenUI?.includes(HiddenUI.LowAddressActivityConfirmation) ? ( ) : null} ) }