import type { Route } from '@lifi/sdk' import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' import SwapVertIcon from '@mui/icons-material/SwapVert' import type { CardProps } from '@mui/material' import { Box, Skeleton, Tooltip } from '@mui/material' import { type JSX, type ReactNode, useLayoutEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useRoutes } from '../../hooks/useRoutes.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' import { useInputModeStore } from '../../stores/inputMode/useInputModeStore.js' import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js' import { getPriceImpact } from '../../utils/getPriceImpact.js' import { fitInputText } from '../../utils/input.js' import { CardTitle } from '../Card/CardTitle.js' import { ProgressToNextUpdate } from '../ProgressToNextUpdate.js' import { TokenPillButton } from '../TokenPillButton/TokenPillButton.js' import { AmountCard, AmountDisplay, amountHeight, CardBodyRow, CardFooterRow, CardHeaderRow, FooterText, footerFontSize, maxInputFontSize, minInputFontSize, ToggleButton, } from './AmountInputCard.style.js' const formType = 'to' as const type ReceiveCardProps = CardProps & { mask?: boolean } const ReceiveAmountCardLayout = ({ cardProps, title, headerEnd, amount, footerStart, footerEnd, }: { cardProps: ReceiveCardProps title: string headerEnd?: ReactNode amount: ReactNode footerStart: ReactNode footerEnd?: ReactNode }): JSX.Element => ( {title} {headerEnd} {amount} {footerStart} {footerEnd} ) const PriceImpactBadge = ({ priceImpact, }: { priceImpact: number | undefined }): JSX.Element | null => { const { t } = useTranslation() if (priceImpact === undefined) { return null } return ( {t('format.percent', { value: priceImpact, usePlusSign: true })} ) } const useReceivePriceImpact = ( route: Route | undefined ): number | undefined => { const { hiddenUI } = useWidgetConfig() if (!route || hiddenUI?.routeCardPriceImpact) { return undefined } return getPriceImpact({ fromAmount: BigInt(route.fromAmount), toAmount: BigInt(route.toAmount), fromToken: route.fromToken, toToken: route.toToken, }) } /** * The receive amount is a read-only quote derived from the best route, with a * fiat/token toggle and price impact in the footer. In limit mode the amount is * still the quote for the current limit (the limit price drives it via the * dedicated price card); it is never edited here, and the header shows the * refetch progress indicator with a "Buy" title. */ export const ReceiveAmountCard: React.FC = ( props ): JSX.Element => { const { t } = useTranslation() const amountRef = useRef(null) const { inputMode, toggleInputMode } = useInputModeStore() const showFiat = inputMode[formType] === 'price' const { hiddenUI, mode } = useWidgetConfig() const isLimit = mode === 'limit' const [selectedRouteId] = useFieldValues('selectedRouteId') const { routes, isFetching, dataUpdatedAt, refetchTime, refetch } = useRoutes() // Reflect the route the user selected in the routes list (falling back to the // best route), so the buy amount matches the highlighted provider card. const route = routes?.find((r) => r.id === selectedRouteId) ?? routes?.[0] const priceImpact = useReceivePriceImpact(route) const showPriceImpact = !hiddenUI?.routeCardPriceImpact const receiveAmount = route?.toAmount ? formatTokenAmount(BigInt(route.toAmount), route.toToken.decimals) : undefined // Only show skeletons on the initial fetch. const showSkeleton = isFetching && !receiveAmount const fiatValue = receiveAmount && route?.toToken.priceUSD ? formatTokenPrice(receiveAmount, route.toToken.priceUSD) : 0 const canToggle = !!receiveAmount && !!route?.toToken.priceUSD const mainDisplay = showFiat ? t('format.currency', { value: fiatValue }) : t('format.tokenAmount', { value: receiveAmount || '0' }) const footerDisplay = showFiat ? t('format.tokenAmount', { value: receiveAmount || '0' }) : t('format.currency', { value: fiatValue }) // biome-ignore lint/correctness/useExhaustiveDependencies: effect must run on value change useLayoutEffect(() => { if (amountRef.current) { fitInputText(maxInputFontSize, minInputFontSize, amountRef.current) } }, [mainDisplay]) return ( refetch()} sx={{ padding: 0 }} /> ) : undefined } amount={ showSkeleton ? ( ) : ( {mainDisplay} ) } footerStart={ showSkeleton ? ( ) : ( toggleInputMode(formType) : undefined} aria-label={canToggle ? 'Toggle fiat value' : undefined} > {footerDisplay} {canToggle ? ( ) : null} ) } footerEnd={ showSkeleton ? ( showPriceImpact ? ( ) : null ) : ( ) } /> ) }