From de95301c9fcc30a54ebcc88d92f21c6b36610ab0 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Fri, 6 Mar 2026 17:32:46 +0000 Subject: [PATCH 01/22] feat: update components and layout for transaction execution --- .../ActiveTransactionItem.tsx | 20 +- .../src/components/Card/CardIconButton.tsx | 1 + .../src/components/RouteCard/RouteCard.tsx | 51 +-- .../RouteCard/RouteCardEssentials.tsx | 48 +-- .../RouteCard/RouteCardEssentialsExpanded.tsx | 57 ---- .../widget/src/components/RouteCard/types.ts | 4 - .../Step/CircularProgress.style.tsx | 100 ------ .../src/components/Step/CircularProgress.tsx | 144 ++++++--- .../Step/DestinationWalletAddress.tsx | 66 ---- .../src/components/Step/ExecutionProgress.tsx | 45 +++ .../src/components/Step/RouteDetails.tsx | 215 ++++++++++++ .../src/components/Step/RouteTokens.tsx | 58 ++++ .../src/components/Step/RouteTransactions.tsx | 26 ++ packages/widget/src/components/Step/Step.tsx | 118 ------- .../widget/src/components/Step/StepAction.tsx | 84 ----- .../widget/src/components/Step/StepList.tsx | 54 ---- .../components/Step/TokenWithExpansion.tsx | 62 ++++ .../components/Step/TransactionLink.style.tsx | 46 +++ .../src/components/Step/TransactionLink.tsx | 84 +++++ .../components/StepActions/StepActions.tsx | 64 ++-- .../StepDivider/StepDivider.style.tsx | 8 - .../components/StepDivider/StepDivider.tsx | 10 - .../widget/src/components/Timer/StepTimer.tsx | 16 +- .../widget/src/components/Token/Token.tsx | 95 ++---- .../src/components/TransactionDetails.tsx | 306 ------------------ packages/widget/src/i18n/en.json | 2 +- packages/widget/src/icons/lifi.ts | 2 - .../ContactSupportButton.tsx | 10 +- .../TransactionDetailsPage.tsx | 102 +++--- .../TransactionDetailsPage/TransferIdCard.tsx | 7 + .../pages/TransactionPage/TransactionPage.tsx | 30 +- packages/widget/src/utils/getStatusColor.ts | 31 ++ 32 files changed, 833 insertions(+), 1133 deletions(-) delete mode 100644 packages/widget/src/components/RouteCard/RouteCardEssentialsExpanded.tsx delete mode 100644 packages/widget/src/components/Step/CircularProgress.style.tsx delete mode 100644 packages/widget/src/components/Step/DestinationWalletAddress.tsx create mode 100644 packages/widget/src/components/Step/ExecutionProgress.tsx create mode 100644 packages/widget/src/components/Step/RouteDetails.tsx create mode 100644 packages/widget/src/components/Step/RouteTokens.tsx create mode 100644 packages/widget/src/components/Step/RouteTransactions.tsx delete mode 100644 packages/widget/src/components/Step/Step.tsx delete mode 100644 packages/widget/src/components/Step/StepAction.tsx delete mode 100644 packages/widget/src/components/Step/StepList.tsx create mode 100644 packages/widget/src/components/Step/TokenWithExpansion.tsx create mode 100644 packages/widget/src/components/Step/TransactionLink.style.tsx create mode 100644 packages/widget/src/components/Step/TransactionLink.tsx delete mode 100644 packages/widget/src/components/StepDivider/StepDivider.style.tsx delete mode 100644 packages/widget/src/components/StepDivider/StepDivider.tsx delete mode 100644 packages/widget/src/components/TransactionDetails.tsx delete mode 100644 packages/widget/src/icons/lifi.ts create mode 100644 packages/widget/src/utils/getStatusColor.ts diff --git a/packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx b/packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx index 71ed28188..98eff9687 100644 --- a/packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx +++ b/packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx @@ -1,7 +1,12 @@ import ArrowForward from '@mui/icons-material/ArrowForward' import ErrorRounded from '@mui/icons-material/ErrorRounded' import InfoRounded from '@mui/icons-material/InfoRounded' -import { ListItemAvatar, ListItemText, Typography } from '@mui/material' +import { + CircularProgress, + ListItemAvatar, + ListItemText, + Typography, +} from '@mui/material' import { useNavigate } from '@tanstack/react-router' import { useActionMessage } from '../../hooks/useActionMessage.js' import { useRouteExecution } from '../../hooks/useRouteExecution.js' @@ -9,9 +14,9 @@ import { RouteExecutionStatus } from '../../stores/routes/types.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { TokenAvatarGroup } from '../Avatar/Avatar.style.js' import { TokenAvatar } from '../Avatar/TokenAvatar.js' -import { StepTimer } from '../Timer/StepTimer.js' import { ListItem, ListItemButton } from './ActiveTransactions.style.js' +// TODO: This will get replaced with transaction history / activity center export const ActiveTransactionItem: React.FC<{ routeId: string dense?: boolean @@ -47,16 +52,7 @@ export const ActiveTransactionItem: React.FC<{ case 'FAILED': return default: - return ( - - - - ) + return } } diff --git a/packages/widget/src/components/Card/CardIconButton.tsx b/packages/widget/src/components/Card/CardIconButton.tsx index edb3735e8..1f53c4b81 100644 --- a/packages/widget/src/components/Card/CardIconButton.tsx +++ b/packages/widget/src/components/Card/CardIconButton.tsx @@ -11,5 +11,6 @@ export const CardIconButton = styled(MuiIconButton)< backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.08)`, }, fontSize: '1rem', + borderRadius: theme.vars.shape.borderRadiusTertiary, } }) diff --git a/packages/widget/src/components/RouteCard/RouteCard.tsx b/packages/widget/src/components/RouteCard/RouteCard.tsx index 76611e8a9..7292f4780 100644 --- a/packages/widget/src/components/RouteCard/RouteCard.tsx +++ b/packages/widget/src/components/RouteCard/RouteCard.tsx @@ -1,23 +1,16 @@ import type { TokenAmount } from '@lifi/sdk' -import ExpandLess from '@mui/icons-material/ExpandLess' -import ExpandMore from '@mui/icons-material/ExpandMore' -import { Box, Collapse, Tooltip } from '@mui/material' -import type { MouseEventHandler } from 'react' -import { useMemo, useState } from 'react' +import { Box, Tooltip } from '@mui/material' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { HiddenUI, type RouteLabel } from '../../types/widget.js' import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js' import type { CardProps } from '../Card/Card.js' import { Card } from '../Card/Card.js' -import { CardIconButton } from '../Card/CardIconButton.js' import { CardLabel, CardLabelTypography } from '../Card/CardLabel.js' -import { StepActions } from '../StepActions/StepActions.js' -import { Token } from '../Token/Token.js' +import { TokenWithExpansion } from '../Step/TokenWithExpansion.js' import { getMatchingLabels } from './getMatchingLabels.js' -import { TokenContainer } from './RouteCard.style.js' import { RouteCardEssentials } from './RouteCardEssentials.js' -import { RouteCardEssentialsExpanded } from './RouteCardEssentialsExpanded.js' import type { RouteCardProps } from './types.js' export const RouteCard: React.FC< @@ -32,12 +25,6 @@ export const RouteCard: React.FC< const { t } = useTranslation() const { subvariant, subvariantOptions, routeLabels, hiddenUI } = useWidgetConfig() - const [cardExpanded, setCardExpanded] = useState(defaultExpanded) - - const handleExpand: MouseEventHandler = (e) => { - e.stopPropagation() - setCardExpanded((expanded) => !expanded) - } const token: TokenAmount = subvariant === 'custom' && subvariantOptions?.custom !== 'deposit' @@ -119,32 +106,12 @@ export const RouteCard: React.FC< ))} ) : null} - - - {!defaultExpanded ? ( - - {cardExpanded ? ( - - ) : ( - - )} - - ) : null} - - - {route.steps.map((step) => ( - - ))} - - + ) diff --git a/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx b/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx index 6e91f4ff3..a528289eb 100644 --- a/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx +++ b/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx @@ -1,3 +1,4 @@ +import type { RouteExtended } from '@lifi/sdk' import AccessTimeFilled from '@mui/icons-material/AccessTimeFilled' import LocalGasStationRounded from '@mui/icons-material/LocalGasStationRounded' import { Box, Tooltip, Typography } from '@mui/material' @@ -7,10 +8,15 @@ import { formatDuration } from '../../utils/format.js' import { FeeBreakdownTooltip } from '../FeeBreakdownTooltip.js' import { IconTypography } from '../IconTypography.js' import { TokenRate } from '../TokenRate/TokenRate.js' -import type { RouteCardEssentialsProps } from './types.js' + +export interface RouteCardEssentialsProps { + route: RouteExtended + showDuration?: boolean +} export const RouteCardEssentials: React.FC = ({ route, + showDuration = true, }) => { const { t, i18n } = useTranslation() const executionTimeSeconds = Math.floor( @@ -71,28 +77,30 @@ export const RouteCardEssentials: React.FC = ({ - - - - - - + - {formatDuration(executionTimeSeconds, i18n.language)} - - - + + + + + {formatDuration(executionTimeSeconds, i18n.language)} + + + + )} ) diff --git a/packages/widget/src/components/RouteCard/RouteCardEssentialsExpanded.tsx b/packages/widget/src/components/RouteCard/RouteCardEssentialsExpanded.tsx deleted file mode 100644 index 021af1061..000000000 --- a/packages/widget/src/components/RouteCard/RouteCardEssentialsExpanded.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import Layers from '@mui/icons-material/Layers' -import { Box, Typography } from '@mui/material' -import { useTranslation } from 'react-i18next' -import { IconTypography } from '../IconTypography.js' -import type { RouteCardEssentialsProps } from './types.js' - -export const RouteCardEssentialsExpanded: React.FC< - RouteCardEssentialsProps -> = ({ route }) => { - const { t } = useTranslation() - return ( - - - - - - - {route.steps.length} - - - - - {t('tooltip.numberOfSteps')} - - - - ) -} diff --git a/packages/widget/src/components/RouteCard/types.ts b/packages/widget/src/components/RouteCard/types.ts index 09c79c62a..6f6a40166 100644 --- a/packages/widget/src/components/RouteCard/types.ts +++ b/packages/widget/src/components/RouteCard/types.ts @@ -7,10 +7,6 @@ export interface RouteCardProps { expanded?: boolean } -export interface RouteCardEssentialsProps { - route: Route -} - export interface RouteCardSkeletonProps { variant?: 'default' | 'cardless' } diff --git a/packages/widget/src/components/Step/CircularProgress.style.tsx b/packages/widget/src/components/Step/CircularProgress.style.tsx deleted file mode 100644 index 34a9723a0..000000000 --- a/packages/widget/src/components/Step/CircularProgress.style.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import type { ExecutionActionStatus, Substatus } from '@lifi/sdk' -import type { Theme } from '@mui/material' -import { - Box, - circularProgressClasses, - keyframes, - CircularProgress as MuiCircularProgress, - styled, -} from '@mui/material' - -const getStatusColor = ( - theme: Theme, - status?: ExecutionActionStatus, - substatus?: Substatus -) => { - switch (status) { - case 'ACTION_REQUIRED': - case 'MESSAGE_REQUIRED': - case 'RESET_REQUIRED': - return `rgba(${theme.vars.palette.info.mainChannel} / 0.12)` - case 'DONE': - if (substatus === 'PARTIAL' || substatus === 'REFUNDED') { - return `rgba(${theme.vars.palette.warning.mainChannel} / 0.48)` - } - return `rgba(${theme.vars.palette.success.mainChannel} / 0.12)` - case 'FAILED': - return `rgba(${theme.vars.palette.error.mainChannel} / 0.12)` - default: - return null - } -} - -export const CircularIcon = styled(Box, { - shouldForwardProp: (prop: string) => !['status', 'substatus'].includes(prop), -})<{ status?: ExecutionActionStatus; substatus?: Substatus }>( - ({ theme, status, substatus }) => { - const statusColor = getStatusColor(theme, status, substatus) - const isSpecialStatus = [ - 'ACTION_REQUIRED', - 'MESSAGE_REQUIRED', - 'RESET_REQUIRED', - 'DONE', - 'FAILED', - ].includes(status!) - - return { - backgroundColor: isSpecialStatus - ? statusColor! - : theme.vars.palette.background.paper, - borderStyle: 'solid', - borderColor: statusColor || theme.vars.palette.grey[300], - borderWidth: !isSpecialStatus ? 3 : 0, - display: 'grid', - position: 'relative', - placeItems: 'center', - width: 40, - height: 40, - borderRadius: '50%', - ...theme.applyStyles('dark', { - borderColor: statusColor || theme.vars.palette.grey[800], - }), - } - } -) - -const circleAnimation = keyframes` - 0% { - stroke-dashoffset: 129; - transform: rotate(0); - } - 50% { - stroke-dashoffset: 56; - transform: rotate(45deg); - }; - 100% { - stroke-dashoffset: 129; - transform: rotate(360deg); - } -` - -// This `styled()` function invokes keyframes. `styled-components` only supports keyframes -// in string templates. Do not convert these styles in JS object as it will break. -export const CircularProgressPending = styled(MuiCircularProgress)` - color: ${({ theme }) => theme.vars.palette.primary.main}; - ${({ theme }) => - theme.applyStyles('dark', { - color: theme.vars.palette.primary.light, - })} - animation-duration: 3s; - position: absolute; - .${circularProgressClasses.circle} { - animation-duration: 2s; - animation-timing-function: linear; - animation-name: ${circleAnimation}; - stroke-dasharray: 129; - stroke-dashoffset: 129; - stroke-linecap: round; - transform-origin: 100% 100%; - } -` diff --git a/packages/widget/src/components/Step/CircularProgress.tsx b/packages/widget/src/components/Step/CircularProgress.tsx index 9ea60cc24..47e8ae348 100644 --- a/packages/widget/src/components/Step/CircularProgress.tsx +++ b/packages/widget/src/components/Step/CircularProgress.tsx @@ -1,57 +1,103 @@ -import type { ExecutionAction } from '@lifi/sdk' +import type { LiFiStepExtended } from '@lifi/sdk' import Done from '@mui/icons-material/Done' import ErrorRounded from '@mui/icons-material/ErrorRounded' -import InfoRounded from '@mui/icons-material/InfoRounded' import WarningRounded from '@mui/icons-material/WarningRounded' -import { - CircularIcon, - CircularProgressPending, -} from './CircularProgress.style.js' - -export function CircularProgress({ action }: { action: ExecutionAction }) { - return ( - - {action.status === 'STARTED' || action.status === 'PENDING' ? ( - - ) : null} - {action.status === 'ACTION_REQUIRED' || - action.status === 'MESSAGE_REQUIRED' || - action.status === 'RESET_REQUIRED' ? ( - - ) : null} - {action.status === 'DONE' && - (action.substatus === 'PARTIAL' || action.substatus === 'REFUNDED') ? ( - ({ - position: 'absolute', - fontSize: '1.5rem', - color: `color-mix(in srgb, ${theme.vars.palette.warning.main} 68%, black)`, - })} - /> - ) : action.status === 'DONE' ? ( - = ({ step }) => { + const theme = useTheme() + + const lastAction = step.execution?.actions?.at(-1) + + if (!step.execution || !lastAction) { + return null + } + + const status = lastAction?.status + const substatus = lastAction?.substatus + + const withTimer = status === 'STARTED' || status === 'PENDING' + const actionRequired = + status === 'ACTION_REQUIRED' || + status === 'MESSAGE_REQUIRED' || + status === 'RESET_REQUIRED' + + if (withTimer || actionRequired) { + return + } + + const backgroundColor = getStatusColor(theme, status, substatus) + + switch (status) { + case 'DONE': + if (substatus === 'PARTIAL' || substatus === 'REFUNDED') { + return ( + + + + ) + } + + return ( + - ) : null} - {action.status === 'FAILED' ? ( - + + + ) + case 'FAILED': + return ( + - ) : null} - - ) + > + + + ) + } } diff --git a/packages/widget/src/components/Step/DestinationWalletAddress.tsx b/packages/widget/src/components/Step/DestinationWalletAddress.tsx deleted file mode 100644 index c45d4f01a..000000000 --- a/packages/widget/src/components/Step/DestinationWalletAddress.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { LiFiStepExtended } from '@lifi/sdk' -import LinkRounded from '@mui/icons-material/LinkRounded' -import Wallet from '@mui/icons-material/Wallet' -import { Box, Link, Typography } from '@mui/material' -import { useTranslation } from 'react-i18next' -import { CardIconButton } from '../Card/CardIconButton.js' -import { CircularIcon } from './CircularProgress.style.js' - -export const DestinationWalletAddress: React.FC<{ - step: LiFiStepExtended - toAddress: string - toAddressLink: string -}> = ({ step, toAddress, toAddressLink }) => { - const { t } = useTranslation() - const isDone = step.execution?.status === 'DONE' - return ( - - - - - - - {isDone - ? t('main.sentToAddress', { - address: toAddress, - }) - : t('main.sendToAddress', { - address: toAddress, - })} - - - - - - - ) -} diff --git a/packages/widget/src/components/Step/ExecutionProgress.tsx b/packages/widget/src/components/Step/ExecutionProgress.tsx new file mode 100644 index 000000000..9ea7cc2ff --- /dev/null +++ b/packages/widget/src/components/Step/ExecutionProgress.tsx @@ -0,0 +1,45 @@ +import type { RouteExtended } from '@lifi/sdk' +import { Box, Typography } from '@mui/material' +import { useActionMessage } from '../../hooks/useActionMessage' +import { CircularProgress } from './CircularProgress' + +export const ExecutionProgress: React.FC<{ + route: RouteExtended +}> = ({ route }) => { + const lastStep = route.steps.at(-1) + const lastAction = lastStep?.execution?.actions?.at(-1) + const { title, message } = useActionMessage(lastStep, lastAction) + + if (!lastStep || !lastAction) { + return null + } + + return ( + + + + + + {title} + + {message ? ( + + {message} + + ) : null} + + ) +} diff --git a/packages/widget/src/components/Step/RouteDetails.tsx b/packages/widget/src/components/Step/RouteDetails.tsx new file mode 100644 index 000000000..40718c002 --- /dev/null +++ b/packages/widget/src/components/Step/RouteDetails.tsx @@ -0,0 +1,215 @@ +import type { RouteExtended } from '@lifi/sdk' +import { useEthereumContext } from '@lifi/widget-provider' +import { Box, Tooltip, Typography } from '@mui/material' +import { useTranslation } from 'react-i18next' +import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider' +import { isRouteDone } from '../../stores/routes/utils' +import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees' +import { formatTokenAmount, formatTokenPrice } from '../../utils/format' +import { getPriceImpact } from '../../utils/getPriceImpact' +import { FeeBreakdownTooltip } from '../FeeBreakdownTooltip' +import { StepActions } from '../StepActions/StepActions' + +interface RouteDetailsProps { + route: RouteExtended +} + +export const RouteDetails = ({ route }: RouteDetailsProps) => { + const { t } = useTranslation() + + const { feeConfig } = useWidgetConfig() + + const { isGaslessStep } = useEthereumContext() + + const { gasCosts, feeCosts, gasCostUSD, feeCostUSD } = + getAccumulatedFeeCostsBreakdown(route) + + const priceImpact = getPriceImpact({ + fromAmount: BigInt(route.fromAmount), + toAmount: BigInt(route.toAmount), + fromToken: route.fromToken, + toToken: route.toToken, + }) + + let feeAmountUSD = 0 + let feePercentage = 0 + + const feeCollectionStep = route.steps[0].includedSteps.find( + (includedStep) => includedStep.tool === 'feeCollection' + ) + + if (feeCollectionStep) { + const estimatedFromAmount = + BigInt(feeCollectionStep.estimate.fromAmount) - + BigInt(feeCollectionStep.estimate.toAmount) + + feeAmountUSD = formatTokenPrice( + estimatedFromAmount, + feeCollectionStep.action.fromToken.priceUSD, + feeCollectionStep.action.fromToken.decimals + ) + + feePercentage = + feeCollectionStep.estimate.feeCosts?.reduce( + (percentage, feeCost) => + percentage + Number.parseFloat(feeCost.percentage || '0'), + 0 + ) ?? 0 + } + + const hasGaslessSupport = route.steps.every((step) => isGaslessStep?.(step)) + + const showIntegratorFeeCollectionDetails = + (feeAmountUSD || Number.isFinite(feeConfig?.fee)) && !hasGaslessSupport + + return ( + + {route.steps.map((step) => ( + + ))} + + {t('main.fees.network')} + + + {!gasCostUSD + ? t('main.fees.free') + : t('format.currency', { + value: gasCostUSD, + })} + + + + {feeCosts.length ? ( + + {t('main.fees.provider')} + + + {t('format.currency', { + value: feeCostUSD, + })} + + + + ) : null} + {showIntegratorFeeCollectionDetails ? ( + + + {feeConfig?.name || t('main.fees.defaultIntegrator')} + {feeConfig?.showFeePercentage && ( + <> ({t('format.percent', { value: feePercentage })}) + )} + + {feeConfig?.showFeeTooltip && + (feeConfig?.name || feeConfig?.feeTooltipComponent) ? ( + + + {t('format.currency', { + value: feeAmountUSD, + })} + + + ) : ( + + {t('format.currency', { + value: feeAmountUSD, + })} + + )} + + ) : null} + + {t('main.priceImpact')} + + + {t('format.percent', { + value: priceImpact, + usePlusSign: true, + })} + + + + {!isRouteDone(route) ? ( + <> + + {t('main.maxSlippage')} + + + {route.steps[0].action.slippage + ? t('format.percent', { + value: route.steps[0].action.slippage, + }) + : t('button.auto')} + + + + + {t('main.minReceived')} + + + {t('format.tokenAmount', { + value: formatTokenAmount( + BigInt(route.toAmountMin), + route.toToken.decimals + ), + })}{' '} + {route.toToken.symbol} + + + + + ) : null} + + ) +} diff --git a/packages/widget/src/components/Step/RouteTokens.tsx b/packages/widget/src/components/Step/RouteTokens.tsx new file mode 100644 index 000000000..098979009 --- /dev/null +++ b/packages/widget/src/components/Step/RouteTokens.tsx @@ -0,0 +1,58 @@ +import type { RouteExtended } from '@lifi/sdk' +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward' +import { Box } from '@mui/material' +import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { Token } from '../Token/Token.js' +import { TokenWithExpansion } from './TokenWithExpansion.js' + +export const RouteTokens: React.FC<{ + route: RouteExtended +}> = ({ route }) => { + const { subvariant, defaultUI } = useWidgetConfig() + + const fromToken = { + ...route.steps[0].action.fromToken, + amount: BigInt(route.steps[0].action.fromAmount), + } + + const lastStepIndex = route.steps.length - 1 + const toToken = { + ...(route.steps[lastStepIndex].execution?.toToken ?? + route.steps[lastStepIndex].action.toToken), + amount: route.steps[lastStepIndex].execution?.toAmount + ? BigInt(route.steps[lastStepIndex].execution.toAmount) + : subvariant === 'custom' + ? BigInt(route.toAmount) + : BigInt(route.steps[lastStepIndex].estimate.toAmount), + } + + const impactToken = { + ...route.steps[0].action.fromToken, + amount: BigInt(route.steps[0].action.fromAmount), + } + + return ( + + {fromToken ? : null} + + + + {toToken ? ( + + ) : null} + + ) +} diff --git a/packages/widget/src/components/Step/RouteTransactions.tsx b/packages/widget/src/components/Step/RouteTransactions.tsx new file mode 100644 index 000000000..32329f952 --- /dev/null +++ b/packages/widget/src/components/Step/RouteTransactions.tsx @@ -0,0 +1,26 @@ +import type { RouteExtended } from '@lifi/sdk' +import { Box } from '@mui/material' +import { prepareActions } from '../../utils/prepareActions' +import { StepTransactionLink } from './TransactionLink' + +export const RouteTransactions: React.FC<{ + route: RouteExtended +}> = ({ route }) => { + return ( + + {route.steps.map((step) => ( + + {prepareActions(step.execution?.actions ?? []).map( + (actionsGroup, index) => ( + + ) + )} + + ))} + + ) +} diff --git a/packages/widget/src/components/Step/Step.tsx b/packages/widget/src/components/Step/Step.tsx deleted file mode 100644 index 01ae3554a..000000000 --- a/packages/widget/src/components/Step/Step.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import type { LiFiStepExtended, TokenAmount } from '@lifi/sdk' -import { Box } from '@mui/material' -import { useTranslation } from 'react-i18next' -import { Card } from '../../components/Card/Card.js' -import { CardTitle } from '../../components/Card/CardTitle.js' -import { StepActions } from '../../components/StepActions/StepActions.js' -import { Token } from '../../components/Token/Token.js' -import { useExplorer } from '../../hooks/useExplorer.js' -import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' -import { prepareActions } from '../../utils/prepareActions.js' -import { shortenAddress } from '../../utils/wallet.js' -import { StepTimer } from '../Timer/StepTimer.js' -import { DestinationWalletAddress } from './DestinationWalletAddress.js' -import { StepAction } from './StepAction.js' - -export const Step: React.FC<{ - step: LiFiStepExtended - fromToken?: TokenAmount - toToken?: TokenAmount - impactToken?: TokenAmount - toAddress?: string -}> = ({ step, fromToken, toToken, impactToken, toAddress }) => { - const { t } = useTranslation() - const { subvariant, subvariantOptions } = useWidgetConfig() - const { getAddressLink } = useExplorer() - const stepHasError = step.execution?.actions?.some( - (action) => action.status === 'FAILED' - ) - - const getCardTitle = () => { - const hasBridgeStep = step.includedSteps.some( - (step) => step.type === 'cross' - ) - const hasSwapStep = step.includedSteps.some((step) => step.type === 'swap') - const hasCustomStep = step.includedSteps.some( - (step) => step.type === 'custom' - ) - - const isCustomVariant = hasCustomStep && subvariant === 'custom' - - if (hasBridgeStep && hasSwapStep) { - return isCustomVariant - ? subvariantOptions?.custom === 'deposit' - ? t('main.stepBridgeAndDeposit') - : t('main.stepBridgeAndBuy') - : t('main.stepSwapAndBridge') - } - if (hasBridgeStep) { - return isCustomVariant - ? subvariantOptions?.custom === 'deposit' - ? t('main.stepBridgeAndDeposit') - : t('main.stepBridgeAndBuy') - : t('main.stepBridge') - } - if (hasSwapStep) { - return isCustomVariant - ? subvariantOptions?.custom === 'deposit' - ? t('main.stepSwapAndDeposit') - : t('main.stepSwapAndBuy') - : t('main.stepSwap') - } - return isCustomVariant - ? subvariantOptions?.custom === 'deposit' - ? t('main.stepDeposit') - : t('main.stepBuy') - : t('main.stepSwap') - } - - const formattedToAddress = shortenAddress(toAddress) - const toAddressLink = toAddress - ? getAddressLink(toAddress, step.action.toChainId) - : undefined - - return ( - - - {getCardTitle()} - - - - - - {fromToken ? : null} - - {prepareActions(step.execution?.actions ?? []).map( - (actionsGroup, index) => ( - - ) - )} - {formattedToAddress && toAddressLink ? ( - - ) : null} - {toToken ? ( - - ) : null} - - - ) -} diff --git a/packages/widget/src/components/Step/StepAction.tsx b/packages/widget/src/components/Step/StepAction.tsx deleted file mode 100644 index 1e50e076d..000000000 --- a/packages/widget/src/components/Step/StepAction.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import type { ExecutionAction, LiFiStepExtended } from '@lifi/sdk' -import OpenInNewRounded from '@mui/icons-material/OpenInNewRounded' -import { Box, Link, Typography } from '@mui/material' -import { useActionMessage } from '../../hooks/useActionMessage.js' -import { useExplorer } from '../../hooks/useExplorer.js' -import { CardIconButton } from '../Card/CardIconButton.js' -import { CircularProgress } from './CircularProgress.js' - -export const StepAction: React.FC<{ - step: LiFiStepExtended - actionsGroup: ExecutionAction[] -}> = ({ step, actionsGroup }) => { - const action = actionsGroup.at(-1) - const { title, message } = useActionMessage(step, action) - const { getTransactionLink } = useExplorer() - - if (!action) { - return null - } - - const transactionLink = action.txHash - ? getTransactionLink({ - txHash: action.txHash, - chain: action.chainId, - }) - : action.txLink - ? getTransactionLink({ - txLink: action.txLink, - chain: action.chainId, - }) - : undefined - - return ( - - - - - {title} - - {transactionLink ? ( - - - - ) : null} - - {message ? ( - - {message} - - ) : null} - - ) -} diff --git a/packages/widget/src/components/Step/StepList.tsx b/packages/widget/src/components/Step/StepList.tsx deleted file mode 100644 index a7c122fc4..000000000 --- a/packages/widget/src/components/Step/StepList.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { RouteExtended, TokenAmount } from '@lifi/sdk' -import { Fragment } from 'react' -import { StepDivider } from '../../components/StepDivider/StepDivider.js' -import type { WidgetSubvariant } from '../../types/widget.js' -import { Step } from './Step.js' - -export const getStepList = ( - route?: RouteExtended, - subvariant?: WidgetSubvariant -) => - route?.steps.map((step, index, steps) => { - const lastIndex = steps.length - 1 - const fromToken: TokenAmount | undefined = - index === 0 - ? { - ...step.action.fromToken, - amount: BigInt(step.action.fromAmount), - } - : undefined - let toToken: TokenAmount | undefined - let impactToken: TokenAmount | undefined - if (index === lastIndex) { - toToken = { - ...(step.execution?.toToken ?? step.action.toToken), - amount: step.execution?.toAmount - ? BigInt(step.execution.toAmount) - : subvariant === 'custom' - ? BigInt(route.toAmount) - : BigInt(step.estimate.toAmount), - } - impactToken = { - ...steps[0].action.fromToken, - amount: BigInt(steps[0].action.fromAmount), - } - } - const toAddress = - index === lastIndex && route.fromAddress !== route.toAddress - ? route.toAddress - : undefined - return ( - - - {steps.length > 1 && index !== steps.length - 1 ? ( - - ) : null} - - ) - }) diff --git a/packages/widget/src/components/Step/TokenWithExpansion.tsx b/packages/widget/src/components/Step/TokenWithExpansion.tsx new file mode 100644 index 000000000..bc087f57d --- /dev/null +++ b/packages/widget/src/components/Step/TokenWithExpansion.tsx @@ -0,0 +1,62 @@ +import type { RouteExtended, TokenAmount } from '@lifi/sdk' +import ExpandLess from '@mui/icons-material/ExpandLess' +import ExpandMore from '@mui/icons-material/ExpandMore' +import { Box, Collapse } from '@mui/material' +import { type MouseEventHandler, useState } from 'react' +import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider' +import { HiddenUI } from '../../types/widget' +import { CardIconButton } from '../Card/CardIconButton' +import { TokenContainer } from '../RouteCard/RouteCard.style' +import { Token } from '../Token/Token' +import { RouteDetails } from './RouteDetails' + +interface TokenWithExpansionProps { + route: RouteExtended + token: TokenAmount + impactToken?: TokenAmount + defaultExpanded?: boolean +} + +export const TokenWithExpansion = ({ + route, + token, + impactToken, + defaultExpanded, +}: TokenWithExpansionProps) => { + const { hiddenUI } = useWidgetConfig() + + const [cardExpanded, setCardExpanded] = useState(defaultExpanded) + + const handleExpand: MouseEventHandler = (e) => { + e.stopPropagation() + setCardExpanded((expanded) => !expanded) + } + + return ( + + + + {!defaultExpanded ? ( + + {cardExpanded ? ( + + ) : ( + + )} + + ) : null} + + + + + + ) +} diff --git a/packages/widget/src/components/Step/TransactionLink.style.tsx b/packages/widget/src/components/Step/TransactionLink.style.tsx new file mode 100644 index 000000000..251a8b4b0 --- /dev/null +++ b/packages/widget/src/components/Step/TransactionLink.style.tsx @@ -0,0 +1,46 @@ +import { Box, Link, styled, Typography } from '@mui/material' + +export const TransactionLinkContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(1), + borderRadius: theme.vars.shape.borderRadiusSecondary, + color: theme.vars.palette.text.primary, + backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, +})) + +export const StatusIconCircle = styled(Box, { + shouldForwardProp: (prop) => prop !== 'failed', +})<{ failed?: boolean }>(({ theme, failed }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 24, + height: 24, + borderRadius: '50%', + backgroundColor: failed + ? `rgba(${theme.vars.palette.error.mainChannel} / 0.12)` + : `rgba(${theme.vars.palette.success.mainChannel} / 0.12)`, + marginRight: theme.spacing(1), +})) + +export const TransactionLinkLabel = styled(Typography)(() => ({ + flex: 1, + fontSize: 12, + fontWeight: 400, + color: 'inherit', +})) + +export const ExternalLinkIcon = styled(Link)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 24, + height: 24, + borderRadius: '50%', + textDecoration: 'none', + color: 'inherit', + '&:hover': { + backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, + }, +})) diff --git a/packages/widget/src/components/Step/TransactionLink.tsx b/packages/widget/src/components/Step/TransactionLink.tsx new file mode 100644 index 000000000..f200d266c --- /dev/null +++ b/packages/widget/src/components/Step/TransactionLink.tsx @@ -0,0 +1,84 @@ +import type { ExecutionAction, LiFiStepExtended } from '@lifi/sdk' +import Done from '@mui/icons-material/Done' +import ErrorRounded from '@mui/icons-material/ErrorRounded' +import OpenInNew from '@mui/icons-material/OpenInNew' +import { Box } from '@mui/material' +import type React from 'react' +import { useActionMessage } from '../../hooks/useActionMessage.js' +import { useExplorer } from '../../hooks/useExplorer.js' +import { + ExternalLinkIcon, + StatusIconCircle, + TransactionLinkContainer, + TransactionLinkLabel, +} from './TransactionLink.style.js' + +export interface TransactionLinkProps { + label: string + href?: string + failed?: boolean +} + +export const TransactionLink: React.FC = ({ + label, + href, + failed, +}) => { + return ( + + + {failed ? ( + + ) : ( + + )} + + {label} + {href ? ( + + + + ) : null} + + ) +} + +export const StepTransactionLink: React.FC<{ + step: LiFiStepExtended + actionsGroup: ExecutionAction[] +}> = ({ step, actionsGroup }) => { + const action = actionsGroup.at(-1) + const { title } = useActionMessage(step, action) + const { getTransactionLink } = useExplorer() + + if (!action) { + return null + } + + const transactionLink = action.txHash + ? getTransactionLink({ + txHash: action.txHash, + chain: action.chainId, + }) + : action.txLink + ? getTransactionLink({ + txLink: action.txLink, + chain: action.chainId, + }) + : undefined + + return ( + ({ + py: 0.5, + borderRadius: theme.vars.shape.borderRadiusTertiary, + })} + > + + + ) +} diff --git a/packages/widget/src/components/StepActions/StepActions.tsx b/packages/widget/src/components/StepActions/StepActions.tsx index 3dd0991ec..60df84097 100644 --- a/packages/widget/src/components/StepActions/StepActions.tsx +++ b/packages/widget/src/components/StepActions/StepActions.tsx @@ -5,7 +5,6 @@ import ExpandLess from '@mui/icons-material/ExpandLess' import ExpandMore from '@mui/icons-material/ExpandMore' import type { StepIconProps } from '@mui/material' import { - Badge, Box, Collapse, Step as MuiStep, @@ -16,14 +15,12 @@ import type { MouseEventHandler } from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useAvailableChains } from '../../hooks/useAvailableChains.js' -import { lifiLogoUrl } from '../../icons/lifi.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { HiddenUI } from '../../types/widget.js' import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js' import { SmallAvatar } from '../Avatar/SmallAvatar.js' import { CardIconButton } from '../Card/CardIconButton.js' import { - StepAvatar, StepConnector, StepContent, StepLabel, @@ -41,7 +38,6 @@ export const StepActions: React.FC = ({ ...other }) => { const { t } = useTranslation() - const { subvariant } = useWidgetConfig() const [cardExpanded, setCardExpanded] = useState(false) const handleExpand: MouseEventHandler = (e) => { @@ -49,62 +45,42 @@ export const StepActions: React.FC = ({ setCardExpanded((expanded) => !expanded) } - // FIXME: step transaction request overrides step tool details, but not included step tool details - const toolDetails = - subvariant === 'custom' - ? step.includedSteps.find( - (step) => step.tool === 'custom' && step.toolDetails.key !== 'custom' - )?.toolDetails || step.toolDetails - : step.toolDetails - return ( - } - > - - {toolDetails.name[0]} - - - - - {toolDetails.name?.includes('LI.FI') - ? toolDetails.name - : t('main.stepDetails', { - tool: toolDetails.name, - })} - - {/* */} - + {t('main.route')} + + {dense ? ( {cardExpanded ? ( ) : ( - + + {step.includedSteps.map((includedStep, index) => ( + 0 ? -0.5 : 0 }} + > + {includedStep.toolDetails.name[0]} + + ))} + + )} ) : null} diff --git a/packages/widget/src/components/StepDivider/StepDivider.style.tsx b/packages/widget/src/components/StepDivider/StepDivider.style.tsx deleted file mode 100644 index adcc88a28..000000000 --- a/packages/widget/src/components/StepDivider/StepDivider.style.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Container as MuiContainer, styled } from '@mui/material' - -export const Container = styled(MuiContainer)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: theme.spacing(2), -})) diff --git a/packages/widget/src/components/StepDivider/StepDivider.tsx b/packages/widget/src/components/StepDivider/StepDivider.tsx deleted file mode 100644 index a8f612238..000000000 --- a/packages/widget/src/components/StepDivider/StepDivider.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Divider } from '@mui/material' -import { Container } from './StepDivider.style.js' - -export const StepDivider: React.FC = () => { - return ( - - - - ) -} diff --git a/packages/widget/src/components/Timer/StepTimer.tsx b/packages/widget/src/components/Timer/StepTimer.tsx index d5c43627c..7bbdf9420 100644 --- a/packages/widget/src/components/Timer/StepTimer.tsx +++ b/packages/widget/src/components/Timer/StepTimer.tsx @@ -20,8 +20,6 @@ export const StepTimer: React.FC<{ step: LiFiStepExtended hideInProgress?: boolean }> = ({ step, hideInProgress }) => { - const { i18n } = useTranslation() - if ( step.execution?.status === 'DONE' || step.execution?.status === 'FAILED' @@ -30,19 +28,7 @@ export const StepTimer: React.FC<{ } if (!step.execution?.signedAt) { - const showSeconds = step.estimate.executionDuration < 60 - const duration = showSeconds - ? Math.floor(step.estimate.executionDuration) - : Math.floor(step.estimate.executionDuration / 60) - return ( - - {duration.toLocaleString(i18n.language, { - style: 'unit', - unit: showSeconds ? 'second' : 'minute', - unitDisplay: 'narrow', - })} - - ) + return null } return ( diff --git a/packages/widget/src/components/Token/Token.tsx b/packages/widget/src/components/Token/Token.tsx index 579698270..5b48c931b 100644 --- a/packages/widget/src/components/Token/Token.tsx +++ b/packages/widget/src/components/Token/Token.tsx @@ -1,7 +1,7 @@ -import type { LiFiStep, TokenAmount } from '@lifi/sdk' +import type { ExtendedChain, LiFiStep, TokenAmount } from '@lifi/sdk' import type { BoxProps } from '@mui/material' import { Box, Grow, Skeleton, Tooltip } from '@mui/material' -import type { FC, PropsWithChildren, ReactElement } from 'react' +import type { FC } from 'react' import { useTranslation } from 'react-i18next' import { useChain } from '../../hooks/useChain.js' import { useToken } from '../../hooks/useToken.js' @@ -92,15 +92,6 @@ const TokenBase: FC = ({ priceImpactPercent = priceImpact * 100 } - const tokenOnChain = !disableDescription ? ( - - {t('main.tokenOnChain', { - tokenSymbol: token.symbol, - chainName: chain?.name, - })} - - ) : null - return ( = ({ • ) : null} - {!disableDescription && step ? ( - - {tokenOnChain} - - ) : ( - tokenOnChain - )} + {!disableDescription ? ( + + ) : null} ) } -const TokenStep: FC>> = ({ - step, - stepVisible, - disableDescription, - children, -}) => { +const IconLabel: FC<{ src?: string; name?: string }> = ({ src, name }) => ( + <> + + + {name?.[0]} + + + {name} + +) + +interface TokenStepProps extends Partial { + chain?: ExtendedChain +} + +const TokenStep: FC = ({ step, stepVisible, chain }) => { return ( >> = ({ }} > - - {children as ReactElement} + + - - - - {step?.toolDetails.name[0]} - - - {step?.toolDetails.name} + + diff --git a/packages/widget/src/components/TransactionDetails.tsx b/packages/widget/src/components/TransactionDetails.tsx deleted file mode 100644 index 24aac59b2..000000000 --- a/packages/widget/src/components/TransactionDetails.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import type { RouteExtended } from '@lifi/sdk' -import { useEthereumContext } from '@lifi/widget-provider' -import ExpandLess from '@mui/icons-material/ExpandLess' -import ExpandMore from '@mui/icons-material/ExpandMore' -import LocalGasStationRounded from '@mui/icons-material/LocalGasStationRounded' -import type { CardProps } from '@mui/material' -import { Box, Collapse, Tooltip, Typography } from '@mui/material' -import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js' -import { isRouteDone } from '../stores/routes/utils.js' -import { getAccumulatedFeeCostsBreakdown } from '../utils/fees.js' -import { formatTokenAmount, formatTokenPrice } from '../utils/format.js' -import { getPriceImpact } from '../utils/getPriceImpact.js' -import { Card } from './Card/Card.js' -import { CardIconButton } from './Card/CardIconButton.js' -import { FeeBreakdownTooltip } from './FeeBreakdownTooltip.js' -import { IconTypography } from './IconTypography.js' -import { TokenRate } from './TokenRate/TokenRate.js' - -interface TransactionDetailsProps extends CardProps { - route: RouteExtended -} - -export const TransactionDetails: React.FC = ({ - route, - ...props -}) => { - const { t } = useTranslation() - const { feeConfig, defaultUI } = useWidgetConfig() - const [cardExpanded, setCardExpanded] = useState( - defaultUI?.transactionDetailsExpanded ?? false - ) - const { isGaslessStep } = useEthereumContext() - - const toggleCard = () => { - setCardExpanded((cardExpanded) => !cardExpanded) - } - const { gasCosts, feeCosts, gasCostUSD, feeCostUSD, combinedFeesUSD } = - getAccumulatedFeeCostsBreakdown(route) - - const priceImpact = getPriceImpact({ - fromAmount: BigInt(route.fromAmount), - toAmount: BigInt(route.toAmount), - fromToken: route.fromToken, - toToken: route.toToken, - }) - - const feeCollectionStep = route.steps[0].includedSteps.find( - (includedStep) => includedStep.tool === 'feeCollection' - ) - - let feeAmountUSD = 0 - let feePercentage = 0 - - if (feeCollectionStep) { - const estimatedFromAmount = - BigInt(feeCollectionStep.estimate.fromAmount) - - BigInt(feeCollectionStep.estimate.toAmount) - - feeAmountUSD = formatTokenPrice( - estimatedFromAmount, - feeCollectionStep.action.fromToken.priceUSD, - feeCollectionStep.action.fromToken.decimals - ) - - feePercentage = - feeCollectionStep.estimate.feeCosts?.reduce( - (percentage, feeCost) => - percentage + Number.parseFloat(feeCost.percentage || '0'), - 0 - ) ?? 0 - } - - const hasGaslessSupport = route.steps.every((step) => isGaslessStep?.(step)) - - const showIntegratorFeeCollectionDetails = - (feeAmountUSD || Number.isFinite(feeConfig?.fee)) && !hasGaslessSupport - - return ( - - - - - - - - - - - - - {!combinedFeesUSD - ? t('main.fees.free') - : t('format.currency', { value: combinedFeesUSD })} - - - - - - {cardExpanded ? ( - - ) : ( - - )} - - - - - - {t('main.fees.network')} - - - {!gasCostUSD - ? t('main.fees.free') - : t('format.currency', { - value: gasCostUSD, - })} - - - - {feeCosts.length ? ( - - {t('main.fees.provider')} - - - {t('format.currency', { - value: feeCostUSD, - })} - - - - ) : null} - {showIntegratorFeeCollectionDetails ? ( - - - {feeConfig?.name || t('main.fees.defaultIntegrator')} - {feeConfig?.showFeePercentage && ( - <> ({t('format.percent', { value: feePercentage })}) - )} - - {feeConfig?.showFeeTooltip && - (feeConfig?.name || feeConfig?.feeTooltipComponent) ? ( - - - {t('format.currency', { - value: feeAmountUSD, - })} - - - ) : ( - - {t('format.currency', { - value: feeAmountUSD, - })} - - )} - - ) : null} - - {t('main.priceImpact')} - - - {t('format.percent', { - value: priceImpact, - usePlusSign: true, - })} - - - - {!isRouteDone(route) ? ( - <> - - {t('main.maxSlippage')} - - - {route.steps[0].action.slippage - ? t('format.percent', { - value: route.steps[0].action.slippage, - }) - : t('button.auto')} - - - - - {t('main.minReceived')} - - - {t('format.tokenAmount', { - value: formatTokenAmount( - BigInt(route.toAmountMin), - route.toToken.decimals - ), - })}{' '} - {route.toToken.symbol} - - - - - ) : null} - - - - ) -} diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json index 1f988bee4..8a459dde7 100644 --- a/packages/widget/src/i18n/en.json +++ b/packages/widget/src/i18n/en.json @@ -207,7 +207,6 @@ }, "multistep": "This route requires multiple transactions to complete the transfer. Each exchange step can contain 1-2 transactions that require a signature.", "gasless": "We handle the gas, so you can transfer assets without holding native tokens. Network costs are included in the transfer.", - "numberOfSteps": "Each exchange step can contain 1-2 transactions that require a signature.", "priceImpact": "The estimated value difference between the source and destination tokens.", "progressToNextUpdate": "Quotes will update in {{value}} seconds. <0/> Click here to update now.", "selectAll": "Select all", @@ -286,6 +285,7 @@ "rateChange": "Rate change", "receiving": "Receiving", "refuelStepDetails": "Get gas via {{tool}}", + "route": "Route", "selectChain": "Select chain", "selectChainAndToken": "Select chain and token", "selectToken": "Select token", diff --git a/packages/widget/src/icons/lifi.ts b/packages/widget/src/icons/lifi.ts deleted file mode 100644 index 30c1bd692..000000000 --- a/packages/widget/src/icons/lifi.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const lifiLogoUrl = - 'https://lifinance.github.io/types/src/assets/icons/bridges/lifi.svg' diff --git a/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx b/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx index 9474dc1e0..ad2cbb21d 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx @@ -1,5 +1,5 @@ -import { Button } from '@mui/material' import { useTranslation } from 'react-i18next' +import { CardIconButton } from '../../components/Card/CardIconButton.js' import { useWidgetEvents } from '../../hooks/useWidgetEvents.js' import { WidgetEvent } from '../../types/events.js' @@ -25,8 +25,12 @@ export const ContactSupportButton = ({ } return ( - + ) } diff --git a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx index d9d5adcbc..b0df945e6 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx @@ -3,10 +3,11 @@ import { Box, Typography } from '@mui/material' import { useLocation, useNavigate } from '@tanstack/react-router' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { ContractComponent } from '../../components/ContractComponent/ContractComponent.js' +import { Card } from '../../components/Card/Card.js' import { PageContainer } from '../../components/PageContainer.js' -import { getStepList } from '../../components/Step/StepList.js' -import { TransactionDetails } from '../../components/TransactionDetails.js' +import { RouteCardEssentials } from '../../components/RouteCard/RouteCardEssentials.js' +import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { RouteTransactions } from '../../components/Step/RouteTransactions.js' import { internalExplorerUrl } from '../../config/constants.js' import { useExplorer } from '../../hooks/useExplorer.js' import { useHeader } from '../../hooks/useHeader.js' @@ -15,23 +16,15 @@ import { useTransactionDetails } from '../../hooks/useTransactionDetails.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { useRouteExecutionStore } from '../../stores/routes/RouteExecutionStore.js' import { getSourceTxHash } from '../../stores/routes/utils.js' -import { HiddenUI } from '../../types/widget.js' import { buildRouteFromTxHistory } from '../../utils/converters.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' -import { ContactSupportButton } from './ContactSupportButton.js' import { TransactionDetailsSkeleton } from './TransactionDetailsSkeleton.js' import { TransferIdCard } from './TransferIdCard.js' export const TransactionDetailsPage: React.FC = () => { const { t, i18n } = useTranslation() const navigate = useNavigate() - const { - subvariant, - subvariantOptions, - contractSecondaryComponent, - explorerUrls, - hiddenUI, - } = useWidgetConfig() + const { subvariant, subvariantOptions, explorerUrls } = useWidgetConfig() const { search }: any = useLocation() const { tools } = useTools() const { getTransactionLink } = useExplorer() @@ -97,58 +90,51 @@ export const TransactionDetailsPage: React.FC = () => { (storedRouteExecution ? 1 : 1000) // local and BE routes have different ms handling ) - return isLoading && !storedRouteExecution ? ( - - ) : ( + if (isLoading && !storedRouteExecution) { + return + } + + if (!routeExecution?.route) { + return null + } + + return ( - - - {startedAt.toLocaleString(i18n.language, { - dateStyle: 'long', - })} - - - {startedAt.toLocaleString(i18n.language, { - timeStyle: 'short', - })} - - - {getStepList(routeExecution?.route, subvariant)} - {subvariant === 'custom' && contractSecondaryComponent ? ( - - {contractSecondaryComponent} - - ) : null} - {routeExecution?.route ? ( - - ) : null} - - {!hiddenUI?.includes(HiddenUI.ContactSupport) ? ( + - + + {startedAt.toLocaleString(i18n.language, { + dateStyle: 'long', + })} + + + {startedAt.toLocaleString(i18n.language, { + timeStyle: 'short', + })} + - ) : null} + + + + + ) } diff --git a/packages/widget/src/pages/TransactionDetailsPage/TransferIdCard.tsx b/packages/widget/src/pages/TransactionDetailsPage/TransferIdCard.tsx index 0a7dd9e7c..5cde668d5 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/TransferIdCard.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/TransferIdCard.tsx @@ -5,6 +5,9 @@ import { useTranslation } from 'react-i18next' import { Card } from '../../components/Card/Card.js' import { CardIconButton } from '../../components/Card/CardIconButton.js' import { CardTitle } from '../../components/Card/CardTitle.js' +import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { HiddenUI } from '../../types/widget.js' +import { ContactSupportButton } from './ContactSupportButton.js' interface TransferIdCardProps { transferId: string @@ -13,6 +16,7 @@ interface TransferIdCardProps { export const TransferIdCard = ({ transferId, txLink }: TransferIdCardProps) => { const { t } = useTranslation() + const { hiddenUI } = useWidgetConfig() const copyTransferId = async () => { await navigator.clipboard.writeText(transferId) @@ -47,6 +51,9 @@ export const TransferIdCard = ({ transferId, txLink }: TransferIdCardProps) => { ) : null} + {!hiddenUI?.includes(HiddenUI.ContactSupport) ? ( + + ) : null} { const setBackAction = useHeaderStore((state) => state.setBackAction) const navigate = useNavigate() const navigateBack = useNavigateBack() - const { - subvariant, - subvariantOptions, - contractSecondaryComponent, - hiddenUI, - } = useWidgetConfig() + const { subvariant, subvariantOptions, hiddenUI } = useWidgetConfig() const { search }: any = useLocation() const stateRouteId = search?.routeId const [routeId, setRouteId] = useState(stateRouteId) @@ -211,13 +208,14 @@ export const TransactionPage = () => { return ( - {getStepList(route, subvariant)} - {subvariant === 'custom' && contractSecondaryComponent ? ( - - {contractSecondaryComponent} - - ) : null} - + + + + + + + + {status === RouteExecutionStatus.Idle || status === RouteExecutionStatus.Failed ? ( <> diff --git a/packages/widget/src/utils/getStatusColor.ts b/packages/widget/src/utils/getStatusColor.ts new file mode 100644 index 000000000..b689bd737 --- /dev/null +++ b/packages/widget/src/utils/getStatusColor.ts @@ -0,0 +1,31 @@ +import type { ExecutionActionStatus, Substatus } from '@lifi/sdk' +import type { Theme } from '@mui/material' + +/** + * Gets the background color for a status circle based on execution status + * @param theme - Material-UI theme + * @param status - Execution status + * @param substatus - Optional substatus for DONE status + * @returns RGBA color string + */ +export const getStatusColor = ( + theme: Theme, + status?: ExecutionActionStatus, + substatus?: Substatus +): string => { + switch (status) { + case 'ACTION_REQUIRED': + case 'MESSAGE_REQUIRED': + case 'RESET_REQUIRED': + return `rgba(${theme.vars.palette.info.mainChannel} / 0.12)` + case 'DONE': + if (substatus === 'PARTIAL' || substatus === 'REFUNDED') { + return `rgba(${theme.vars.palette.warning.mainChannel} / 0.12)` + } + return `rgba(${theme.vars.palette.success.mainChannel} / 0.12)` + case 'FAILED': + return `rgba(${theme.vars.palette.error.mainChannel} / 0.12)` + default: + return theme.vars.palette.info.mainChannel + } +} From 88347c594d64a9670546a0ffc9c34030a749a246 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Mon, 9 Mar 2026 11:04:53 +0000 Subject: [PATCH 02/22] feat: outline main page cards --- .../AmountInput/AmountInput.style.tsx | 90 +++++-- .../components/AmountInput/AmountInput.tsx | 98 ++++---- .../AmountInputAdornment.style.tsx | 73 ++---- .../AmountInput/AmountInputEndAdornment.tsx | 45 ++-- .../AmountInput/AmountInputStartAdornment.tsx | 26 -- .../AmountInput/PriceFormHelperText.tsx | 77 ++---- .../components/RouteCard/RouteCard.style.ts | 5 +- .../RouteCard/RouteCardEssentials.tsx | 22 +- .../widget/src/components/RouteCard/types.ts | 7 +- .../SelectTokenButton.style.tsx | 116 ++++----- .../SelectTokenButton/SelectTokenButton.tsx | 62 ++--- .../src/components/Step/ExecutionProgress.tsx | 4 +- .../src/components/Step/RouteDetails.tsx | 238 ++++++++++-------- .../src/components/Step/RouteTransactions.tsx | 4 +- .../components/Step/TokenWithExpansion.tsx | 12 +- .../WalletAddressBadge.style.tsx | 10 + .../WalletAddressBadge/WalletAddressBadge.tsx | 30 +++ 17 files changed, 458 insertions(+), 461 deletions(-) delete mode 100644 packages/widget/src/components/AmountInput/AmountInputStartAdornment.tsx create mode 100644 packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.style.tsx create mode 100644 packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx diff --git a/packages/widget/src/components/AmountInput/AmountInput.style.tsx b/packages/widget/src/components/AmountInput/AmountInput.style.tsx index 3a87a276e..7e007637b 100644 --- a/packages/widget/src/components/AmountInput/AmountInput.style.tsx +++ b/packages/widget/src/components/AmountInput/AmountInput.style.tsx @@ -4,30 +4,33 @@ import { inputBaseClasses, FormControl as MuiFormControl, styled, + Typography, } from '@mui/material' import { CardTitle } from '../Card/CardTitle.js' +import { InputCard } from '../Card/InputCard.js' -export const maxInputFontSize = 24 -export const minInputFontSize = 14 - -export const FormContainer = styled(Box)(({ theme }) => ({ +export const AmountInputCard = styled(InputCard)(({ theme }) => ({ display: 'flex', - alignItems: 'center', - padding: theme.spacing(2), + flexDirection: 'column', + gap: theme.spacing(1.5), + padding: theme.spacing(3), })) +export const maxInputFontSize = 40 +export const minInputFontSize = 14 + export const FormControl = styled(MuiFormControl)(() => ({ - height: 40, + flex: 1, })) export const Input = styled(InputBase)(({ theme }) => ({ - fontSize: 24, + fontSize: 40, fontWeight: 700, boxShadow: 'none', - lineHeight: 1.5, + lineHeight: 1.4, [`.${inputBaseClasses.input}`]: { - height: 24, - padding: theme.spacing(0, 0, 0, 2), + padding: 0, + height: 'auto', }, '& input[type="number"]::-webkit-outer-spin-button, & input[type="number"]::-webkit-inner-spin-button': { @@ -45,14 +48,67 @@ export const Input = styled(InputBase)(({ theme }) => ({ }, })) -export const AmountInputCardTitle = styled(CardTitle)(({ theme }) => ({ - padding: theme.spacing(2, 0, 0, 0), +export const AmountInputCardTitle = styled(CardTitle)(() => ({ + padding: 0, + fontSize: 14, + fontWeight: 700, + lineHeight: 1.4286, })) -export const AmountInputCardHeader = styled(Box)(({ theme }) => ({ - padding: theme.spacing(0, 2, 0, 2), +export const AmountInputCardHeader = styled(Box)(() => ({ display: 'flex', justifyContent: 'space-between', - alignItems: 'start', - height: 30, + alignItems: 'center', + width: '100%', +})) + +export const TokenAmountRow = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(2), + width: '100%', +})) + +export const LabelRow = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(2), + width: '100%', +})) + +export const LabelDescriptionColumn = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + flex: 1, + minWidth: 0, + gap: theme.spacing(0.5), +})) + +export const DescriptionRow = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + width: '100%', +})) + +export const DescriptionText = styled(Typography)(({ theme }) => ({ + fontSize: 12, + fontWeight: 500, + lineHeight: 1.3333, + color: theme.vars.palette.text.secondary, + whiteSpace: 'nowrap', +})) + +export const BalanceText = styled(Typography)(({ theme }) => ({ + fontSize: 12, + fontWeight: 500, + lineHeight: 1.3333, + color: theme.vars.palette.text.primary, + whiteSpace: 'nowrap', +})) + +export const PercentageRow = styled(Box)(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1), + width: '100%', })) diff --git a/packages/widget/src/components/AmountInput/AmountInput.tsx b/packages/widget/src/components/AmountInput/AmountInput.tsx index 3c463c4c9..f7749b96f 100644 --- a/packages/widget/src/components/AmountInput/AmountInput.tsx +++ b/packages/widget/src/components/AmountInput/AmountInput.tsx @@ -3,6 +3,7 @@ import type { CardProps } from '@mui/material' import type { ChangeEvent, ReactNode } from 'react' import { useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useChain } from '../../hooks/useChain.js' import { useToken } from '../../hooks/useToken.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { FormKeyHelper, type FormTypeProps } from '../../stores/form/types.js' @@ -17,18 +18,21 @@ import { usdDecimals, } from '../../utils/format.js' import { fitInputText } from '../../utils/input.js' -import { InputCard } from '../Card/InputCard.js' +import { AvatarBadgedDefault } from '../Avatar/Avatar.js' +import { TokenAvatar } from '../Avatar/TokenAvatar.js' +import { WalletAddressBadge } from '../WalletAddressBadge/WalletAddressBadge.js' import { + AmountInputCard, AmountInputCardHeader, AmountInputCardTitle, - FormContainer, FormControl, Input, + LabelDescriptionColumn, + LabelRow, maxInputFontSize, minInputFontSize, } from './AmountInput.style.js' import { AmountInputEndAdornment } from './AmountInputEndAdornment.js' -import { AmountInputStartAdornment } from './AmountInputStartAdornment.js' import { PriceFormHelperText } from './PriceFormHelperText.js' export const AmountInput: React.FC = ({ @@ -52,7 +56,6 @@ export const AmountInput: React.FC = ({ endAdornment={ !disabled ? : undefined } - bottomAdornment={} disabled={disabled} {...props} /> @@ -65,18 +68,9 @@ const AmountInputBase: React.FC< token?: Token startAdornment?: ReactNode endAdornment?: ReactNode - bottomAdornment?: ReactNode disabled?: boolean } -> = ({ - formType, - token, - startAdornment, - endAdornment, - bottomAdornment, - disabled, - ...props -}) => { +> = ({ formType, token, startAdornment, endAdornment, disabled, ...props }) => { const { t } = useTranslation() const { subvariant, subvariantOptions } = useWidgetConfig() const ref = useRef(null) @@ -85,10 +79,17 @@ const AmountInputBase: React.FC< const [formattedPriceInput, setFormattedPriceInput] = useState('') const amountKey = FormKeyHelper.getAmountKey(formType) + const [chainId, , toAddress] = useFieldValues( + FormKeyHelper.getChainKey(formType), + FormKeyHelper.getTokenKey(formType), + 'toAddress' + ) const [value] = useFieldValues(amountKey) const { setFieldValue } = useFieldActions() const { inputMode } = useInputModeStore() + const { chain } = useChain(chainId) + const currentInputMode = inputMode[formType] let displayValue: string if (isEditingRef.current) { @@ -170,40 +171,47 @@ const AmountInputBase: React.FC< : t('header.youPay') : t('header.send') + const isSelected = !!(chain && token) + return ( - + {title} - {endAdornment} + {toAddress ? : null} - - - - - {bottomAdornment} - - - + + + {startAdornment ?? + (isSelected ? ( + + ) : ( + + ))} + + + + + + + {endAdornment} + ) } diff --git a/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx b/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx index a596e3a14..ce581f146 100644 --- a/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx @@ -1,69 +1,28 @@ import { Box, styled } from '@mui/material' -import { cardClasses } from '@mui/material/Card' import { ButtonTertiary } from '../ButtonTertiary.js' export const ButtonContainer = styled(Box)(({ theme }) => ({ display: 'flex', - gap: theme.spacing(0.5), + gap: theme.spacing(1), })) -export const MaxButton = styled(ButtonTertiary)(({ theme }) => ({ - padding: theme.spacing(0.5, 1, 0.5, 1), - margin: theme.spacing(0, 0, 0, 0.5), - lineHeight: 1, +export const PercentagePill = styled(ButtonTertiary)(({ theme }) => ({ + padding: theme.spacing(0.5, 0.75), + lineHeight: 1.3333, fontSize: '0.75rem', + fontWeight: 700, minWidth: 'unset', - height: 24, - opacity: 0, - transform: 'scale(0.85) translateY(-10px)', - transition: - 'opacity 200ms cubic-bezier(0.4, 0, 0.2, 1), transform 200ms cubic-bezier(0.4, 0, 0.2, 1)', - '&[data-delay="0"]': { - [`.${cardClasses.root}:hover &`]: { - opacity: 1, - transform: 'scale(1) translateY(0)', - transitionDelay: '75ms', - }, - [`.${cardClasses.root}:not(:hover) &`]: { - opacity: 0, - transform: 'scale(0.85) translateY(-10px)', - transitionDelay: '0ms', - }, - }, - '&[data-delay="1"]': { - [`.${cardClasses.root}:hover &`]: { - opacity: 1, - transform: 'scale(1) translateY(0)', - transitionDelay: '50ms', - }, - [`.${cardClasses.root}:not(:hover) &`]: { - opacity: 0, - transform: 'scale(0.85) translateY(-10px)', - transitionDelay: '25ms', - }, + height: 'auto', + flex: 1, + borderRadius: 12, + backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, + '&:hover, &:active': { + backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.08)`, }, - '&[data-delay="2"]': { - [`.${cardClasses.root}:hover &`]: { - opacity: 1, - transform: 'scale(1) translateY(0)', - transitionDelay: '25ms', + ...theme.applyStyles('dark', { + backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, + '&:hover, &:active': { + backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.08)`, }, - [`.${cardClasses.root}:not(:hover) &`]: { - opacity: 0, - transform: 'scale(0.85) translateY(-10px)', - transitionDelay: '50ms', - }, - }, - '&[data-delay="3"]': { - [`.${cardClasses.root}:hover &`]: { - opacity: 1, - transform: 'scale(1) translateY(0)', - transitionDelay: '0ms', - }, - [`.${cardClasses.root}:not(:hover) &`]: { - opacity: 0, - transform: 'scale(0.85) translateY(-10px)', - transitionDelay: '75ms', - }, - }, + }), })) diff --git a/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx b/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx index 0bc4ab1d5..4b3f25975 100644 --- a/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx @@ -1,5 +1,4 @@ import { formatUnits } from '@lifi/sdk' -import { InputAdornment } from '@mui/material' import { memo } from 'react' import { useTranslation } from 'react-i18next' import { useAvailableChains } from '../../hooks/useAvailableChains.js' @@ -9,7 +8,10 @@ import type { FormTypeProps } from '../../stores/form/types.js' import { FormKeyHelper } from '../../stores/form/types.js' import { useFieldActions } from '../../stores/form/useFieldActions.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' -import { ButtonContainer, MaxButton } from './AmountInputAdornment.style.js' +import { + ButtonContainer, + PercentagePill, +} from './AmountInputAdornment.style.js' export const AmountInputEndAdornment = memo(({ formType }: FormTypeProps) => { const { t } = useTranslation() @@ -21,10 +23,7 @@ export const AmountInputEndAdornment = memo(({ formType }: FormTypeProps) => { FormKeyHelper.getTokenKey(formType) ) - // We get gas recommendations for the source chain to make sure that after pressing the Max button - // the user will have enough funds remaining to cover gas costs const { data } = useGasRecommendation(chainId) - const { token } = useTokenAddressBalance(chainId, tokenAddress) const getMaxAmount = () => { @@ -49,9 +48,7 @@ export const AmountInputEndAdornment = memo(({ formType }: FormTypeProps) => { setFieldValue( FormKeyHelper.getAmountKey(formType), formatUnits(percentageAmount, token.decimals), - { - isTouched: true, - } + { isTouched: true } ) } } @@ -62,31 +59,21 @@ export const AmountInputEndAdornment = memo(({ formType }: FormTypeProps) => { setFieldValue( FormKeyHelper.getAmountKey(formType), formatUnits(maxAmount, token.decimals), - { - isTouched: true, - } + { isTouched: true } ) } } + if (formType !== 'from' || !token?.amount) { + return null + } + return ( - - {formType === 'from' && token?.amount ? ( - - handlePercentage(25)} data-delay="0"> - 25% - - handlePercentage(50)} data-delay="1"> - 50% - - handlePercentage(75)} data-delay="2"> - 75% - - - {t('button.max')} - - - ) : null} - + + handlePercentage(25)}>25% + handlePercentage(50)}>50% + handlePercentage(75)}>75% + {t('button.max')} + ) }) diff --git a/packages/widget/src/components/AmountInput/AmountInputStartAdornment.tsx b/packages/widget/src/components/AmountInput/AmountInputStartAdornment.tsx deleted file mode 100644 index c06ba1632..000000000 --- a/packages/widget/src/components/AmountInput/AmountInputStartAdornment.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useChain } from '../../hooks/useChain.js' -import { useToken } from '../../hooks/useToken.js' -import type { FormTypeProps } from '../../stores/form/types.js' -import { FormKeyHelper } from '../../stores/form/types.js' -import { useFieldValues } from '../../stores/form/useFieldValues.js' -import { AvatarBadgedDefault } from '../Avatar/Avatar.js' -import { TokenAvatar } from '../Avatar/TokenAvatar.js' - -export const AmountInputStartAdornment: React.FC = ({ - formType, -}) => { - const [chainId, tokenAddress] = useFieldValues( - FormKeyHelper.getChainKey(formType), - FormKeyHelper.getTokenKey(formType) - ) - - const { chain } = useChain(chainId) - const { token } = useToken(chainId, tokenAddress) - const isSelected = !!(chain && token) - - return isSelected ? ( - - ) : ( - - ) -} diff --git a/packages/widget/src/components/AmountInput/PriceFormHelperText.tsx b/packages/widget/src/components/AmountInput/PriceFormHelperText.tsx index 8155418e8..6db587711 100644 --- a/packages/widget/src/components/AmountInput/PriceFormHelperText.tsx +++ b/packages/widget/src/components/AmountInput/PriceFormHelperText.tsx @@ -1,6 +1,6 @@ import type { TokenAmount } from '@lifi/sdk' import SwapVertIcon from '@mui/icons-material/SwapVert' -import { FormHelperText, Skeleton, Typography } from '@mui/material' +import { Skeleton } from '@mui/material' import { memo } from 'react' import { useTranslation } from 'react-i18next' import { useTokenAddressBalance } from '../../hooks/useTokenAddressBalance.js' @@ -9,6 +9,11 @@ import { FormKeyHelper } from '../../stores/form/types.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' import { useInputModeStore } from '../../stores/inputMode/useInputModeStore.js' import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js' +import { + BalanceText, + DescriptionRow, + DescriptionText, +} from './AmountInput.style.js' import { InputPriceButton } from './PriceFormHelperText.style.js' export const PriceFormHelperText = memo(({ formType }) => { @@ -53,9 +58,8 @@ const PriceFormHelperTextBase: React.FC< token?.decimals ) return t('format.currency', { value: tokenPrice }) - } else { - return t('format.tokenAmount', { value: amount || '0' }) } + return t('format.tokenAmount', { value: amount || '0' }) } const handleToggleMode = (e: React.MouseEvent) => { @@ -64,70 +68,27 @@ const PriceFormHelperTextBase: React.FC< } return ( - + - - {getPriceAmountDisplayValue()} - + {getPriceAmountDisplayValue()} {currentInputMode === 'price' && token?.symbol ? ( - - {token.symbol} - + {token.symbol} + ) : null} + {token?.priceUSD ? ( + ) : null} - {token?.priceUSD && } {isLoading && tokenAddress ? ( ) : token?.amount ? ( - - {`/ ${t('format.tokenAmount', { - value: tokenAmount, - })}`} - + + {`/ ${t('format.tokenAmount', { value: tokenAmount })}`} + ) : null} - + ) } diff --git a/packages/widget/src/components/RouteCard/RouteCard.style.ts b/packages/widget/src/components/RouteCard/RouteCard.style.ts index bffe01b61..d91ca16d2 100644 --- a/packages/widget/src/components/RouteCard/RouteCard.style.ts +++ b/packages/widget/src/components/RouteCard/RouteCard.style.ts @@ -1,8 +1,7 @@ import { Box, styled } from '@mui/material' -export const TokenContainer = styled(Box)(() => ({ +export const TokenContainer = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', - justifyContent: 'space-between', - height: 40, + gap: theme.spacing(3), })) diff --git a/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx b/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx index a528289eb..89f88ff32 100644 --- a/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx +++ b/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx @@ -1,4 +1,3 @@ -import type { RouteExtended } from '@lifi/sdk' import AccessTimeFilled from '@mui/icons-material/AccessTimeFilled' import LocalGasStationRounded from '@mui/icons-material/LocalGasStationRounded' import { Box, Tooltip, Typography } from '@mui/material' @@ -8,11 +7,7 @@ import { formatDuration } from '../../utils/format.js' import { FeeBreakdownTooltip } from '../FeeBreakdownTooltip.js' import { IconTypography } from '../IconTypography.js' import { TokenRate } from '../TokenRate/TokenRate.js' - -export interface RouteCardEssentialsProps { - route: RouteExtended - showDuration?: boolean -} +import type { RouteCardEssentialsProps } from './types.js' export const RouteCardEssentials: React.FC = ({ route, @@ -36,15 +31,11 @@ export const RouteCardEssentials: React.FC = ({ justifyContent: 'space-between', flex: 1, mt: 2, + gap: 1.5, }} > - + = ({ - + = ({ sx={{ display: 'flex', alignItems: 'center', + gap: 0.75, }} > - + - !['selected', 'compact'].includes(prop as string), -})<{ selected?: boolean; compact?: boolean }>( - ({ theme, selected, compact }) => ({ - padding: theme.spacing(2), - [`.${cardHeaderClasses.title}`]: { - color: theme.vars.palette.text.secondary, - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', - width: 256, - fontSize: compact && !selected ? 16 : 18, - fontWeight: 500, - [theme.breakpoints.down(theme.breakpoints.values.sm)]: { - width: 224, - }, - [theme.breakpoints.down(theme.breakpoints.values.xs)]: { - width: 180, - fontSize: 16, - }, - }, - [`.${cardHeaderClasses.subheader}`]: { - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', - width: 256, - [theme.breakpoints.down(theme.breakpoints.values.sm)]: { - width: 224, - }, - [theme.breakpoints.down(theme.breakpoints.values.xs)]: { - width: 180, - }, - }, - variants: [ - { - props: ({ selected }) => selected, - style: { - [`.${cardHeaderClasses.title}`]: { - color: theme.vars.palette.text.primary, - fontWeight: 600, - }, - }, - }, - { - props: ({ compact }) => compact, - style: { - [`.${cardHeaderClasses.title}`]: { - width: 96, - [theme.breakpoints.down(theme.breakpoints.values.sm)]: { - width: 96, - }, - }, - [`.${cardHeaderClasses.subheader}`]: { - width: 96, - [theme.breakpoints.down(theme.breakpoints.values.sm)]: { - width: 96, - }, - }, - }, - }, - ], - }) -) export const SelectTokenCard = styled(Card)(({ theme }) => { const cardVariant = theme.components?.MuiCard?.defaultProps?.variant return { flex: 1, + display: 'flex', + flexDirection: 'column', ...(cardVariant !== 'outlined' && { background: 'none', '&:hover': { @@ -96,13 +33,17 @@ export const CardContent = styled(MuiCardContent, { const horizontal = compact ? direction : '50%' const vertical = compact ? '50%' : direction return { - padding: 0, + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1.5), + padding: theme.spacing(3), + flex: 1, transition: theme.transitions.create(['background-color'], { duration: theme.transitions.duration.enteringScreen, easing: theme.transitions.easing.easeOut, }), '&:last-child': { - paddingBottom: 0, + paddingBottom: theme.spacing(3), }, ...(cardVariant !== 'outlined' && { backgroundColor: theme.vars.palette.background.paper, @@ -122,3 +63,40 @@ export const CardContent = styled(MuiCardContent, { } } ) + +export const AvatarItemRow = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(2), +})) + +export const TokenLabelColumn = styled(Box)(() => ({ + display: 'flex', + flexDirection: 'column', + minWidth: 0, + flex: 1, +})) + +export const TokenNameText = styled(Typography, { + shouldForwardProp: (prop) => prop !== 'selected', +})<{ selected?: boolean }>(({ theme, selected }) => ({ + fontSize: 18, + fontWeight: 700, + lineHeight: 1.3333, + color: selected + ? theme.vars.palette.text.primary + : theme.vars.palette.text.secondary, + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + overflow: 'hidden', +})) + +export const ChainNameText = styled(Typography)(({ theme }) => ({ + fontSize: 14, + fontWeight: 500, + lineHeight: 1.2857, + color: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.48)`, + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + overflow: 'hidden', +})) diff --git a/packages/widget/src/components/SelectTokenButton/SelectTokenButton.tsx b/packages/widget/src/components/SelectTokenButton/SelectTokenButton.tsx index ad8e3ea86..7b6090d9f 100644 --- a/packages/widget/src/components/SelectTokenButton/SelectTokenButton.tsx +++ b/packages/widget/src/components/SelectTokenButton/SelectTokenButton.tsx @@ -16,9 +16,12 @@ import { AvatarBadgedDefault, AvatarBadgedSkeleton } from '../Avatar/Avatar.js' import { TokenAvatar } from '../Avatar/TokenAvatar.js' import { CardTitle } from '../Card/CardTitle.js' import { + AvatarItemRow, CardContent, + ChainNameText, SelectTokenCard, - SelectTokenCardHeader, + TokenLabelColumn, + TokenNameText, } from './SelectTokenButton.style.js' export const SelectTokenButton: React.FC< @@ -67,41 +70,38 @@ export const SelectTokenButton: React.FC< formType === 'from' && subvariant === 'custom' ? t('header.payWith') : t(`main.${formType}`) + return ( - {cardTitle} + {cardTitle} {chainId && tokenAddress && (isChainLoading || isTokenLoading) ? ( - } - title={} - subheader={} - compact={compact} - /> + + + + + + + ) : ( - - ) : ( - - ) - } - title={isSelected ? token.symbol : defaultPlaceholder} - slotProps={{ - title: { - title: isSelected ? token.symbol : defaultPlaceholder, - }, - subheader: { - title: isSelected ? chain.name : undefined, - }, - }} - subheader={isSelected ? chain.name : null} - selected={isSelected} - compact={compact} - /> + + {isSelected ? ( + + ) : ( + + )} + + + {isSelected ? token.symbol : defaultPlaceholder} + + {isSelected ? ( + {chain.name} + ) : null} + + )} diff --git a/packages/widget/src/components/Step/ExecutionProgress.tsx b/packages/widget/src/components/Step/ExecutionProgress.tsx index 9ea7cc2ff..7a2e555e1 100644 --- a/packages/widget/src/components/Step/ExecutionProgress.tsx +++ b/packages/widget/src/components/Step/ExecutionProgress.tsx @@ -1,7 +1,7 @@ import type { RouteExtended } from '@lifi/sdk' import { Box, Typography } from '@mui/material' -import { useActionMessage } from '../../hooks/useActionMessage' -import { CircularProgress } from './CircularProgress' +import { useActionMessage } from '../../hooks/useActionMessage.js' +import { CircularProgress } from './CircularProgress.js' export const ExecutionProgress: React.FC<{ route: RouteExtended diff --git a/packages/widget/src/components/Step/RouteDetails.tsx b/packages/widget/src/components/Step/RouteDetails.tsx index 40718c002..fe153adcf 100644 --- a/packages/widget/src/components/Step/RouteDetails.tsx +++ b/packages/widget/src/components/Step/RouteDetails.tsx @@ -1,14 +1,15 @@ import type { RouteExtended } from '@lifi/sdk' import { useEthereumContext } from '@lifi/widget-provider' +import InfoOutlined from '@mui/icons-material/InfoOutlined' import { Box, Tooltip, Typography } from '@mui/material' import { useTranslation } from 'react-i18next' -import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider' -import { isRouteDone } from '../../stores/routes/utils' -import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees' -import { formatTokenAmount, formatTokenPrice } from '../../utils/format' -import { getPriceImpact } from '../../utils/getPriceImpact' -import { FeeBreakdownTooltip } from '../FeeBreakdownTooltip' -import { StepActions } from '../StepActions/StepActions' +import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { isRouteDone } from '../../stores/routes/utils.js' +import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js' +import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js' +import { getPriceImpact } from '../../utils/getPriceImpact.js' +import { FeeBreakdownTooltip } from '../FeeBreakdownTooltip.js' +import { StepActions } from '../StepActions/StepActions.js' interface RouteDetailsProps { route: RouteExtended @@ -63,7 +64,7 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { (feeAmountUSD || Number.isFinite(feeConfig?.fee)) && !hasGaslessSupport return ( - + {route.steps.map((step) => ( ))} @@ -71,39 +72,54 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { sx={{ display: 'flex', justifyContent: 'space-between', - mb: 0.5, + alignItems: 'center', + gap: 1, }} > - {t('main.fees.network')} - - - {!gasCostUSD - ? t('main.fees.free') - : t('format.currency', { - value: gasCostUSD, - })} - - + + {t('main.fees.network')} + + + + + + {!gasCostUSD + ? t('main.fees.free') + : t('format.currency', { + value: gasCostUSD, + })} + {feeCosts.length ? ( - {t('main.fees.provider')} - - - {t('format.currency', { - value: feeCostUSD, - })} - - + + {t('main.fees.provider')} + + + + + + {t('format.currency', { + value: feeCostUSD, + })} + ) : null} {showIntegratorFeeCollectionDetails ? ( @@ -111,57 +127,66 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { sx={{ display: 'flex', justifyContent: 'space-between', - mb: 0.5, + alignItems: 'center', + gap: 1, }} > - - {feeConfig?.name || t('main.fees.defaultIntegrator')} - {feeConfig?.showFeePercentage && ( - <> ({t('format.percent', { value: feePercentage })}) - )} - - {feeConfig?.showFeeTooltip && - (feeConfig?.name || feeConfig?.feeTooltipComponent) ? ( - - - {t('format.currency', { - value: feeAmountUSD, - })} - - - ) : ( - - {t('format.currency', { - value: feeAmountUSD, - })} + + + {feeConfig?.name || t('main.fees.defaultIntegrator')} + {feeConfig?.showFeePercentage && ( + <> ({t('format.percent', { value: feePercentage })}) + )} - )} + {feeConfig?.showFeeTooltip && + (feeConfig?.name || feeConfig?.feeTooltipComponent) ? ( + + + + ) : null} + + + {t('format.currency', { + value: feeAmountUSD, + })} + ) : null} - {t('main.priceImpact')} - - - {t('format.percent', { - value: priceImpact, - usePlusSign: true, - })} - - + + {t('main.priceImpact')} + + + + + + {t('format.percent', { + value: priceImpact, + usePlusSign: true, + })} + {!isRouteDone(route) ? ( <> @@ -169,44 +194,57 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { sx={{ display: 'flex', justifyContent: 'space-between', - mb: 0.5, + alignItems: 'center', + gap: 1, }} > - {t('main.maxSlippage')} - - - {route.steps[0].action.slippage - ? t('format.percent', { - value: route.steps[0].action.slippage, - }) - : t('button.auto')} - - + + {t('main.maxSlippage')} + + + + + + {route.steps[0].action.slippage + ? t('format.percent', { + value: route.steps[0].action.slippage, + }) + : t('button.auto')} + - {t('main.minReceived')} - - - {t('format.tokenAmount', { - value: formatTokenAmount( - BigInt(route.toAmountMin), - route.toToken.decimals - ), - })}{' '} - {route.toToken.symbol} - - + + {t('main.minReceived')} + + + + + + {t('format.tokenAmount', { + value: formatTokenAmount( + BigInt(route.toAmountMin), + route.toToken.decimals + ), + })}{' '} + {route.toToken.symbol} + ) : null} diff --git a/packages/widget/src/components/Step/RouteTransactions.tsx b/packages/widget/src/components/Step/RouteTransactions.tsx index 32329f952..0be437e9f 100644 --- a/packages/widget/src/components/Step/RouteTransactions.tsx +++ b/packages/widget/src/components/Step/RouteTransactions.tsx @@ -1,7 +1,7 @@ import type { RouteExtended } from '@lifi/sdk' import { Box } from '@mui/material' -import { prepareActions } from '../../utils/prepareActions' -import { StepTransactionLink } from './TransactionLink' +import { prepareActions } from '../../utils/prepareActions.js' +import { StepTransactionLink } from './TransactionLink.js' export const RouteTransactions: React.FC<{ route: RouteExtended diff --git a/packages/widget/src/components/Step/TokenWithExpansion.tsx b/packages/widget/src/components/Step/TokenWithExpansion.tsx index bc087f57d..6195f325c 100644 --- a/packages/widget/src/components/Step/TokenWithExpansion.tsx +++ b/packages/widget/src/components/Step/TokenWithExpansion.tsx @@ -3,12 +3,12 @@ import ExpandLess from '@mui/icons-material/ExpandLess' import ExpandMore from '@mui/icons-material/ExpandMore' import { Box, Collapse } from '@mui/material' import { type MouseEventHandler, useState } from 'react' -import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider' -import { HiddenUI } from '../../types/widget' -import { CardIconButton } from '../Card/CardIconButton' -import { TokenContainer } from '../RouteCard/RouteCard.style' -import { Token } from '../Token/Token' -import { RouteDetails } from './RouteDetails' +import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { HiddenUI } from '../../types/widget.js' +import { CardIconButton } from '../Card/CardIconButton.js' +import { TokenContainer } from '../RouteCard/RouteCard.style.js' +import { Token } from '../Token/Token.js' +import { RouteDetails } from './RouteDetails.js' interface TokenWithExpansionProps { route: RouteExtended diff --git a/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.style.tsx b/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.style.tsx new file mode 100644 index 000000000..8d086deaa --- /dev/null +++ b/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.style.tsx @@ -0,0 +1,10 @@ +import { Box, styled } from '@mui/material' + +export const BadgeRoot = styled(Box)(({ theme }) => ({ + display: 'inline-flex', + alignItems: 'center', + gap: theme.spacing(0.5), + padding: theme.spacing(0.5, 0.75), + borderRadius: 12, + backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, +})) diff --git a/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx b/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx new file mode 100644 index 000000000..8d4708daf --- /dev/null +++ b/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx @@ -0,0 +1,30 @@ +import Wallet from '@mui/icons-material/Wallet' +import { Typography } from '@mui/material' +import { shortenAddress } from '../../utils/wallet.js' +import { BadgeRoot } from './WalletAddressBadge.style.js' + +interface WalletAddressBadgeProps { + address: string +} + +export const WalletAddressBadge: React.FC = ({ + address, +}) => { + return ( + + + + {shortenAddress(address)} + + + ) +} From 443bc1e367e3599906634602036cd193f1faec4e Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Mon, 9 Mar 2026 12:11:25 +0000 Subject: [PATCH 03/22] style: transaction details --- .../AmountInput/AmountInput.style.tsx | 2 +- .../src/components/Card/CardIconButton.tsx | 4 +- .../components/Step/RouteDetails.style.tsx | 34 ++++ .../src/components/Step/RouteDetails.tsx | 185 ++++++------------ .../src/components/Step/RouteTokens.tsx | 2 +- .../components/Step/TokenWithExpansion.tsx | 6 +- .../components/StepActions/StepActions.tsx | 77 ++++---- .../widget/src/components/Token/Token.tsx | 4 + .../TransactionDetailsPage.tsx | 4 +- .../TransactionDetailsPage/TransferIdCard.tsx | 13 +- 10 files changed, 150 insertions(+), 181 deletions(-) create mode 100644 packages/widget/src/components/Step/RouteDetails.style.tsx diff --git a/packages/widget/src/components/AmountInput/AmountInput.style.tsx b/packages/widget/src/components/AmountInput/AmountInput.style.tsx index 7e007637b..f89ba3eff 100644 --- a/packages/widget/src/components/AmountInput/AmountInput.style.tsx +++ b/packages/widget/src/components/AmountInput/AmountInput.style.tsx @@ -23,7 +23,7 @@ export const FormControl = styled(MuiFormControl)(() => ({ flex: 1, })) -export const Input = styled(InputBase)(({ theme }) => ({ +export const Input = styled(InputBase)(() => ({ fontSize: 40, fontWeight: 700, boxShadow: 'none', diff --git a/packages/widget/src/components/Card/CardIconButton.tsx b/packages/widget/src/components/Card/CardIconButton.tsx index 1f53c4b81..c22e0f2a6 100644 --- a/packages/widget/src/components/Card/CardIconButton.tsx +++ b/packages/widget/src/components/Card/CardIconButton.tsx @@ -5,12 +5,12 @@ export const CardIconButton = styled(MuiIconButton)< IconButtonProps & Pick >(({ theme }) => { return { - padding: theme.spacing(0.5), + padding: theme.spacing(1), backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, '&:hover': { backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.08)`, }, fontSize: '1rem', - borderRadius: theme.vars.shape.borderRadiusTertiary, + borderRadius: theme.vars.shape.borderRadiusSecondary, } }) diff --git a/packages/widget/src/components/Step/RouteDetails.style.tsx b/packages/widget/src/components/Step/RouteDetails.style.tsx new file mode 100644 index 000000000..a275b8b55 --- /dev/null +++ b/packages/widget/src/components/Step/RouteDetails.style.tsx @@ -0,0 +1,34 @@ +import { Box, styled, Typography } from '@mui/material' + +export const DetailRow = styled(Box)(() => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + gap: 8, +})) + +export const DetailLabelContainer = styled(Box)(() => ({ + display: 'flex', + alignItems: 'center', + gap: 4, +})) + +export const DetailLabel = styled(Typography)(({ theme }) => ({ + fontSize: 12, + fontWeight: 500, + lineHeight: '16px', + color: theme.vars.palette.text.secondary, +})) + +export const DetailValue = styled(Typography)(() => ({ + fontSize: 12, + fontWeight: 700, + lineHeight: '16px', + textAlign: 'right', +})) + +export const DetailInfoIcon = { + fontSize: 16, + color: 'text.secondary', + cursor: 'help', +} as const diff --git a/packages/widget/src/components/Step/RouteDetails.tsx b/packages/widget/src/components/Step/RouteDetails.tsx index fe153adcf..66f5868c9 100644 --- a/packages/widget/src/components/Step/RouteDetails.tsx +++ b/packages/widget/src/components/Step/RouteDetails.tsx @@ -1,7 +1,7 @@ import type { RouteExtended } from '@lifi/sdk' import { useEthereumContext } from '@lifi/widget-provider' import InfoOutlined from '@mui/icons-material/InfoOutlined' -import { Box, Tooltip, Typography } from '@mui/material' +import { Box, Tooltip } from '@mui/material' import { useTranslation } from 'react-i18next' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { isRouteDone } from '../../stores/routes/utils.js' @@ -10,6 +10,13 @@ import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js' import { getPriceImpact } from '../../utils/getPriceImpact.js' import { FeeBreakdownTooltip } from '../FeeBreakdownTooltip.js' import { StepActions } from '../StepActions/StepActions.js' +import { + DetailInfoIcon, + DetailLabel, + DetailLabelContainer, + DetailRow, + DetailValue, +} from './RouteDetails.style.js' interface RouteDetailsProps { route: RouteExtended @@ -64,80 +71,47 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { (feeAmountUSD || Number.isFinite(feeConfig?.fee)) && !hasGaslessSupport return ( - - {route.steps.map((step) => ( - - ))} - - - {t('main.fees.network')} + + + + + {t('main.fees.network')} - + - - + + {!gasCostUSD ? t('main.fees.free') : t('format.currency', { value: gasCostUSD, })} - - + + {feeCosts.length ? ( - - - {t('main.fees.provider')} + + + {t('main.fees.provider')} - + - - + + {t('format.currency', { value: feeCostUSD, })} - - + + ) : null} {showIntegratorFeeCollectionDetails ? ( - - - + + + {feeConfig?.name || t('main.fees.defaultIntegrator')} {feeConfig?.showFeePercentage && ( <> ({t('format.percent', { value: feePercentage })}) )} - + {feeConfig?.showFeeTooltip && (feeConfig?.name || feeConfig?.feeTooltipComponent) ? ( { t('tooltip.feeCollection', { tool: feeConfig.name }) } > - + ) : null} - - + + {t('format.currency', { value: feeAmountUSD, })} - - + + ) : null} - - - {t('main.priceImpact')} + + + {t('main.priceImpact')} - + - - + + {t('format.percent', { value: priceImpact, usePlusSign: true, })} - - + + {!isRouteDone(route) ? ( <> - - - {t('main.maxSlippage')} + + + {t('main.maxSlippage')} - + - - + + {route.steps[0].action.slippage ? t('format.percent', { value: route.steps[0].action.slippage, }) : t('button.auto')} - - - - - {t('main.minReceived')} + + + + + {t('main.minReceived')} - + - - + + {t('format.tokenAmount', { value: formatTokenAmount( BigInt(route.toAmountMin), @@ -244,8 +177,8 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { ), })}{' '} {route.toToken.symbol} - - + + ) : null} diff --git a/packages/widget/src/components/Step/RouteTokens.tsx b/packages/widget/src/components/Step/RouteTokens.tsx index 098979009..07c406dd4 100644 --- a/packages/widget/src/components/Step/RouteTokens.tsx +++ b/packages/widget/src/components/Step/RouteTokens.tsx @@ -32,7 +32,7 @@ export const RouteTokens: React.FC<{ } return ( - + {fromToken ? : null} {!defaultExpanded ? ( - + {cardExpanded ? ( ) : ( diff --git a/packages/widget/src/components/StepActions/StepActions.tsx b/packages/widget/src/components/StepActions/StepActions.tsx index 60df84097..07d143a8b 100644 --- a/packages/widget/src/components/StepActions/StepActions.tsx +++ b/packages/widget/src/components/StepActions/StepActions.tsx @@ -1,4 +1,4 @@ -import type { LiFiStep, StepExtended } from '@lifi/sdk' +import type { LiFiStep, RouteExtended, StepExtended } from '@lifi/sdk' import { useEthereumContext } from '@lifi/widget-provider' import ArrowForward from '@mui/icons-material/ArrowForward' import ExpandLess from '@mui/icons-material/ExpandLess' @@ -26,17 +26,11 @@ import { StepLabel, StepLabelTypography, } from './StepActions.style.js' -import type { - IncludedStepsProps, - StepActionsProps, - StepDetailsLabelProps, -} from './types.js' +import type { IncludedStepsProps, StepDetailsLabelProps } from './types.js' -export const StepActions: React.FC = ({ - step, - dense, - ...other -}) => { +export const StepActions: React.FC<{ + route: RouteExtended +}> = ({ route }) => { const { t } = useTranslation() const [cardExpanded, setCardExpanded] = useState(false) @@ -45,8 +39,10 @@ export const StepActions: React.FC = ({ setCardExpanded((expanded) => !expanded) } + const includedSteps = route.steps.flatMap((step) => step.includedSteps) + return ( - + = ({ {t('main.route')} - {dense ? ( - - {cardExpanded ? ( - - ) : ( - - {step.includedSteps.map((includedStep, index) => ( - 0 ? -0.5 : 0 }} - > - {includedStep.toolDetails.name[0]} - - ))} - - - )} - - ) : null} + + {cardExpanded ? ( + + ) : ( + + {includedSteps.map((includedStep, index) => ( + 0 ? 0 : -0.75, + width: 20, + height: 20, + mr: 0.5, + }} + > + {includedStep.toolDetails.name[0]} + + ))} + + + )} + - {dense ? ( - - - - ) : ( - - )} + + {route.steps.map((step) => ( + + ))} + ) } diff --git a/packages/widget/src/components/Token/Token.tsx b/packages/widget/src/components/Token/Token.tsx index 5b48c931b..466e6b60f 100644 --- a/packages/widget/src/components/Token/Token.tsx +++ b/packages/widget/src/components/Token/Token.tsx @@ -141,6 +141,10 @@ const TokenBase: FC = ({ value: tokenPrice, })} + + • + + {token.symbol} {impactToken ? ( • diff --git a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx index b0df945e6..4d122ead9 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx @@ -100,10 +100,10 @@ export const TransactionDetailsPage: React.FC = () => { return ( - + { } return ( - + - {t('main.transferId')} + {t('main.transferId')} @@ -59,9 +58,7 @@ export const TransferIdCard = ({ transferId, txLink }: TransferIdCardProps) => { From d4d6fecc2bf61f14ffbcdb50c8d65c58117818d5 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Mon, 9 Mar 2026 15:49:58 +0000 Subject: [PATCH 04/22] feat: replace active transactions with activity center --- packages/widget/src/AppDefault.tsx | 29 +--- .../ActiveTransactionItem.tsx | 108 ------------ .../ActiveTransactions.style.ts | 28 --- .../ActiveTransactions/ActiveTransactions.tsx | 47 ------ .../Header/TransactionHistoryButton.style.tsx | 21 +++ .../Header/TransactionHistoryButton.tsx | 59 ++++++- .../Step/CircularProgress.style.tsx | 34 ++++ .../src/components/Step/CircularProgress.tsx | 84 +++------ .../components/Step/CircularProgressTimer.tsx | 94 +++++++++++ .../src/components/Step/ExecutionProgress.tsx | 8 +- .../src/components/Step/RouteTransactions.tsx | 68 +++++++- .../components/Step/TransactionLink.style.tsx | 5 +- .../src/components/Step/TransactionLink.tsx | 20 +-- packages/widget/src/i18n/en.json | 1 + .../ActiveTransactionsEmpty.tsx | 45 ----- .../ActiveTransactionsPage.tsx | 96 ----------- .../widget/src/pages/MainPage/MainPage.tsx | 2 - .../ActiveTransactionCard.style.tsx | 47 ++++++ .../ActiveTransactionCard.tsx | 104 ++++++++++++ .../TransactionHistoryItem.tsx | 120 ++++--------- .../TransactionHistoryPage.tsx | 30 +++- .../TransactionHistorySkeleton.tsx | 80 ++++----- .../TransactionPage/StatusBottomSheet.tsx | 22 ++- .../TokenValueBottomSheet.style.tsx | 43 +++++ .../TransactionPage/TokenValueBottomSheet.tsx | 159 +++++------------- .../pages/TransactionPage/TransactionPage.tsx | 17 +- .../src/stores/routes/useHasFailedRoutes.ts | 16 ++ packages/widget/src/utils/getStatusColor.ts | 6 +- packages/widget/src/utils/navigationRoutes.ts | 3 - 29 files changed, 701 insertions(+), 695 deletions(-) delete mode 100644 packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx delete mode 100644 packages/widget/src/components/ActiveTransactions/ActiveTransactions.style.ts delete mode 100644 packages/widget/src/components/ActiveTransactions/ActiveTransactions.tsx create mode 100644 packages/widget/src/components/Header/TransactionHistoryButton.style.tsx create mode 100644 packages/widget/src/components/Step/CircularProgress.style.tsx create mode 100644 packages/widget/src/components/Step/CircularProgressTimer.tsx delete mode 100644 packages/widget/src/pages/ActiveTransactionsPage/ActiveTransactionsEmpty.tsx delete mode 100644 packages/widget/src/pages/ActiveTransactionsPage/ActiveTransactionsPage.tsx create mode 100644 packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx create mode 100644 packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx create mode 100644 packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.style.tsx create mode 100644 packages/widget/src/stores/routes/useHasFailedRoutes.ts diff --git a/packages/widget/src/AppDefault.tsx b/packages/widget/src/AppDefault.tsx index 54f92fc41..fb66397be 100644 --- a/packages/widget/src/AppDefault.tsx +++ b/packages/widget/src/AppDefault.tsx @@ -8,7 +8,6 @@ import { } from '@tanstack/react-router' import { AppLayout } from './AppLayout.js' import { NotFound } from './components/NotFound.js' -import { ActiveTransactionsPage } from './pages/ActiveTransactionsPage/ActiveTransactionsPage.js' import { LanguagesPage } from './pages/LanguagesPage.js' import { MainPage } from './pages/MainPage/MainPage.js' import { RoutesPage } from './pages/RoutesPage/RoutesPage.js' @@ -134,23 +133,6 @@ const routesTransactionExecutionDetailsRoute = createRoute({ component: TransactionDetailsPage, }) -const activeTransactionsLayoutRoute = createRoute({ - getParentRoute: () => rootRoute, - path: navigationRoutes.activeTransactions, -}) - -const activeTransactionsIndexRoute = createRoute({ - getParentRoute: () => activeTransactionsLayoutRoute, - path: '/', - component: ActiveTransactionsPage, -}) - -const activeTransactionExecutionRoute = createRoute({ - getParentRoute: () => activeTransactionsLayoutRoute, - path: navigationRoutes.transactionExecution, - component: TransactionPage, -}) - const sendToWalletLayoutRoute = createRoute({ getParentRoute: () => rootRoute, path: navigationRoutes.sendToWallet, @@ -203,6 +185,12 @@ const transactionHistoryDetailsRoute = createRoute({ component: TransactionDetailsPage, }) +const transactionHistoryExecutionRoute = createRoute({ + getParentRoute: () => transactionHistoryLayoutRoute, + path: navigationRoutes.transactionExecution, + component: TransactionPage, +}) + const transactionExecutionLayoutRoute = createRoute({ getParentRoute: () => rootRoute, path: navigationRoutes.transactionExecution, @@ -245,10 +233,6 @@ const routeTree = rootRoute.addChildren([ transactionExecutionIndexRoute, transactionExecutionDetailsRoute, ]), - activeTransactionsLayoutRoute.addChildren([ - activeTransactionsIndexRoute, - activeTransactionExecutionRoute, - ]), sendToWalletLayoutRoute.addChildren([ sendToWalletIndexRoute, sendToWalletBookmarksRoute, @@ -259,6 +243,7 @@ const routeTree = rootRoute.addChildren([ transactionHistoryLayoutRoute.addChildren([ transactionHistoryIndexRoute, transactionHistoryDetailsRoute, + transactionHistoryExecutionRoute, ]), ]) diff --git a/packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx b/packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx deleted file mode 100644 index 98eff9687..000000000 --- a/packages/widget/src/components/ActiveTransactions/ActiveTransactionItem.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import ArrowForward from '@mui/icons-material/ArrowForward' -import ErrorRounded from '@mui/icons-material/ErrorRounded' -import InfoRounded from '@mui/icons-material/InfoRounded' -import { - CircularProgress, - ListItemAvatar, - ListItemText, - Typography, -} from '@mui/material' -import { useNavigate } from '@tanstack/react-router' -import { useActionMessage } from '../../hooks/useActionMessage.js' -import { useRouteExecution } from '../../hooks/useRouteExecution.js' -import { RouteExecutionStatus } from '../../stores/routes/types.js' -import { navigationRoutes } from '../../utils/navigationRoutes.js' -import { TokenAvatarGroup } from '../Avatar/Avatar.style.js' -import { TokenAvatar } from '../Avatar/TokenAvatar.js' -import { ListItem, ListItemButton } from './ActiveTransactions.style.js' - -// TODO: This will get replaced with transaction history / activity center -export const ActiveTransactionItem: React.FC<{ - routeId: string - dense?: boolean -}> = ({ routeId, dense }) => { - const navigate = useNavigate() - const { route, status } = useRouteExecution({ - routeId, - executeInBackground: true, - }) - - const lastActiveStep = route?.steps.findLast((step) => step.execution) - const lastActiveAction = lastActiveStep?.execution?.actions?.at(-1) - - const { title } = useActionMessage(lastActiveStep, lastActiveAction) - - if (!route || !lastActiveStep) { - return null - } - - const handleClick = () => { - navigate({ - to: navigationRoutes.transactionExecution, - search: { routeId }, - }) - } - - const getStatusComponent = () => { - switch (lastActiveAction?.status) { - case 'ACTION_REQUIRED': - case 'MESSAGE_REQUIRED': - case 'RESET_REQUIRED': - return - case 'FAILED': - return - default: - return - } - } - - const ListItemComponent = dense ? ListItem : ListItemButton - - return ( - - - - - - - - - {route.fromToken.symbol} - - {route.toToken.symbol} - - } - secondary={ - status !== RouteExecutionStatus.Done ? ( - - {title} - - ) : null - } - /> - {getStatusComponent()} - - ) -} diff --git a/packages/widget/src/components/ActiveTransactions/ActiveTransactions.style.ts b/packages/widget/src/components/ActiveTransactions/ActiveTransactions.style.ts deleted file mode 100644 index a5b80ef2f..000000000 --- a/packages/widget/src/components/ActiveTransactions/ActiveTransactions.style.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - listItemSecondaryActionClasses, - ListItem as MuiListItem, - ListItemButton as MuiListItemButton, - styled, -} from '@mui/material' - -export const ListItemButton = styled(MuiListItemButton)(({ theme }) => ({ - borderRadius: theme.vars.shape.borderRadius, - paddingLeft: theme.spacing(1.5), - paddingRight: theme.spacing(1.5), - height: 64, - '&:hover': { - backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, - }, -})) - -export const ListItem = styled(MuiListItem, { - shouldForwardProp: (prop) => prop !== 'disableRipple', -})(({ theme }) => ({ - padding: theme.spacing(0), - [`.${listItemSecondaryActionClasses.root}`]: { - right: theme.spacing(3), - }, - '&:hover': { - cursor: 'pointer', - }, -})) diff --git a/packages/widget/src/components/ActiveTransactions/ActiveTransactions.tsx b/packages/widget/src/components/ActiveTransactions/ActiveTransactions.tsx deleted file mode 100644 index 0e6c9f417..000000000 --- a/packages/widget/src/components/ActiveTransactions/ActiveTransactions.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type { CardProps } from '@mui/material' -import { Stack } from '@mui/material' -import { useNavigate } from '@tanstack/react-router' -import { useTranslation } from 'react-i18next' -import { useExecutingRoutesIds } from '../../stores/routes/useExecutingRoutesIds.js' -import { navigationRoutes } from '../../utils/navigationRoutes.js' -import { ButtonTertiary } from '../ButtonTertiary.js' -import { Card } from '../Card/Card.js' -import { CardTitle } from '../Card/CardTitle.js' -import { ActiveTransactionItem } from './ActiveTransactionItem.js' - -export const ActiveTransactions: React.FC = (props) => { - const { t } = useTranslation() - const navigate = useNavigate() - const executingRoutes = useExecutingRoutesIds() - - if (!executingRoutes?.length) { - return null - } - - const handleShowAll = () => { - navigate({ to: navigationRoutes.activeTransactions }) - } - - const hasShowAll = executingRoutes?.length > 2 - - return ( - - {t('header.activeTransactions')} - - {executingRoutes.slice(0, 2).map((routeId) => ( - - ))} - {hasShowAll ? ( - - {t('button.showAll')} - - ) : null} - - - ) -} diff --git a/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx b/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx new file mode 100644 index 000000000..45e9e8eb2 --- /dev/null +++ b/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx @@ -0,0 +1,21 @@ +import { Badge, Box, styled } from '@mui/material' + +export const ErrorBadge = styled(Badge)({ + '& .MuiBadge-badge': { + padding: 0, + minWidth: 'unset', + width: 16, + height: 16, + borderRadius: '50%', + backgroundColor: 'white', + top: '0px', + left: '8px', + }, +}) + +export const ProgressContainer = styled(Box)({ + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}) diff --git a/packages/widget/src/components/Header/TransactionHistoryButton.tsx b/packages/widget/src/components/Header/TransactionHistoryButton.tsx index 4824ac022..0813a5e3b 100644 --- a/packages/widget/src/components/Header/TransactionHistoryButton.tsx +++ b/packages/widget/src/components/Header/TransactionHistoryButton.tsx @@ -1,12 +1,38 @@ +import ErrorRounded from '@mui/icons-material/ErrorRounded' import ReceiptLong from '@mui/icons-material/ReceiptLong' -import { IconButton, Tooltip } from '@mui/material' +import { CircularProgress, IconButton, Tooltip } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' +import { useExecutingRoutesIds } from '../../stores/routes/useExecutingRoutesIds.js' +import { useHasFailedRoutes } from '../../stores/routes/useHasFailedRoutes.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' +import { + ErrorBadge, + ProgressContainer, +} from './TransactionHistoryButton.style.js' + +const progressTrackSx = (theme: import('@mui/material').Theme) => ({ + position: 'absolute' as const, + color: theme.vars.palette.grey[300], + ...theme.applyStyles('dark', { + color: theme.vars.palette.grey[800], + }), +}) + +const progressFillSx = (theme: import('@mui/material').Theme) => ({ + position: 'absolute' as const, + color: theme.vars.palette.primary.main, + ...theme.applyStyles('dark', { + color: theme.vars.palette.primary.light, + }), +}) export const TransactionHistoryButton = () => { const { t } = useTranslation() const navigate = useNavigate() + const hasFailedRoutes = useHasFailedRoutes() + const executingRouteIds = useExecutingRoutesIds() + const hasActiveRoutes = executingRouteIds.length > 0 return ( @@ -14,7 +40,36 @@ export const TransactionHistoryButton = () => { size="medium" onClick={() => navigate({ to: navigationRoutes.transactionHistory })} > - + + } + overlap="circular" + anchorOrigin={{ vertical: 'top', horizontal: 'right' }} + > + + {hasActiveRoutes ? ( + <> + + + + ) : null} + + + ) diff --git a/packages/widget/src/components/Step/CircularProgress.style.tsx b/packages/widget/src/components/Step/CircularProgress.style.tsx new file mode 100644 index 000000000..f4a0bba65 --- /dev/null +++ b/packages/widget/src/components/Step/CircularProgress.style.tsx @@ -0,0 +1,34 @@ +import { + Box, + CircularProgress as MuiCircularProgress, + styled, + Typography, +} from '@mui/material' + +export const circleSize = 96 + +export const StatusCircle = styled(Box)({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: circleSize, + height: circleSize, + borderRadius: '50%', +}) + +export const RingContainer = styled(Box)({ + position: 'relative', + width: circleSize, + height: circleSize, +}) + +export const ProgressTrack = styled(MuiCircularProgress)({ + position: 'absolute', + color: 'divider', +}) + +export const TimerLabel = styled(Typography)({ + fontSize: 22, + fontWeight: 700, + fontVariantNumeric: 'tabular-nums', +}) diff --git a/packages/widget/src/components/Step/CircularProgress.tsx b/packages/widget/src/components/Step/CircularProgress.tsx index 47e8ae348..3f46bc1b2 100644 --- a/packages/widget/src/components/Step/CircularProgress.tsx +++ b/packages/widget/src/components/Step/CircularProgress.tsx @@ -1,25 +1,18 @@ import type { LiFiStepExtended } from '@lifi/sdk' import Done from '@mui/icons-material/Done' import ErrorRounded from '@mui/icons-material/ErrorRounded' +import InfoRounded from '@mui/icons-material/InfoRounded' import WarningRounded from '@mui/icons-material/WarningRounded' -import { Box, useTheme } from '@mui/material' -import type React from 'react' +import { useTheme } from '@mui/material' import { getStatusColor } from '../../utils/getStatusColor.js' -import { StepTimer } from '../Timer/StepTimer.js' +import { StatusCircle } from './CircularProgress.style.js' +import { IndeterminateRing, TimerRing } from './CircularProgressTimer.js' interface CircularProgressProps { step: LiFiStepExtended } -const commonStyles = { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: 96, - height: 96, - border: '3px solid', - borderRadius: '50%', -} +const iconSx = { fontSize: 48 } export const CircularProgress: React.FC = ({ step }) => { const theme = useTheme() @@ -39,65 +32,42 @@ export const CircularProgress: React.FC = ({ step }) => { status === 'MESSAGE_REQUIRED' || status === 'RESET_REQUIRED' - if (withTimer || actionRequired) { - return + if (withTimer) { + if (!step.execution?.signedAt) { + return + } + return } const backgroundColor = getStatusColor(theme, status, substatus) + if (actionRequired) { + return ( + + + + ) + } + switch (status) { case 'DONE': if (substatus === 'PARTIAL' || substatus === 'REFUNDED') { return ( - - - + + + ) } - return ( - - - + + + ) case 'FAILED': return ( - - - + + + ) } } diff --git a/packages/widget/src/components/Step/CircularProgressTimer.tsx b/packages/widget/src/components/Step/CircularProgressTimer.tsx new file mode 100644 index 000000000..53247c0dd --- /dev/null +++ b/packages/widget/src/components/Step/CircularProgressTimer.tsx @@ -0,0 +1,94 @@ +import type { LiFiStepExtended } from '@lifi/sdk' +import { CircularProgress as MuiCircularProgress } from '@mui/material' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useTimer } from '../../hooks/timer/useTimer.js' +import { formatTimer } from '../../utils/timer.js' +import { + circleSize, + ProgressTrack, + RingContainer, + StatusCircle, + TimerLabel, +} from './CircularProgress.style.js' + +const getExpiryTimestamp = (step: LiFiStepExtended) => { + const execution = step?.execution + if (!execution) { + return new Date() + } + return new Date( + (execution.signedAt ?? Date.now()) + step.estimate.executionDuration * 1000 + ) +} + +export const IndeterminateRing: React.FC = () => ( + + + + +) + +export const TimerRing: React.FC<{ step: LiFiStepExtended }> = ({ step }) => { + const { i18n } = useTranslation() + const [isExpired, setExpired] = useState(false) + + const totalDuration = step.estimate.executionDuration * 1000 + const expiryTimestamp = getExpiryTimestamp(step) + + const { days, hours, minutes, seconds } = useTimer({ + autoStart: true, + expiryTimestamp, + onExpire: () => setExpired(true), + }) + + const isTimerExpired = isExpired || (!minutes && !seconds) + const remaining = Math.max(expiryTimestamp.getTime() - Date.now(), 0) + const progress = + totalDuration > 0 + ? Math.min(((totalDuration - remaining) / totalDuration) * 100, 100) + : 0 + + if (isTimerExpired) { + return + } + + return ( + + + + + + {formatTimer({ + locale: i18n.language, + days, + hours, + minutes, + seconds, + })} + + + + ) +} diff --git a/packages/widget/src/components/Step/ExecutionProgress.tsx b/packages/widget/src/components/Step/ExecutionProgress.tsx index 7a2e555e1..0ad8acf5b 100644 --- a/packages/widget/src/components/Step/ExecutionProgress.tsx +++ b/packages/widget/src/components/Step/ExecutionProgress.tsx @@ -21,8 +21,8 @@ export const ExecutionProgress: React.FC<{ {message} diff --git a/packages/widget/src/components/Step/RouteTransactions.tsx b/packages/widget/src/components/Step/RouteTransactions.tsx index 0be437e9f..ac8328d39 100644 --- a/packages/widget/src/components/Step/RouteTransactions.tsx +++ b/packages/widget/src/components/Step/RouteTransactions.tsx @@ -1,15 +1,53 @@ import type { RouteExtended } from '@lifi/sdk' -import { Box } from '@mui/material' +import ContentCopyRounded from '@mui/icons-material/ContentCopyRounded' +import OpenInNew from '@mui/icons-material/OpenInNew' +import Wallet from '@mui/icons-material/Wallet' +import { Box, IconButton } from '@mui/material' +import type { MouseEvent } from 'react' +import { useTranslation } from 'react-i18next' +import { useExplorer } from '../../hooks/useExplorer.js' import { prepareActions } from '../../utils/prepareActions.js' +import { shortenAddress } from '../../utils/wallet.js' import { StepTransactionLink } from './TransactionLink.js' +import { + ExternalLinkIcon, + StatusIconCircle, + TransactionLinkContainer, + TransactionLinkLabel, +} from './TransactionLink.style.js' + +const isRouteCompleted = (route: RouteExtended) => { + const lastStep = route.steps.at(-1) + const lastAction = lastStep?.execution?.actions?.at(-1) + return lastAction?.status === 'DONE' +} export const RouteTransactions: React.FC<{ route: RouteExtended }> = ({ route }) => { + const { t } = useTranslation() + const { getAddressLink } = useExplorer() + const completed = isRouteCompleted(route) + const toAddress = route.toAddress + + const handleCopy = (e: MouseEvent) => { + e.stopPropagation() + if (toAddress) { + navigator.clipboard.writeText(toAddress) + } + } + + const addressLink = toAddress + ? getAddressLink(toAddress, route.toChainId) + : undefined + return ( - + {route.steps.map((step) => ( - + {prepareActions(step.execution?.actions ?? []).map( (actionsGroup, index) => ( ))} + {completed && toAddress ? ( + + + + + + {t('main.sentToWallet', { + address: shortenAddress(toAddress), + })} + + + + + {addressLink ? ( + + + + ) : null} + + ) : null} ) } diff --git a/packages/widget/src/components/Step/TransactionLink.style.tsx b/packages/widget/src/components/Step/TransactionLink.style.tsx index 251a8b4b0..9afb299a0 100644 --- a/packages/widget/src/components/Step/TransactionLink.style.tsx +++ b/packages/widget/src/components/Step/TransactionLink.style.tsx @@ -4,7 +4,7 @@ export const TransactionLinkContainer = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', padding: theme.spacing(1), - borderRadius: theme.vars.shape.borderRadiusSecondary, + borderRadius: theme.spacing(4), color: theme.vars.palette.text.primary, backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, })) @@ -18,9 +18,10 @@ export const StatusIconCircle = styled(Box, { width: 24, height: 24, borderRadius: '50%', + // TODO: how to resolve new colors? backgroundColor: failed ? `rgba(${theme.vars.palette.error.mainChannel} / 0.12)` - : `rgba(${theme.vars.palette.success.mainChannel} / 0.12)`, + : '#D6FFE7', marginRight: theme.spacing(1), })) diff --git a/packages/widget/src/components/Step/TransactionLink.tsx b/packages/widget/src/components/Step/TransactionLink.tsx index f200d266c..c143ac925 100644 --- a/packages/widget/src/components/Step/TransactionLink.tsx +++ b/packages/widget/src/components/Step/TransactionLink.tsx @@ -2,7 +2,6 @@ import type { ExecutionAction, LiFiStepExtended } from '@lifi/sdk' import Done from '@mui/icons-material/Done' import ErrorRounded from '@mui/icons-material/ErrorRounded' import OpenInNew from '@mui/icons-material/OpenInNew' -import { Box } from '@mui/material' import type React from 'react' import { useActionMessage } from '../../hooks/useActionMessage.js' import { useExplorer } from '../../hooks/useExplorer.js' @@ -51,7 +50,7 @@ export const StepTransactionLink: React.FC<{ const { title } = useActionMessage(step, action) const { getTransactionLink } = useExplorer() - if (!action) { + if (!action || (action.status !== 'DONE' && action.status !== 'FAILED')) { return null } @@ -68,17 +67,10 @@ export const StepTransactionLink: React.FC<{ : undefined return ( - ({ - py: 0.5, - borderRadius: theme.vars.shape.borderRadiusTertiary, - })} - > - - + ) } diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json index 8a459dde7..f81937f1a 100644 --- a/packages/widget/src/i18n/en.json +++ b/packages/widget/src/i18n/en.json @@ -240,6 +240,7 @@ "pinnedTokens": "Pinned tokens", "popularTokens": "Popular tokens", "priceImpact": "Price impact", + "sentToWallet": "Sent to wallet: {{address}}", "process": { "bridge": { "actionRequired": "Sign bridge transaction", diff --git a/packages/widget/src/pages/ActiveTransactionsPage/ActiveTransactionsEmpty.tsx b/packages/widget/src/pages/ActiveTransactionsPage/ActiveTransactionsEmpty.tsx deleted file mode 100644 index d6c06df12..000000000 --- a/packages/widget/src/pages/ActiveTransactionsPage/ActiveTransactionsEmpty.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import SwapHoriz from '@mui/icons-material/SwapHoriz' -import { Container, Typography } from '@mui/material' -import { useTranslation } from 'react-i18next' - -export const ActiveTransactionsEmpty: React.FC = () => { - const { t } = useTranslation() - return ( - - - - - - {t('info.title.emptyActiveTransactions')} - - - {t('info.message.emptyActiveTransactions')} - - - ) -} diff --git a/packages/widget/src/pages/ActiveTransactionsPage/ActiveTransactionsPage.tsx b/packages/widget/src/pages/ActiveTransactionsPage/ActiveTransactionsPage.tsx deleted file mode 100644 index 129a6fd9c..000000000 --- a/packages/widget/src/pages/ActiveTransactionsPage/ActiveTransactionsPage.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import DeleteOutline from '@mui/icons-material/DeleteOutline' -import type { IconButtonProps } from '@mui/material' -import { - Button, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - IconButton, - List, - useTheme, -} from '@mui/material' -import { useCallback, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { ActiveTransactionItem } from '../../components/ActiveTransactions/ActiveTransactionItem.js' -import { Dialog } from '../../components/Dialog.js' -import { PageContainer } from '../../components/PageContainer.js' -import { useHeader } from '../../hooks/useHeader.js' -import { useRouteExecutionStore } from '../../stores/routes/RouteExecutionStore.js' -import { useExecutingRoutesIds } from '../../stores/routes/useExecutingRoutesIds.js' -import { ActiveTransactionsEmpty } from './ActiveTransactionsEmpty.js' - -const DeleteIconButton: React.FC = ({ onClick }) => { - const theme = useTheme() - - return ( - - - - ) -} - -export const ActiveTransactionsPage = () => { - const { t } = useTranslation() - const executingRoutes = useExecutingRoutesIds() - const deleteRoutes = useRouteExecutionStore((store) => store.deleteRoutes) - const [open, setOpen] = useState(false) - - const toggleDialog = useCallback(() => { - setOpen((open) => !open) - }, []) - - const headerAction = useMemo( - () => - executingRoutes.length ? ( - - ) : undefined, - [executingRoutes.length, toggleDialog] - ) - - useHeader(t('header.activeTransactions'), headerAction) - - if (!executingRoutes.length) { - return - } - - return ( - - - {executingRoutes.map((routeId) => ( - - ))} - - - {t('warning.title.deleteActiveTransactions')} - - - {t('warning.message.deleteActiveTransactions')} - - - - - - - - - ) -} diff --git a/packages/widget/src/pages/MainPage/MainPage.tsx b/packages/widget/src/pages/MainPage/MainPage.tsx index 95201d9e8..252a0b375 100644 --- a/packages/widget/src/pages/MainPage/MainPage.tsx +++ b/packages/widget/src/pages/MainPage/MainPage.tsx @@ -1,6 +1,5 @@ import { Box } from '@mui/material' import { useTranslation } from 'react-i18next' -import { ActiveTransactions } from '../../components/ActiveTransactions/ActiveTransactions.js' import { AmountInput } from '../../components/AmountInput/AmountInput.js' import { ContractComponent } from '../../components/ContractComponent/ContractComponent.js' import { GasRefuelMessage } from '../../components/Messages/GasRefuelMessage.js' @@ -47,7 +46,6 @@ export const MainPage: React.FC = () => { return ( - {custom ? ( {contractComponent} ) : null} diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx new file mode 100644 index 000000000..294e5fb2a --- /dev/null +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx @@ -0,0 +1,47 @@ +import { Box, styled, Typography } from '@mui/material' + +export const CardContent = styled(Box)({ + padding: 24, +}) + +export const StatusBar = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: 12, + marginBottom: 12, + paddingLeft: 16, + paddingRight: 16, + paddingTop: 12, + paddingBottom: 12, + borderRadius: theme.vars.shape.borderRadiusSecondary, + backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, +})) + +export const ErrorIconCircle = styled(Box)({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 32, + height: 32, + borderRadius: '50%', + backgroundColor: 'rgba(211, 47, 47, 0.12)', + flexShrink: 0, +}) + +export const StatusTitle = styled(Typography)({ + fontSize: 14, + fontWeight: 600, + flex: 1, +}) + +export const StatusMessage = styled(Typography)({ + fontSize: 14, + fontWeight: 500, + flex: 1, +}) + +export const TimerText = styled(Typography)({ + fontSize: 14, + fontWeight: 700, + fontVariantNumeric: 'tabular-nums', +}) diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx new file mode 100644 index 000000000..90f048799 --- /dev/null +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx @@ -0,0 +1,104 @@ +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline' +import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded' +import { Button, CircularProgress, IconButton } from '@mui/material' +import { useNavigate } from '@tanstack/react-router' +import type { MouseEvent } from 'react' +import { useTranslation } from 'react-i18next' +import { Card } from '../../components/Card/Card.js' +import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { StepTimer } from '../../components/Timer/StepTimer.js' +import { useActionMessage } from '../../hooks/useActionMessage.js' +import { useRouteExecution } from '../../hooks/useRouteExecution.js' +import { RouteExecutionStatus } from '../../stores/routes/types.js' +import { navigationRoutes } from '../../utils/navigationRoutes.js' +import { + CardContent, + ErrorIconCircle, + StatusBar, + StatusMessage, + StatusTitle, + TimerText, +} from './ActiveTransactionCard.style.js' + +export const ActiveTransactionCard: React.FC<{ + routeId: string +}> = ({ routeId }) => { + const { t } = useTranslation() + const navigate = useNavigate() + const { route, status, restartRoute, deleteRoute } = useRouteExecution({ + routeId, + executeInBackground: true, + }) + + const lastStep = route?.steps.findLast((step) => step.execution) + const lastAction = lastStep?.execution?.actions?.at(-1) + const { title } = useActionMessage(lastStep, lastAction) + + if (!route) { + return null + } + + const isFailed = status === RouteExecutionStatus.Failed + + const handleClick = () => { + navigate({ + to: navigationRoutes.transactionExecution, + search: { routeId }, + }) + } + + const handleDelete = (e: MouseEvent) => { + e.stopPropagation() + deleteRoute() + } + + const handleRetry = (e: MouseEvent) => { + e.stopPropagation() + restartRoute() + } + + return ( + + + {isFailed ? ( + + + + + {t('error.title.transactionFailed')} + + + + + + ) : null} + {!isFailed && title ? ( + + + {title} + {lastStep ? ( + + + + ) : null} + + ) : null} + + + + ) +} diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx index c6c87cf2f..fcd684e6b 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx @@ -1,27 +1,29 @@ -import type { - ExtendedTransactionInfo, - FullStatusData, - StatusResponse, - TokenAmount, -} from '@lifi/sdk' +import type { FullStatusData, StatusResponse } from '@lifi/sdk' import { Box, Typography } from '@mui/material' import { useNavigate } from '@tanstack/react-router' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Card } from '../../components/Card/Card.js' -import { Token } from '../../components/Token/Token.js' -import { TokenDivider } from '../../components/Token/Token.style.js' +import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { useTools } from '../../hooks/useTools.js' +import { buildRouteFromTxHistory } from '../../utils/converters.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' export const TransactionHistoryItem: React.FC<{ transaction: StatusResponse - start: number -}> = ({ transaction, start }) => { +}> = ({ transaction }) => { const { i18n } = useTranslation() const navigate = useNavigate() + const { tools } = useTools() - const sending = transaction.sending as ExtendedTransactionInfo - const receiving = (transaction as FullStatusData) - .receiving as ExtendedTransactionInfo + const routeExecution = useMemo( + () => buildRouteFromTxHistory(transaction as FullStatusData, tools), + [transaction, tools] + ) + + if (!routeExecution?.route) { + return null + } const handleClick = () => { navigate({ @@ -32,86 +34,32 @@ export const TransactionHistoryItem: React.FC<{ }) } - const startedAt = new Date((sending.timestamp ?? 0) * 1000) - - if (!sending.token?.chainId || !receiving.token?.chainId) { - return null - } - - const fromToken: TokenAmount = { - ...sending.token, - amount: BigInt(sending.amount ?? '0'), - priceUSD: sending.token.priceUSD ?? '0', - symbol: sending.token?.symbol ?? '', - decimals: sending.token?.decimals ?? 0, - name: sending.token?.name ?? '', - chainId: sending.token?.chainId, - } - - const toToken: TokenAmount = { - ...receiving.token, - amount: BigInt(receiving.amount ?? '0'), - priceUSD: receiving.token.priceUSD ?? '0', - symbol: receiving.token?.symbol ?? '', - decimals: receiving.token?.decimals ?? 0, - name: receiving.token?.name ?? '', - chainId: receiving.token?.chainId, - } + const startedAt = new Date( + (routeExecution.route.steps[0].execution?.startedAt ?? 0) * 1000 + ) return ( - - - - {startedAt.toLocaleString(i18n.language, { dateStyle: 'long' })} - - - {startedAt.toLocaleString(i18n.language, { - timeStyle: 'short', - })} - - - - + + - + + {startedAt.toLocaleString(i18n.language, { dateStyle: 'long' })} + + + {startedAt.toLocaleString(i18n.language, { timeStyle: 'short' })} + - + ) diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx index 0f496d0fb..e6d700dc3 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx @@ -7,6 +7,8 @@ import { PageContainer } from '../../components/PageContainer.js' import { useHeader } from '../../hooks/useHeader.js' import { useListHeight } from '../../hooks/useListHeight.js' import { useTransactionHistory } from '../../hooks/useTransactionHistory.js' +import { useExecutingRoutesIds } from '../../stores/routes/useExecutingRoutesIds.js' +import { ActiveTransactionCard } from './ActiveTransactionCard.js' import { minTransactionListHeight } from './constants.js' import { TransactionHistoryEmpty } from './TransactionHistoryEmpty.js' import { TransactionHistoryItem } from './TransactionHistoryItem.js' @@ -16,6 +18,7 @@ export const TransactionHistoryPage = () => { // Parent ref and useVirtualizer should be in one file to avoid blank page (0 virtual items) issue const parentRef = useRef(null) const { data: transactions, isLoading } = useTransactionHistory() + const executingRoutes = useExecutingRoutesIds() const { t } = useTranslation() useHeader(t('header.transactionHistory')) @@ -31,16 +34,16 @@ export const TransactionHistoryPage = () => { [transactions] ) - const { getVirtualItems, getTotalSize } = useVirtualizer({ + const { getVirtualItems, getTotalSize, measureElement } = useVirtualizer({ count: transactions.length, overscan: 3, paddingEnd: 12, getScrollElement: () => parentRef.current, - estimateSize: () => 186, + estimateSize: () => 216, getItemKey, }) - if (!transactions.length && !isLoading) { + if (!transactions.length && !executingRoutes.length && !isLoading) { return } @@ -59,6 +62,9 @@ export const TransactionHistoryPage = () => { paddingX: 3, }} > + {executingRoutes.map((routeId) => ( + + ))} {isLoading ? ( {Array.from({ length: 3 }).map((_, index) => ( @@ -77,11 +83,21 @@ export const TransactionHistoryPage = () => { {getVirtualItems().map((item) => { const transaction = transactions[item.index] return ( - + ref={measureElement} + data-index={item.index} + style={{ + position: 'absolute', + top: 0, + left: 0, + width: '100%', + paddingBottom: 12, + transform: `translateY(${item.start}px)`, + }} + > + + ) })} diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistorySkeleton.tsx b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistorySkeleton.tsx index 8c7fc2905..5c8174cbf 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistorySkeleton.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistorySkeleton.tsx @@ -1,57 +1,51 @@ +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward' import { Box, Skeleton } from '@mui/material' import { Card } from '../../components/Card/Card.js' import { TokenSkeleton } from '../../components/Token/Token.js' -import { TokenDivider } from '../../components/Token/Token.style.js' export const TransactionHistoryItemSkeleton = () => { return ( - - - ({ - borderRadius: theme.vars.shape.borderRadius, - })} - /> - ({ - borderRadius: theme.vars.shape.borderRadius, - })} - /> - - - + + - + ({ + borderRadius: theme.vars.shape.borderRadius, + })} + /> + ({ + borderRadius: theme.vars.shape.borderRadius, + })} + /> + + + + + + + - ) diff --git a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx index 2a0b2ade6..21a4cc5f1 100644 --- a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx +++ b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx @@ -11,6 +11,7 @@ import type { BottomSheetBase } from '../../components/BottomSheet/types.js' import { Card } from '../../components/Card/Card.js' import { CardTitle } from '../../components/Card/CardTitle.js' import { Token } from '../../components/Token/Token.js' +import { WalletAddressBadge } from '../../components/WalletAddressBadge/WalletAddressBadge.js' import { useAvailableChains } from '../../hooks/useAvailableChains.js' import { useSetContentHeight } from '../../hooks/useSetContentHeight.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' @@ -270,11 +271,22 @@ const StatusBottomSheetContent: React.FC = ({ padding: 2, }} > - - {hasEnumFlag(status, RouteExecutionStatus.Refunded) - ? t('header.refunded') - : t('header.received')} - + + + {hasEnumFlag(status, RouteExecutionStatus.Refunded) + ? t('header.refunded') + : t('header.received')} + + {route.toAddress ? ( + + ) : null} + {primaryMessage && ( ({ + paddingBottom: 16, + color: theme.vars.palette.text.secondary, + fontSize: 14, +})) + +export const DetailRow = styled(Box)({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + paddingTop: 6, + paddingBottom: 6, +}) + +export const DetailLabel = styled(Typography)(({ theme }) => ({ + fontSize: 14, + fontWeight: 500, + color: theme.vars.palette.text.secondary, +})) + +export const DetailValue = styled(Typography)({ + fontSize: 14, + fontWeight: 700, +}) + +export const ButtonRow = styled(Box)({ + display: 'flex', + marginTop: 24, + gap: 12, +}) diff --git a/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx b/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx index 7f63cfc0b..94ea473ed 100644 --- a/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx +++ b/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx @@ -1,6 +1,6 @@ import type { Route } from '@lifi/sdk' import WarningRounded from '@mui/icons-material/WarningRounded' -import { Box, Button, Typography } from '@mui/material' +import { Button } from '@mui/material' import type { RefObject } from 'react' import { forwardRef, useRef } from 'react' import { useTranslation } from 'react-i18next' @@ -10,6 +10,15 @@ import { FeeBreakdownTooltip } from '../../components/FeeBreakdownTooltip.js' import { useSetContentHeight } from '../../hooks/useSetContentHeight.js' import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js' import { CenterContainer, IconCircle } from './StatusBottomSheet.style.js' +import { + ButtonRow, + ContentContainer, + DetailLabel, + DetailRow, + DetailValue, + WarningMessage, + WarningTitle, +} from './TokenValueBottomSheet.style.js' import { calculateValueLossPercentage } from './utils.js' interface TokenValueBottomSheetProps { @@ -50,119 +59,52 @@ const TokenValueBottomSheetContent: React.FC = ({ getAccumulatedFeeCostsBreakdown(route) const fromAmountUSD = Number.parseFloat(route.fromAmountUSD) const toAmountUSD = Number.parseFloat(route.toAmountUSD) + return ( - + - - + {/* TODO: how to resolve colors? */} + + - - {t('warning.title.highValueLoss')} - + {t('warning.title.highValueLoss')} - - {t('warning.message.highValueLoss')} - - - {t('main.sending')} - + {t('warning.message.highValueLoss')} + + {t('main.sending')} + {t('format.currency', { value: route.fromAmountUSD })} - - - - {t('main.fees.network')} + + + + {t('main.fees.network')} - + {!gasCostUSD ? t('main.fees.free') : t('format.currency', { value: gasCostUSD })} - + - + {feeCostUSD ? ( - - {t('main.fees.provider')} + + {t('main.fees.provider')} - + {t('format.currency', { value: feeCostUSD })} - + - + ) : null} - - {t('main.receiving')} - + + {t('main.receiving')} + {t('format.currency', { value: route.toAmountUSD })} - - - - {t('main.valueLoss')} - + + + + {t('main.valueLoss')} + {calculateValueLossPercentage( fromAmountUSD, toAmountUSD, @@ -170,27 +112,16 @@ const TokenValueBottomSheetContent: React.FC = ({ feeCostUSD )} % - - - + + + - - - + + ) } diff --git a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx index 58cb06750..280c2c045 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx @@ -207,12 +207,17 @@ export const TransactionPage = () => { } return ( - - - - - - + + {status !== RouteExecutionStatus.Idle ? ( + + + + + ) : null} + diff --git a/packages/widget/src/stores/routes/useHasFailedRoutes.ts b/packages/widget/src/stores/routes/useHasFailedRoutes.ts new file mode 100644 index 000000000..3cb6ddfba --- /dev/null +++ b/packages/widget/src/stores/routes/useHasFailedRoutes.ts @@ -0,0 +1,16 @@ +import { useAccount } from '@lifi/wallet-management' +import { useRouteExecutionStore } from './RouteExecutionStore.js' +import type { RouteExecution } from './types.js' +import { RouteExecutionStatus } from './types.js' + +export const useHasFailedRoutes = () => { + const { accounts } = useAccount() + const accountAddresses = accounts.map((account) => account.address) + return useRouteExecutionStore((state) => + (Object.values(state.routes) as RouteExecution[]).some( + (item) => + accountAddresses.includes(item.route.fromAddress) && + item.status === RouteExecutionStatus.Failed + ) + ) +} diff --git a/packages/widget/src/utils/getStatusColor.ts b/packages/widget/src/utils/getStatusColor.ts index b689bd737..af2582730 100644 --- a/packages/widget/src/utils/getStatusColor.ts +++ b/packages/widget/src/utils/getStatusColor.ts @@ -22,10 +22,12 @@ export const getStatusColor = ( if (substatus === 'PARTIAL' || substatus === 'REFUNDED') { return `rgba(${theme.vars.palette.warning.mainChannel} / 0.12)` } - return `rgba(${theme.vars.palette.success.mainChannel} / 0.12)` + // TODO: how to resolve new colors? + return '#D6FFE7' + // return `rgba(${theme.vars.palette.success.mainChannel} / 0.12)` case 'FAILED': return `rgba(${theme.vars.palette.error.mainChannel} / 0.12)` default: - return theme.vars.palette.info.mainChannel + return `rgba(${theme.vars.palette.info.mainChannel} / 0.12)` } } diff --git a/packages/widget/src/utils/navigationRoutes.ts b/packages/widget/src/utils/navigationRoutes.ts index a82fa9d77..4c6489415 100644 --- a/packages/widget/src/utils/navigationRoutes.ts +++ b/packages/widget/src/utils/navigationRoutes.ts @@ -1,6 +1,5 @@ export const navigationRoutes = { home: '/', - activeTransactions: 'active-transactions', bridges: 'bridges', exchanges: 'exchanges', fromChain: 'from-chain', @@ -24,7 +23,6 @@ export const navigationRoutes = { export const navigationRoutesValues = Object.values(navigationRoutes) export const stickyHeaderRoutes = [ - navigationRoutes.activeTransactions, navigationRoutes.bridges, navigationRoutes.exchanges, navigationRoutes.fromChain, @@ -44,7 +42,6 @@ export const stickyHeaderRoutes = [ ] export const backButtonRoutes = [ - navigationRoutes.activeTransactions, navigationRoutes.bridges, navigationRoutes.exchanges, navigationRoutes.languages, From c78ea0dd51fc70b7cbf6bf53038484cadc8f3239 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Mon, 9 Mar 2026 17:04:21 +0000 Subject: [PATCH 05/22] refactor: replace SendToWallet components --- .../components/AmountInput/AmountInput.tsx | 32 +++- .../widget/src/components/Card/CardHeader.tsx | 26 --- .../ReverseTokensButton.style.tsx | 17 +- .../src/components/SelectChainAndToken.tsx | 23 +-- .../SelectTokenButton.style.tsx | 3 +- .../SendToWallet/SendToWallet.style.tsx | 55 ------ .../SendToWallet/SendToWalletButton.tsx | 172 ------------------ .../SendToWallet/SendToWalletExpandButton.tsx | 63 ------- .../WalletAddressBadge/WalletAddressBadge.tsx | 14 +- .../src/hooks/useToAddressAutoPopulate.ts | 4 - .../widget/src/hooks/useToAddressReset.ts | 25 +-- .../widget/src/pages/MainPage/MainPage.tsx | 4 - .../src/pages/SendToWallet/BookmarksPage.tsx | 3 - .../SendToWallet/ConfirmAddressSheet.tsx | 4 - .../SendToWallet/ConnectedWalletsPage.tsx | 3 - .../pages/SendToWallet/RecentWalletsPage.tsx | 3 - .../widget/src/stores/form/FormUpdater.tsx | 6 - .../stores/form/URLSearchParamsBuilder.tsx | 4 - packages/widget/src/stores/form/useFormRef.ts | 13 +- packages/widget/src/stores/settings/types.ts | 8 - .../stores/settings/useSendToWalletStore.ts | 25 --- 21 files changed, 59 insertions(+), 448 deletions(-) delete mode 100644 packages/widget/src/components/Card/CardHeader.tsx delete mode 100644 packages/widget/src/components/SendToWallet/SendToWallet.style.tsx delete mode 100644 packages/widget/src/components/SendToWallet/SendToWalletButton.tsx delete mode 100644 packages/widget/src/components/SendToWallet/SendToWalletExpandButton.tsx delete mode 100644 packages/widget/src/stores/settings/useSendToWalletStore.ts diff --git a/packages/widget/src/components/AmountInput/AmountInput.tsx b/packages/widget/src/components/AmountInput/AmountInput.tsx index f7749b96f..ed0fc1b6a 100644 --- a/packages/widget/src/components/AmountInput/AmountInput.tsx +++ b/packages/widget/src/components/AmountInput/AmountInput.tsx @@ -1,5 +1,6 @@ import type { Token } from '@lifi/sdk' import type { CardProps } from '@mui/material' +import { useNavigate } from '@tanstack/react-router' import type { ChangeEvent, ReactNode } from 'react' import { useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -10,7 +11,7 @@ import { FormKeyHelper, type FormTypeProps } from '../../stores/form/types.js' import { useFieldActions } from '../../stores/form/useFieldActions.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' import { useInputModeStore } from '../../stores/inputMode/useInputModeStore.js' -import { DisabledUI } from '../../types/widget.js' +import { DisabledUI, HiddenUI } from '../../types/widget.js' import { formatInputAmount, formatTokenPrice, @@ -18,6 +19,7 @@ import { usdDecimals, } from '../../utils/format.js' import { fitInputText } from '../../utils/input.js' +import { navigationRoutes } from '../../utils/navigationRoutes.js' import { AvatarBadgedDefault } from '../Avatar/Avatar.js' import { TokenAvatar } from '../Avatar/TokenAvatar.js' import { WalletAddressBadge } from '../WalletAddressBadge/WalletAddressBadge.js' @@ -72,7 +74,9 @@ const AmountInputBase: React.FC< } > = ({ formType, token, startAdornment, endAdornment, disabled, ...props }) => { const { t } = useTranslation() - const { subvariant, subvariantOptions } = useWidgetConfig() + const navigate = useNavigate() + const { subvariant, subvariantOptions, toAddresses, disabledUI, hiddenUI } = + useWidgetConfig() const ref = useRef(null) const isEditingRef = useRef(false) @@ -90,6 +94,9 @@ const AmountInputBase: React.FC< const { chain } = useChain(chainId) + const hiddenToAddress = hiddenUI?.includes(HiddenUI.ToAddress) + const disabledToAddress = disabledUI?.includes(DisabledUI.ToAddress) + const currentInputMode = inputMode[formType] let displayValue: string if (isEditingRef.current) { @@ -177,7 +184,26 @@ const AmountInputBase: React.FC< {title} - {toAddress ? : null} + {!hiddenToAddress && !(disabledToAddress && !toAddress) ? ( + + navigate({ + to: toAddresses?.length + ? navigationRoutes.configuredWallets + : navigationRoutes.sendToWallet, + }) + } + /> + ) : null} diff --git a/packages/widget/src/components/Card/CardHeader.tsx b/packages/widget/src/components/Card/CardHeader.tsx deleted file mode 100644 index 71a48fe46..000000000 --- a/packages/widget/src/components/Card/CardHeader.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { - cardHeaderClasses, - CardHeader as MuiCardHeader, - styled, -} from '@mui/material' - -export const CardHeader = styled(MuiCardHeader)(({ theme }) => ({ - [`.${cardHeaderClasses.action}`]: { - marginTop: -2, - alignSelf: 'center', - }, - [`.${cardHeaderClasses.title}`]: { - fontWeight: 600, - fontSize: 18, - lineHeight: 1.3334, - color: theme.vars.palette.text.primary, - textAlign: 'left', - }, - [`.${cardHeaderClasses.subheader}`]: { - fontWeight: 500, - fontSize: 12, - lineHeight: 1.3334, - color: theme.vars.palette.text.secondary, - textAlign: 'left', - }, -})) diff --git a/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.style.tsx b/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.style.tsx index a341ae91c..6868969a0 100644 --- a/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.style.tsx +++ b/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.style.tsx @@ -2,8 +2,8 @@ import { Box, styled } from '@mui/material' import { Card } from '../Card/Card.js' export const IconCard = styled(Card)(({ theme }) => ({ - height: 32, - width: 32, + height: 40, + width: 40, fontSize: 16, display: 'flex', alignItems: 'center', @@ -17,15 +17,12 @@ export const ReverseContainer = styled(Box)(({ theme }) => { display: 'flex', justifyContent: 'center', alignItems: 'center', - margin: theme.spacing(-1), + margin: theme.spacing(-2.5), } }) -export const ReverseTokensButtonEmpty = styled(Box)(({ theme }) => { - return { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - margin: theme.spacing(1), - } +export const ReverseTokensButtonEmpty = styled(Box)({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', }) diff --git a/packages/widget/src/components/SelectChainAndToken.tsx b/packages/widget/src/components/SelectChainAndToken.tsx index 1cfdc3317..ea22b46ec 100644 --- a/packages/widget/src/components/SelectChainAndToken.tsx +++ b/packages/widget/src/components/SelectChainAndToken.tsx @@ -3,7 +3,6 @@ import { Box, useMediaQuery } from '@mui/material' import { ReverseTokensButton } from '../components/ReverseTokensButton/ReverseTokensButton.js' import { SelectTokenButton } from '../components/SelectTokenButton/SelectTokenButton.js' import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js' -import { useFieldValues } from '../stores/form/useFieldValues.js' import { DisabledUI, HiddenUI } from '../types/widget.js' import { ReverseTokensButtonEmpty } from './ReverseTokensButton/ReverseTokensButton.style.js' @@ -13,13 +12,6 @@ export const SelectChainAndToken: React.FC = (props) => { ) const { disabledUI, hiddenUI, subvariant } = useWidgetConfig() - const [fromChain, toChain, fromToken, toToken] = useFieldValues( - 'fromChain', - 'toChain', - 'fromToken', - 'toToken' - ) - const hiddenReverse = subvariant === 'refuel' || disabledUI?.includes(DisabledUI.FromToken) || @@ -32,18 +24,15 @@ export const SelectChainAndToken: React.FC = (props) => { const hiddenToToken = subvariant === 'custom' || hiddenUI?.includes(HiddenUI.ToToken) - const isCompact = - !!fromChain && - !!toChain && - !!fromToken && - !!toToken && - !prefersNarrowView && - !hiddenToToken && - !hiddenFromToken + const isCompact = !prefersNarrowView && !hiddenToToken && !hiddenFromToken return ( {!hiddenFromToken ? ( diff --git a/packages/widget/src/components/SelectTokenButton/SelectTokenButton.style.tsx b/packages/widget/src/components/SelectTokenButton/SelectTokenButton.style.tsx index f05b35ced..95a05d0d1 100644 --- a/packages/widget/src/components/SelectTokenButton/SelectTokenButton.style.tsx +++ b/packages/widget/src/components/SelectTokenButton/SelectTokenButton.style.tsx @@ -75,13 +75,14 @@ export const TokenLabelColumn = styled(Box)(() => ({ flexDirection: 'column', minWidth: 0, flex: 1, + textAlign: 'left', })) export const TokenNameText = styled(Typography, { shouldForwardProp: (prop) => prop !== 'selected', })<{ selected?: boolean }>(({ theme, selected }) => ({ fontSize: 18, - fontWeight: 700, + fontWeight: selected ? 700 : 500, lineHeight: 1.3333, color: selected ? theme.vars.palette.text.primary diff --git a/packages/widget/src/components/SendToWallet/SendToWallet.style.tsx b/packages/widget/src/components/SendToWallet/SendToWallet.style.tsx deleted file mode 100644 index 895044ad8..000000000 --- a/packages/widget/src/components/SendToWallet/SendToWallet.style.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { cardHeaderClasses, styled } from '@mui/material' -import { CardHeader } from '../Card/CardHeader.js' - -export const SendToWalletCardHeader = styled(CardHeader, { - shouldForwardProp: (prop) => !['selected'].includes(prop as string), -})<{ selected?: boolean }>(({ theme }) => ({ - width: '100%', - [`.${cardHeaderClasses.title}`]: { - color: theme.vars.palette.text.secondary, - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', - fontWeight: 500, - width: 254, - [theme.breakpoints.down(theme.breakpoints.values.sm)]: { - width: 224, - }, - }, - [`.${cardHeaderClasses.subheader}`]: { - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', - width: 254, - [theme.breakpoints.down(theme.breakpoints.values.sm)]: { - width: 224, - }, - }, - [`.${cardHeaderClasses.action}`]: { - marginRight: 0, - }, - [`.${cardHeaderClasses.action} > button`]: { - fontSize: 16, - }, - variants: [ - { - props: ({ selected }) => selected, - style: { - [`.${cardHeaderClasses.title}`]: { - color: theme.vars.palette.text.primary, - fontWeight: 600, - width: 224, - [theme.breakpoints.down(theme.breakpoints.values.sm)]: { - width: 192, - }, - }, - [`.${cardHeaderClasses.subheader}`]: { - width: 224, - [theme.breakpoints.down(theme.breakpoints.values.sm)]: { - width: 192, - }, - }, - }, - }, - ], -})) diff --git a/packages/widget/src/components/SendToWallet/SendToWalletButton.tsx b/packages/widget/src/components/SendToWallet/SendToWalletButton.tsx deleted file mode 100644 index b1c168c36..000000000 --- a/packages/widget/src/components/SendToWallet/SendToWalletButton.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { useAccount } from '@lifi/wallet-management' -import { useChainTypeFromAddress } from '@lifi/widget-provider' -import CloseRounded from '@mui/icons-material/CloseRounded' -import { Box, Collapse } from '@mui/material' -import { useNavigate } from '@tanstack/react-router' -import { type MouseEventHandler, useEffect, useRef } from 'react' -import { useTranslation } from 'react-i18next' -import { useToAddressRequirements } from '../../hooks/useToAddressRequirements.js' -import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' -import { useBookmarkActions } from '../../stores/bookmarks/useBookmarkActions.js' -import { useBookmarks } from '../../stores/bookmarks/useBookmarks.js' -import { useFieldActions } from '../../stores/form/useFieldActions.js' -import { useFieldValues } from '../../stores/form/useFieldValues.js' -import { useSendToWalletStore } from '../../stores/settings/useSendToWalletStore.js' -import { DisabledUI, HiddenUI } from '../../types/widget.js' -import { defaultChainIdsByType } from '../../utils/chainType.js' -import { navigationRoutes } from '../../utils/navigationRoutes.js' -import { shortenAddress } from '../../utils/wallet.js' -import { AccountAvatar } from '../Avatar/AccountAvatar.js' -import type { CardProps } from '../Card/Card.js' -import { Card } from '../Card/Card.js' -import { CardIconButton } from '../Card/CardIconButton.js' -import { CardTitle } from '../Card/CardTitle.js' -import { SendToWalletCardHeader } from './SendToWallet.style.js' - -export const SendToWalletButton: React.FC = (props) => { - const { t } = useTranslation() - const navigate = useNavigate() - const { - disabledUI, - hiddenUI, - toAddress, - toAddresses, - subvariant, - subvariantOptions, - } = useWidgetConfig() - const { showSendToWallet } = useSendToWalletStore((state) => state) - const [toAddressFieldValue, toChainId, toTokenAddress] = useFieldValues( - 'toAddress', - 'toChain', - 'toToken' - ) - const { setFieldValue } = useFieldActions() - const { selectedBookmark } = useBookmarks() - const { setSelectedBookmark } = useBookmarkActions() - const { accounts } = useAccount() - const { requiredToAddress } = useToAddressRequirements() - const { getChainTypeFromAddress } = useChainTypeFromAddress() - const disabledToAddress = disabledUI?.includes(DisabledUI.ToAddress) - const hiddenToAddress = hiddenUI?.includes(HiddenUI.ToAddress) - - const address = toAddressFieldValue - ? shortenAddress(toAddressFieldValue) - : t('sendToWallet.enterAddress', { - context: 'short', - }) - - const matchingConnectedAccount = accounts.find( - (account) => account.address === toAddressFieldValue - ) - - const chainType = !matchingConnectedAccount - ? selectedBookmark?.chainType || - (toAddressFieldValue - ? getChainTypeFromAddress(toAddressFieldValue) - : undefined) - : undefined - - const chainId = - toChainId && toTokenAddress - ? toChainId - : matchingConnectedAccount - ? matchingConnectedAccount.chainId - : chainType - ? defaultChainIdsByType[chainType] - : undefined - - const isConnectedAccount = - selectedBookmark?.isConnectedAccount && - matchingConnectedAccount?.isConnected - const connectedAccountName = matchingConnectedAccount?.connector?.name - const bookmarkName = selectedBookmark?.name - - const headerTitle = isConnectedAccount - ? connectedAccountName || address - : bookmarkName || connectedAccountName || address - - const headerSubheader = - isConnectedAccount || bookmarkName || connectedAccountName ? address : null - - const disabledForChanges = Boolean(toAddressFieldValue) && disabledToAddress - - const handleOnClick = () => { - navigate({ - to: toAddresses?.length - ? navigationRoutes.configuredWallets - : navigationRoutes.sendToWallet, - }) - } - - const clearSelectedBookmark: MouseEventHandler = (e) => { - e.stopPropagation() - setFieldValue('toAddress', '', { isTouched: true }) - setSelectedBookmark() - } - - // The collapse opens instantly on first page load/component mount when there is an address to display - // After which it needs an animated transition for open and closing. - // collapseTransitionTime is used specify the transition time for opening and closing - const collapseTransitionTime = useRef(0) - - // Timeout is needed here to push the collapseTransitionTime update to the back of the event loop so that it doesn't fired too quickly - useEffect(() => { - const timeout = setTimeout(() => { - collapseTransitionTime.current = 225 - }, 0) - return () => clearTimeout(timeout) - }, []) - - const isOpenCollapse = - !hiddenToAddress && (requiredToAddress || showSendToWallet) - - const title = - subvariant === 'custom' && subvariantOptions?.custom === 'deposit' - ? t('header.depositTo') - : t('header.sendToWallet') - - return ( - - - {title} - - - } - title={headerTitle} - subheader={headerSubheader} - selected={!!toAddressFieldValue || disabledToAddress} - action={ - !!toAddressFieldValue && !disabledForChanges ? ( - - - - ) : null - } - /> - - - - ) -} diff --git a/packages/widget/src/components/SendToWallet/SendToWalletExpandButton.tsx b/packages/widget/src/components/SendToWallet/SendToWalletExpandButton.tsx deleted file mode 100644 index effb0d6dd..000000000 --- a/packages/widget/src/components/SendToWallet/SendToWalletExpandButton.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import Wallet from '@mui/icons-material/Wallet' -import { Button, Tooltip } from '@mui/material' -import { useTranslation } from 'react-i18next' -import { useToAddressRequirements } from '../../hooks/useToAddressRequirements.js' -import { useWidgetEvents } from '../../hooks/useWidgetEvents.js' -import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' -import { useBookmarkActions } from '../../stores/bookmarks/useBookmarkActions.js' -import { useFieldActions } from '../../stores/form/useFieldActions.js' -import { useFieldValues } from '../../stores/form/useFieldValues.js' -import { - sendToWalletStore, - useSendToWalletStore, -} from '../../stores/settings/useSendToWalletStore.js' -import { WidgetEvent } from '../../types/events.js' -import { DisabledUI, HiddenUI } from '../../types/widget.js' - -export const SendToWalletExpandButton: React.FC = () => { - const { t } = useTranslation() - const { disabledUI, hiddenUI } = useWidgetConfig() - const { setFieldValue } = useFieldActions() - const { setSelectedBookmark } = useBookmarkActions() - const emitter = useWidgetEvents() - const { showSendToWallet, setSendToWallet } = useSendToWalletStore( - (state) => state - ) - const [toAddressFieldValue] = useFieldValues('toAddress') - const { requiredToAddress } = useToAddressRequirements() - - if (requiredToAddress || hiddenUI?.includes(HiddenUI.ToAddress)) { - return null - } - - const handleClick = () => { - if (showSendToWallet && !disabledUI?.includes(DisabledUI.ToAddress)) { - setFieldValue('toAddress', '', { isTouched: true }) - setSelectedBookmark() - } - setSendToWallet(!showSendToWallet) - emitter.emit( - WidgetEvent.SendToWalletToggled, - sendToWalletStore.getState().showSendToWallet - ) - } - - const buttonVariant = - showSendToWallet || Boolean(toAddressFieldValue) ? 'contained' : 'text' - - return ( - - - - ) -} diff --git a/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx b/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx index 8d4708daf..481d05704 100644 --- a/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx +++ b/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx @@ -4,14 +4,22 @@ import { shortenAddress } from '../../utils/wallet.js' import { BadgeRoot } from './WalletAddressBadge.style.js' interface WalletAddressBadgeProps { - address: string + address?: string + label?: string + onClick?: React.MouseEventHandler } export const WalletAddressBadge: React.FC = ({ address, + label, + onClick, }) => { return ( - + = ({ px: 0.5, }} > - {shortenAddress(address)} + {address ? shortenAddress(address) : label} ) diff --git a/packages/widget/src/hooks/useToAddressAutoPopulate.ts b/packages/widget/src/hooks/useToAddressAutoPopulate.ts index 24bd48ca5..26172309b 100644 --- a/packages/widget/src/hooks/useToAddressAutoPopulate.ts +++ b/packages/widget/src/hooks/useToAddressAutoPopulate.ts @@ -4,7 +4,6 @@ import { useCallback } from 'react' import { useBookmarkActions } from '../stores/bookmarks/useBookmarkActions.js' import type { FormType } from '../stores/form/types.js' import { useFieldActions } from '../stores/form/useFieldActions.js' -import { useSendToWalletActions } from '../stores/settings/useSendToWalletStore.js' import { useAvailableChains } from './useAvailableChains.js' type UpdateToAddressArgs = { @@ -20,7 +19,6 @@ type UpdateToAddressArgs = { */ export const useToAddressAutoPopulate = () => { const { setFieldValue } = useFieldActions() - const { setSendToWallet } = useSendToWalletActions() const { setSelectedBookmark } = useBookmarkActions() const { getChainById } = useAvailableChains() const { accounts } = useAccount() @@ -81,7 +79,6 @@ export const useToAddressAutoPopulate = () => { chainType: destinationAccount.chainType, isConnectedAccount: true, }) - setSendToWallet(true) return destinationAccount.address } }, @@ -90,7 +87,6 @@ export const useToAddressAutoPopulate = () => { getChainById, setFieldValue, setSelectedBookmark, - setSendToWallet, getChainTypeFromAddress, ] ) diff --git a/packages/widget/src/hooks/useToAddressReset.ts b/packages/widget/src/hooks/useToAddressReset.ts index 32ac94974..776978a38 100644 --- a/packages/widget/src/hooks/useToAddressReset.ts +++ b/packages/widget/src/hooks/useToAddressReset.ts @@ -4,16 +4,13 @@ import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js' import { useBookmarkActions } from '../stores/bookmarks/useBookmarkActions.js' import { useBookmarks } from '../stores/bookmarks/useBookmarks.js' import { useFieldActions } from '../stores/form/useFieldActions.js' -import { useSendToWalletActions } from '../stores/settings/useSendToWalletStore.js' import { RequiredUI } from '../types/widget.js' export const useToAddressReset = () => { const { requiredUI } = useWidgetConfig() - const { setFieldValue, isDirty } = useFieldActions() + const { setFieldValue } = useFieldActions() const { selectedBookmark } = useBookmarks() const { setSelectedBookmark } = useBookmarkActions() - const { setSendToWallet } = useSendToWalletActions() - const tryResetToAddress = useCallback( (toChain: ExtendedChain) => { const requiredToAddress = requiredUI?.includes(RequiredUI.ToAddress) @@ -24,30 +21,14 @@ export const useToAddressReset = () => { const shouldResetToAddress = !requiredToAddress && !bookmarkSatisfiesToChainType - // The toAddress field is required and always visible when bridging between - // different ecosystems (fromChain and toChain have different chain types). // We reset toAddress on each chain change if it's no longer required, ensuring that - // switching chain types doesn't leave a previously set toAddress value when - // the "Send to Wallet" field is hidden. + // switching chain types doesn't leave a stale toAddress from a different ecosystem. if (shouldResetToAddress) { setFieldValue('toAddress', '', { isTouched: true }) setSelectedBookmark() - // If toAddress was auto-filled (e.g., when making cross-ecosystem bridging and compatible destination wallet was connected) - // and not manually edited by the user, we need to hide "Send to Wallet". - const isToAddressDirty = isDirty('toAddress') - if (!isToAddressDirty) { - setSendToWallet(false) - } } }, - [ - setFieldValue, - isDirty, - setSelectedBookmark, - setSendToWallet, - requiredUI, - selectedBookmark, - ] + [setFieldValue, setSelectedBookmark, requiredUI, selectedBookmark] ) return { diff --git a/packages/widget/src/pages/MainPage/MainPage.tsx b/packages/widget/src/pages/MainPage/MainPage.tsx index 252a0b375..edf8a4c8e 100644 --- a/packages/widget/src/pages/MainPage/MainPage.tsx +++ b/packages/widget/src/pages/MainPage/MainPage.tsx @@ -7,8 +7,6 @@ import { PageContainer } from '../../components/PageContainer.js' import { PoweredBy } from '../../components/PoweredBy/PoweredBy.js' import { Routes } from '../../components/Routes/Routes.js' import { SelectChainAndToken } from '../../components/SelectChainAndToken.js' -import { SendToWalletButton } from '../../components/SendToWallet/SendToWalletButton.js' -import { SendToWalletExpandButton } from '../../components/SendToWallet/SendToWalletExpandButton.js' import { useHeader } from '../../hooks/useHeader.js' import { useWideVariant } from '../../hooks/useWideVariant.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' @@ -54,7 +52,6 @@ export const MainPage: React.FC = () => { ) : null} {!wideVariant ? : null} - {showGasRefuelMessage ? : null} { }} > - {showPoweredBy ? : null} diff --git a/packages/widget/src/pages/SendToWallet/BookmarksPage.tsx b/packages/widget/src/pages/SendToWallet/BookmarksPage.tsx index c3d391685..07c3d824a 100644 --- a/packages/widget/src/pages/SendToWallet/BookmarksPage.tsx +++ b/packages/widget/src/pages/SendToWallet/BookmarksPage.tsx @@ -20,7 +20,6 @@ import type { Bookmark } from '../../stores/bookmarks/types.js' import { useBookmarkActions } from '../../stores/bookmarks/useBookmarkActions.js' import { useBookmarks } from '../../stores/bookmarks/useBookmarks.js' import { useFieldActions } from '../../stores/form/useFieldActions.js' -import { useSendToWalletActions } from '../../stores/settings/useSendToWalletStore.js' import { defaultChainIdsByType } from '../../utils/chainType.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { shortenAddress } from '../../utils/wallet.js' @@ -43,7 +42,6 @@ export const BookmarksPage = () => { useBookmarkActions() const navigate = useNavigate() const { setFieldValue } = useFieldActions() - const { setSendToWallet } = useSendToWalletActions() const { variant } = useWidgetConfig() const { getAddressLink } = useExplorer() @@ -59,7 +57,6 @@ export const BookmarksPage = () => { isDirty: true, }) setSelectedBookmark(bookmark) - setSendToWallet(true) navigate({ to: navigationRoutes.home, replace: true }) } diff --git a/packages/widget/src/pages/SendToWallet/ConfirmAddressSheet.tsx b/packages/widget/src/pages/SendToWallet/ConfirmAddressSheet.tsx index 707fdc145..65a76277a 100644 --- a/packages/widget/src/pages/SendToWallet/ConfirmAddressSheet.tsx +++ b/packages/widget/src/pages/SendToWallet/ConfirmAddressSheet.tsx @@ -11,7 +11,6 @@ import { useNavigateBack } from '../../hooks/useNavigateBack.js' import { useSetContentHeight } from '../../hooks/useSetContentHeight.js' import type { Bookmark } from '../../stores/bookmarks/types.js' import { useFieldActions } from '../../stores/form/useFieldActions.js' -import { useSendToWalletActions } from '../../stores/settings/useSendToWalletStore.js' import { IconContainer, SendToWalletButtonRow, @@ -52,8 +51,6 @@ const ConfirmAddressSheetContent: React.FC = ({ const { t } = useTranslation() const navigateBack = useNavigateBack() const { setFieldValue } = useFieldActions() - const { setSendToWallet } = useSendToWalletActions() - const containerRef = useRef(null) useSetContentHeight(containerRef) @@ -64,7 +61,6 @@ const ConfirmAddressSheetContent: React.FC = ({ isDirty: true, }) onConfirm?.(validatedBookmark) - setSendToWallet(true) onClose() navigateBack() } diff --git a/packages/widget/src/pages/SendToWallet/ConnectedWalletsPage.tsx b/packages/widget/src/pages/SendToWallet/ConnectedWalletsPage.tsx index 584fb9b5a..a0ae57246 100644 --- a/packages/widget/src/pages/SendToWallet/ConnectedWalletsPage.tsx +++ b/packages/widget/src/pages/SendToWallet/ConnectedWalletsPage.tsx @@ -17,7 +17,6 @@ import { useHeader } from '../../hooks/useHeader.js' import { useToAddressRequirements } from '../../hooks/useToAddressRequirements.js' import { useBookmarkActions } from '../../stores/bookmarks/useBookmarkActions.js' import { useFieldActions } from '../../stores/form/useFieldActions.js' -import { useSendToWalletActions } from '../../stores/settings/useSendToWalletStore.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { shortenAddress } from '../../utils/wallet.js' import { EmptyListIndicator } from './EmptyListIndicator.js' @@ -35,7 +34,6 @@ export const ConnectedWalletsPage = () => { const { requiredToChainType } = useToAddressRequirements() const navigate = useNavigate() const { setFieldValue } = useFieldActions() - const { setSendToWallet } = useSendToWalletActions() const [moreMenuAnchorEl, setMenuAnchorEl] = useState() const moreMenuId = useId() const open = Boolean(moreMenuAnchorEl) @@ -54,7 +52,6 @@ export const ConnectedWalletsPage = () => { chainType: account.chainType!, isConnectedAccount: true, }) - setSendToWallet(true) navigate({ to: navigationRoutes.home, replace: true, diff --git a/packages/widget/src/pages/SendToWallet/RecentWalletsPage.tsx b/packages/widget/src/pages/SendToWallet/RecentWalletsPage.tsx index cff25ae09..0688ca3f1 100644 --- a/packages/widget/src/pages/SendToWallet/RecentWalletsPage.tsx +++ b/packages/widget/src/pages/SendToWallet/RecentWalletsPage.tsx @@ -20,7 +20,6 @@ import type { Bookmark } from '../../stores/bookmarks/types.js' import { useBookmarkActions } from '../../stores/bookmarks/useBookmarkActions.js' import { useBookmarks } from '../../stores/bookmarks/useBookmarks.js' import { useFieldActions } from '../../stores/form/useFieldActions.js' -import { useSendToWalletActions } from '../../stores/settings/useSendToWalletStore.js' import { defaultChainIdsByType } from '../../utils/chainType.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { shortenAddress } from '../../utils/wallet.js' @@ -46,7 +45,6 @@ export const RecentWalletsPage = () => { addRecentWallet, } = useBookmarkActions() const { setFieldValue } = useFieldActions() - const { setSendToWallet } = useSendToWalletActions() const moreMenuId = useId() const [moreMenuAnchorEl, setMenuAnchorEl] = useState() const open = Boolean(moreMenuAnchorEl) @@ -61,7 +59,6 @@ export const RecentWalletsPage = () => { isDirty: true, }) setSelectedBookmark(recentWallet) - setSendToWallet(true) navigate({ to: navigationRoutes.home, replace: true, diff --git a/packages/widget/src/stores/form/FormUpdater.tsx b/packages/widget/src/stores/form/FormUpdater.tsx index a7f83ccfb..b4ea32b77 100644 --- a/packages/widget/src/stores/form/FormUpdater.tsx +++ b/packages/widget/src/stores/form/FormUpdater.tsx @@ -3,7 +3,6 @@ import { useEffect } from 'react' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { useBookmarkActions } from '../../stores/bookmarks/useBookmarkActions.js' import { formDefaultValues } from '../../stores/form/createFormStore.js' -import { useSendToWalletActions } from '../../stores/settings/useSendToWalletStore.js' import type { DefaultValues } from './types.js' import { useFieldActions } from './useFieldActions.js' @@ -12,7 +11,6 @@ export const FormUpdater: React.FC<{ }> = ({ reactiveFormValues }) => { const { toAddress } = useWidgetConfig() const { account } = useAccount() - const { setSendToWallet } = useSendToWalletActions() const { setSelectedBookmark } = useBookmarkActions() const { setUserAndDefaultValues } = useFieldActions() @@ -20,9 +18,6 @@ export const FormUpdater: React.FC<{ // Includes special logic for chain fields, where account.chainId is only a fallback and not a direct reactivity source. // biome-ignore lint/correctness/useExhaustiveDependencies: account.chainId is used as a fallback only and does not need to be a dependency for reactivity. useEffect(() => { - if (reactiveFormValues.toAddress) { - setSendToWallet(true) - } if (toAddress) { setSelectedBookmark(toAddress) } @@ -34,7 +29,6 @@ export const FormUpdater: React.FC<{ toAddress, reactiveFormValues, setUserAndDefaultValues, - setSendToWallet, setSelectedBookmark, ]) diff --git a/packages/widget/src/stores/form/URLSearchParamsBuilder.tsx b/packages/widget/src/stores/form/URLSearchParamsBuilder.tsx index 69cdefc7a..fe927c3ce 100644 --- a/packages/widget/src/stores/form/URLSearchParamsBuilder.tsx +++ b/packages/widget/src/stores/form/URLSearchParamsBuilder.tsx @@ -2,7 +2,6 @@ import { useLocation } from '@tanstack/react-router' import { useEffect } from 'react' import { useAddressValidation } from '../../hooks/useAddressValidation.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' -import { useSendToWalletActions } from '../../stores/settings/useSendToWalletStore.js' import { useBookmarkActions } from '../bookmarks/useBookmarkActions.js' import type { FormFieldNames } from '../form/types.js' import { useFieldActions } from '../form/useFieldActions.js' @@ -23,7 +22,6 @@ export const URLSearchParamsBuilder = () => { const { pathname } = useLocation() const touchedFields = useTouchedFields() const values = useFieldValues(...formValueKeys) - const { setSendToWallet } = useSendToWalletActions() const { setSelectedBookmark, addRecentWallet } = useBookmarkActions() const { validateAddress } = useAddressValidation() const { buildUrl } = useWidgetConfig() @@ -58,7 +56,6 @@ export const URLSearchParamsBuilder = () => { setUserAndDefaultValues({ toAddress }) setSelectedBookmark(bookmark) addRecentWallet(bookmark) - setSendToWallet(true) } } catch (_) { // Address validation failed @@ -70,7 +67,6 @@ export const URLSearchParamsBuilder = () => { initializeFromAddress() }, [ setUserAndDefaultValues, - setSendToWallet, validateAddress, setSelectedBookmark, addRecentWallet, diff --git a/packages/widget/src/stores/form/useFormRef.ts b/packages/widget/src/stores/form/useFormRef.ts index a751aa84f..2c812df30 100644 --- a/packages/widget/src/stores/form/useFormRef.ts +++ b/packages/widget/src/stores/form/useFormRef.ts @@ -1,12 +1,10 @@ import { useImperativeHandle } from 'react' import { useBookmarkActions } from '../../stores/bookmarks/useBookmarkActions.js' import { formDefaultValues } from '../../stores/form/createFormStore.js' -import { useSendToWalletActions } from '../../stores/settings/useSendToWalletStore.js' import type { FormRef } from '../../types/widget.js' import type { FormStoreStore, GenericFormValue } from './types.js' export const useFormRef = (formStore: FormStoreStore, formRef?: FormRef) => { - const { setSendToWallet } = useSendToWalletActions() const { setSelectedBookmark } = useBookmarkActions() useImperativeHandle(formRef, () => { @@ -26,15 +24,6 @@ export const useFormRef = (formStore: FormStoreStore, formRef?: FormRef) => { (isToAddressObj ? value?.address : value) || formDefaultValues.toAddress - // sets the send to wallet button state to be open - // if there is an address to display - if (address) { - setSendToWallet(address) - } - - // we can assume that the toAddress has been passed as ToAddress object - // and display it accordingly - this ensures that if a name is included - // that it is displayed in the Send To Wallet form field correctly if (isToAddressObj) { setSelectedBookmark(value) } @@ -58,5 +47,5 @@ export const useFormRef = (formStore: FormStoreStore, formRef?: FormRef) => { .setFieldValue(fieldName, sanitizedValue, fieldValueOptions) }, } - }, [formStore, setSendToWallet, setSelectedBookmark]) + }, [formStore, setSelectedBookmark]) } diff --git a/packages/widget/src/stores/settings/types.ts b/packages/widget/src/stores/settings/types.ts index 8726b88fb..a9f4886a1 100644 --- a/packages/widget/src/stores/settings/types.ts +++ b/packages/widget/src/stores/settings/types.ts @@ -48,14 +48,6 @@ export interface SettingsActions { export type SettingsState = SettingsProps & SettingsActions -interface SendToWalletState { - showSendToWallet: boolean -} - -export interface SendToWalletStore extends SendToWalletState { - setSendToWallet(value: boolean): void -} - export interface SplitSubvariantState { state?: SplitSubvariant setState(state: SplitSubvariant): void diff --git a/packages/widget/src/stores/settings/useSendToWalletStore.ts b/packages/widget/src/stores/settings/useSendToWalletStore.ts deleted file mode 100644 index a790dbf47..000000000 --- a/packages/widget/src/stores/settings/useSendToWalletStore.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { create } from 'zustand' -import { useShallow } from 'zustand/shallow' -import type { SendToWalletStore } from './types.js' - -export const sendToWalletStore = create((set) => ({ - showSendToWallet: false, - setSendToWallet: (value) => - set({ - showSendToWallet: value, - }), -})) - -export const useSendToWalletStore = ( - selector: (state: SendToWalletStore) => T -): T => { - return sendToWalletStore(useShallow(selector)) -} - -export const useSendToWalletActions = () => { - const actions = useSendToWalletStore((store) => ({ - setSendToWallet: store.setSendToWallet, - })) - - return actions -} From da5c1681addd08e13f86ae8ff37ea4f2917f05ab Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Mon, 9 Mar 2026 17:46:51 +0000 Subject: [PATCH 06/22] feat: add receipts card --- .../components/AmountInput/AmountInput.tsx | 8 ++++- .../WalletAddressBadge/WalletAddressBadge.tsx | 2 +- packages/widget/src/i18n/en.json | 1 + .../TransactionDetailsPage.tsx | 36 ++++++++++--------- .../StatusBottomSheet.style.tsx | 19 ++++++---- .../TransactionPage/StatusBottomSheet.tsx | 2 +- 6 files changed, 41 insertions(+), 27 deletions(-) diff --git a/packages/widget/src/components/AmountInput/AmountInput.tsx b/packages/widget/src/components/AmountInput/AmountInput.tsx index ed0fc1b6a..86cef8ff4 100644 --- a/packages/widget/src/components/AmountInput/AmountInput.tsx +++ b/packages/widget/src/components/AmountInput/AmountInput.tsx @@ -1,4 +1,5 @@ import type { Token } from '@lifi/sdk' +import { useAccount } from '@lifi/wallet-management' import type { CardProps } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import type { ChangeEvent, ReactNode } from 'react' @@ -93,9 +94,14 @@ const AmountInputBase: React.FC< const { inputMode } = useInputModeStore() const { chain } = useChain(chainId) + const { account } = useAccount() const hiddenToAddress = hiddenUI?.includes(HiddenUI.ToAddress) const disabledToAddress = disabledUI?.includes(DisabledUI.ToAddress) + const showWalletBadge = + !hiddenToAddress && + !(disabledToAddress && !toAddress) && + account.isConnected const currentInputMode = inputMode[formType] let displayValue: string @@ -184,7 +190,7 @@ const AmountInputBase: React.FC< {title} - {!hiddenToAddress && !(disabledToAddress && !toAddress) ? ( + {showWalletBadge ? ( = ({ fontWeight: 700, lineHeight: '16px', color: 'text.primary', - px: 0.5, + p: 0.5, }} > {address ? shortenAddress(address) : label} diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json index f81937f1a..76261b90b 100644 --- a/packages/widget/src/i18n/en.json +++ b/packages/widget/src/i18n/en.json @@ -240,6 +240,7 @@ "pinnedTokens": "Pinned tokens", "popularTokens": "Popular tokens", "priceImpact": "Price impact", + "receipts": "Receipts", "sentToWallet": "Sent to wallet: {{address}}", "process": { "bridge": { diff --git a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx index 4d122ead9..4364b38ff 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx @@ -1,5 +1,5 @@ import type { FullStatusData } from '@lifi/sdk' -import { Box, Typography } from '@mui/material' +import { Typography } from '@mui/material' import { useLocation, useNavigate } from '@tanstack/react-router' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -99,41 +99,43 @@ export const TransactionDetailsPage: React.FC = () => { } return ( - + - - + {startedAt.toLocaleString(i18n.language, { dateStyle: 'long', })} - - + + {startedAt.toLocaleString(i18n.language, { timeStyle: 'short', })} - - + + - + + + {t('main.receipts')} + + + ) diff --git a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.style.tsx b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.style.tsx index 64560fb9c..0b61d4613 100644 --- a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.style.tsx +++ b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.style.tsx @@ -8,6 +8,8 @@ const getStatusColor = (status: StatusColor, theme: Theme) => { switch (status) { case RouteExecutionStatus.Done: return { + // TODO: how to resolve new colors? + backgroundColor: '#D6FFE7', color: theme.vars.palette.success.mainChannel, alpha: 0.12, lightDarken: 0, @@ -51,23 +53,26 @@ export const IconCircle = styled(Box, { const statusConfig = getStatusColor(status, theme) return { - backgroundColor: `rgba(${statusConfig.color} / ${statusConfig.alpha})`, + backgroundColor: + 'backgroundColor' in statusConfig + ? statusConfig.backgroundColor + : `rgba(${statusConfig.color} / ${statusConfig.alpha})`, borderRadius: '50%', - width: 72, - height: 72, + width: 90, + height: 90, display: 'grid', position: 'relative', placeItems: 'center', '& > svg': { color: `color-mix(in srgb, rgb(${statusConfig.color}) ${(1 - statusConfig.lightDarken) * 100}%, black)`, - width: 36, - height: 36, + width: 48, + height: 48, }, ...theme.applyStyles('dark', { '& > svg': { color: `color-mix(in srgb, rgb(${statusConfig.color}) ${(1 - statusConfig.darkDarken) * 100}%, black)`, - width: 36, - height: 36, + width: 48, + height: 48, }, }), } diff --git a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx index 21a4cc5f1..91420fe85 100644 --- a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx +++ b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx @@ -268,7 +268,7 @@ const StatusBottomSheetContent: React.FC = ({ display: 'flex', flexDirection: 'column', gap: 2, - padding: 2, + padding: 3, }} > Date: Tue, 10 Mar 2026 12:30:19 +0000 Subject: [PATCH 07/22] refactor: status circles --- .../AmountInput/AmountInput.style.tsx | 1 + .../AmountInputAdornment.style.tsx | 2 +- .../widget/src/components/Card/CardLabel.tsx | 6 +- .../IconCircle/IconCircle.style.tsx | 74 +++++++++++++++++++ .../src/components/IconCircle/IconCircle.tsx | 24 ++++++ .../src/components/IconCircle/statusIcons.ts | 13 ++++ .../src/components/RouteCard/RouteCard.tsx | 3 + .../Step/CircularProgress.style.tsx | 34 --------- .../src/components/Step/CircularProgress.tsx | 73 ------------------ .../src/components/Step/ExecutionProgress.tsx | 4 +- .../src/components/Step/RouteDetails.tsx | 4 +- .../components/Step/StepStatusIndicator.tsx | 37 ++++++++++ .../components/Step/TokenWithExpansion.tsx | 2 +- .../components/Step/TransactionLink.style.tsx | 3 +- .../components/StepActions/StepActions.tsx | 40 +++++----- .../Timer/StepStatusTimer.style.tsx | 50 +++++++++++++ .../StepStatusTimer.tsx} | 28 ++----- .../ExchangeRateBottomSheet.tsx | 8 +- .../StatusBottomSheet.style.tsx | 72 ------------------ .../TransactionPage/StatusBottomSheet.tsx | 40 +++++----- .../TransactionPage/TokenValueBottomSheet.tsx | 9 +-- packages/widget/src/utils/getStatusColor.ts | 33 --------- 22 files changed, 268 insertions(+), 292 deletions(-) create mode 100644 packages/widget/src/components/IconCircle/IconCircle.style.tsx create mode 100644 packages/widget/src/components/IconCircle/IconCircle.tsx create mode 100644 packages/widget/src/components/IconCircle/statusIcons.ts delete mode 100644 packages/widget/src/components/Step/CircularProgress.style.tsx delete mode 100644 packages/widget/src/components/Step/CircularProgress.tsx create mode 100644 packages/widget/src/components/Step/StepStatusIndicator.tsx create mode 100644 packages/widget/src/components/Timer/StepStatusTimer.style.tsx rename packages/widget/src/components/{Step/CircularProgressTimer.tsx => Timer/StepStatusTimer.tsx} (81%) delete mode 100644 packages/widget/src/utils/getStatusColor.ts diff --git a/packages/widget/src/components/AmountInput/AmountInput.style.tsx b/packages/widget/src/components/AmountInput/AmountInput.style.tsx index f89ba3eff..abbae656a 100644 --- a/packages/widget/src/components/AmountInput/AmountInput.style.tsx +++ b/packages/widget/src/components/AmountInput/AmountInput.style.tsx @@ -87,6 +87,7 @@ export const LabelDescriptionColumn = styled(Box)(({ theme }) => ({ export const DescriptionRow = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', + justifyContent: 'space-between', gap: theme.spacing(1), width: '100%', })) diff --git a/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx b/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx index ce581f146..f91766435 100644 --- a/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx @@ -7,7 +7,7 @@ export const ButtonContainer = styled(Box)(({ theme }) => ({ })) export const PercentagePill = styled(ButtonTertiary)(({ theme }) => ({ - padding: theme.spacing(0.5, 0.75), + padding: theme.spacing(1, 0.75), lineHeight: 1.3333, fontSize: '0.75rem', fontWeight: 700, diff --git a/packages/widget/src/components/Card/CardLabel.tsx b/packages/widget/src/components/Card/CardLabel.tsx index c59053785..e48bf94dc 100644 --- a/packages/widget/src/components/Card/CardLabel.tsx +++ b/packages/widget/src/components/Card/CardLabel.tsx @@ -45,10 +45,10 @@ export const CardLabel = styled(Box, { export const CardLabelTypography = styled(Typography, { shouldForwardProp: (prop) => prop !== 'type', })<{ type?: 'icon' }>(({ theme }) => ({ - padding: theme.spacing(0.75, 1.5), - fontSize: 12, + padding: theme.spacing(0.75, 1), + fontSize: 10, lineHeight: 1, - fontWeight: '600', + fontWeight: '700', variants: [ { props: { diff --git a/packages/widget/src/components/IconCircle/IconCircle.style.tsx b/packages/widget/src/components/IconCircle/IconCircle.style.tsx new file mode 100644 index 000000000..f2bfbd37f --- /dev/null +++ b/packages/widget/src/components/IconCircle/IconCircle.style.tsx @@ -0,0 +1,74 @@ +import type { Theme } from '@mui/material' +import { Box, styled } from '@mui/material' + +export const iconCircleSize = 90 + +interface StatusColorConfig { + color: string + alpha: number + lightDarken: number + darkDarken: number +} + +export type StatusColor = 'success' | 'error' | 'warning' | 'info' + +export const getStatusColor = ( + status: StatusColor, + theme: Theme +): StatusColorConfig => { + switch (status) { + case 'success': + return { + color: theme.vars.palette.success.mainChannel, + alpha: 0.12, + lightDarken: 0, + darkDarken: 0, + } + case 'error': + return { + color: theme.vars.palette.error.mainChannel, + alpha: 0.12, + lightDarken: 0, + darkDarken: 0, + } + case 'warning': + return { + color: theme.vars.palette.warning.mainChannel, + alpha: 0.48, + lightDarken: 0.32, + darkDarken: 0, + } + case 'info': + default: + return { + color: theme.vars.palette.info.mainChannel, + alpha: 0.12, + lightDarken: 0, + darkDarken: 0, + } + } +} + +export const IconCircleRoot = styled(Box, { + shouldForwardProp: (prop) => prop !== 'colorConfig', +})<{ colorConfig: StatusColorConfig }>(({ theme, colorConfig }) => ({ + backgroundColor: `rgba(${colorConfig.color} / ${colorConfig.alpha})`, + borderRadius: '50%', + width: iconCircleSize, + height: iconCircleSize, + display: 'grid', + position: 'relative', + placeItems: 'center', + '& > svg': { + color: `color-mix(in srgb, rgb(${colorConfig.color}) ${(1 - colorConfig.lightDarken) * 100}%, black)`, + width: 48, + height: 48, + }, + ...theme.applyStyles('dark', { + '& > svg': { + color: `color-mix(in srgb, rgb(${colorConfig.color}) ${(1 - colorConfig.darkDarken) * 100}%, black)`, + width: 48, + height: 48, + }, + }), +})) diff --git a/packages/widget/src/components/IconCircle/IconCircle.tsx b/packages/widget/src/components/IconCircle/IconCircle.tsx new file mode 100644 index 000000000..0938fafcb --- /dev/null +++ b/packages/widget/src/components/IconCircle/IconCircle.tsx @@ -0,0 +1,24 @@ +import type { BoxProps } from '@mui/material' +import { useTheme } from '@mui/material' +import type { StatusColor } from './IconCircle.style.js' +import { getStatusColor, IconCircleRoot } from './IconCircle.style.js' +import { statusIcons } from './statusIcons.js' + +interface IconCircleProps extends Omit { + status: StatusColor +} + +export const IconCircle: React.FC = ({ status, ...rest }) => { + const theme = useTheme() + const colorConfig = getStatusColor(status, theme) + const Icon = statusIcons[status] + + return ( + + + + ) +} + +export type { StatusColor } from './IconCircle.style.js' +export { iconCircleSize } from './IconCircle.style.js' diff --git a/packages/widget/src/components/IconCircle/statusIcons.ts b/packages/widget/src/components/IconCircle/statusIcons.ts new file mode 100644 index 000000000..25ba7aac7 --- /dev/null +++ b/packages/widget/src/components/IconCircle/statusIcons.ts @@ -0,0 +1,13 @@ +import Done from '@mui/icons-material/Done' +import ErrorRounded from '@mui/icons-material/ErrorRounded' +import InfoRounded from '@mui/icons-material/InfoRounded' +import WarningRounded from '@mui/icons-material/WarningRounded' +import type { SvgIcon } from '@mui/material' +import type { StatusColor } from './IconCircle.style.js' + +export const statusIcons: Record = { + success: Done, + error: ErrorRounded, + warning: WarningRounded, + info: InfoRounded, +} diff --git a/packages/widget/src/components/RouteCard/RouteCard.tsx b/packages/widget/src/components/RouteCard/RouteCard.tsx index 7292f4780..e65cf40de 100644 --- a/packages/widget/src/components/RouteCard/RouteCard.tsx +++ b/packages/widget/src/components/RouteCard/RouteCard.tsx @@ -123,6 +123,9 @@ export const RouteCard: React.FC< type={active ? 'selected' : 'default'} selectionColor="secondary" indented + sx={{ + padding: 3, + }} {...other} > {cardContent} diff --git a/packages/widget/src/components/Step/CircularProgress.style.tsx b/packages/widget/src/components/Step/CircularProgress.style.tsx deleted file mode 100644 index f4a0bba65..000000000 --- a/packages/widget/src/components/Step/CircularProgress.style.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { - Box, - CircularProgress as MuiCircularProgress, - styled, - Typography, -} from '@mui/material' - -export const circleSize = 96 - -export const StatusCircle = styled(Box)({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: circleSize, - height: circleSize, - borderRadius: '50%', -}) - -export const RingContainer = styled(Box)({ - position: 'relative', - width: circleSize, - height: circleSize, -}) - -export const ProgressTrack = styled(MuiCircularProgress)({ - position: 'absolute', - color: 'divider', -}) - -export const TimerLabel = styled(Typography)({ - fontSize: 22, - fontWeight: 700, - fontVariantNumeric: 'tabular-nums', -}) diff --git a/packages/widget/src/components/Step/CircularProgress.tsx b/packages/widget/src/components/Step/CircularProgress.tsx deleted file mode 100644 index 3f46bc1b2..000000000 --- a/packages/widget/src/components/Step/CircularProgress.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type { LiFiStepExtended } from '@lifi/sdk' -import Done from '@mui/icons-material/Done' -import ErrorRounded from '@mui/icons-material/ErrorRounded' -import InfoRounded from '@mui/icons-material/InfoRounded' -import WarningRounded from '@mui/icons-material/WarningRounded' -import { useTheme } from '@mui/material' -import { getStatusColor } from '../../utils/getStatusColor.js' -import { StatusCircle } from './CircularProgress.style.js' -import { IndeterminateRing, TimerRing } from './CircularProgressTimer.js' - -interface CircularProgressProps { - step: LiFiStepExtended -} - -const iconSx = { fontSize: 48 } - -export const CircularProgress: React.FC = ({ step }) => { - const theme = useTheme() - - const lastAction = step.execution?.actions?.at(-1) - - if (!step.execution || !lastAction) { - return null - } - - const status = lastAction?.status - const substatus = lastAction?.substatus - - const withTimer = status === 'STARTED' || status === 'PENDING' - const actionRequired = - status === 'ACTION_REQUIRED' || - status === 'MESSAGE_REQUIRED' || - status === 'RESET_REQUIRED' - - if (withTimer) { - if (!step.execution?.signedAt) { - return - } - return - } - - const backgroundColor = getStatusColor(theme, status, substatus) - - if (actionRequired) { - return ( - - - - ) - } - - switch (status) { - case 'DONE': - if (substatus === 'PARTIAL' || substatus === 'REFUNDED') { - return ( - - - - ) - } - return ( - - - - ) - case 'FAILED': - return ( - - - - ) - } -} diff --git a/packages/widget/src/components/Step/ExecutionProgress.tsx b/packages/widget/src/components/Step/ExecutionProgress.tsx index 0ad8acf5b..071c1ef27 100644 --- a/packages/widget/src/components/Step/ExecutionProgress.tsx +++ b/packages/widget/src/components/Step/ExecutionProgress.tsx @@ -1,7 +1,7 @@ import type { RouteExtended } from '@lifi/sdk' import { Box, Typography } from '@mui/material' import { useActionMessage } from '../../hooks/useActionMessage.js' -import { CircularProgress } from './CircularProgress.js' +import { StepStatusIndicator } from './StepStatusIndicator.js' export const ExecutionProgress: React.FC<{ route: RouteExtended @@ -17,7 +17,7 @@ export const ExecutionProgress: React.FC<{ return ( - + { (feeAmountUSD || Number.isFinite(feeConfig?.fee)) && !hasGaslessSupport return ( - + diff --git a/packages/widget/src/components/Step/StepStatusIndicator.tsx b/packages/widget/src/components/Step/StepStatusIndicator.tsx new file mode 100644 index 000000000..c0dfe1b8b --- /dev/null +++ b/packages/widget/src/components/Step/StepStatusIndicator.tsx @@ -0,0 +1,37 @@ +import type { LiFiStepExtended } from '@lifi/sdk' +import { IconCircle } from '../IconCircle/IconCircle.js' +import { IndeterminateRing, TimerRing } from '../Timer/StepStatusTimer.js' + +interface StepStatusIndicatorProps { + step: LiFiStepExtended +} + +export const StepStatusIndicator: React.FC = ({ + step, +}) => { + const lastAction = step.execution?.actions?.at(-1) + + const status = lastAction?.status || 'PENDING' + const substatus = lastAction?.substatus + + switch (status) { + case 'STARTED': + case 'PENDING': { + if (!step.execution?.signedAt) { + return + } + return + } + case 'ACTION_REQUIRED': + case 'MESSAGE_REQUIRED': + case 'RESET_REQUIRED': + return + case 'DONE': + if (substatus === 'PARTIAL' || substatus === 'REFUNDED') { + return + } + return + case 'FAILED': + return + } +} diff --git a/packages/widget/src/components/Step/TokenWithExpansion.tsx b/packages/widget/src/components/Step/TokenWithExpansion.tsx index 960d697f4..9c56df524 100644 --- a/packages/widget/src/components/Step/TokenWithExpansion.tsx +++ b/packages/widget/src/components/Step/TokenWithExpansion.tsx @@ -46,7 +46,7 @@ export const TokenWithExpansion = ({ /> {!defaultExpanded ? ( diff --git a/packages/widget/src/components/Step/TransactionLink.style.tsx b/packages/widget/src/components/Step/TransactionLink.style.tsx index 9afb299a0..c4a8e32ad 100644 --- a/packages/widget/src/components/Step/TransactionLink.style.tsx +++ b/packages/widget/src/components/Step/TransactionLink.style.tsx @@ -18,10 +18,9 @@ export const StatusIconCircle = styled(Box, { width: 24, height: 24, borderRadius: '50%', - // TODO: how to resolve new colors? backgroundColor: failed ? `rgba(${theme.vars.palette.error.mainChannel} / 0.12)` - : '#D6FFE7', + : `rgba(${theme.vars.palette.success.mainChannel} / 0.12)`, marginRight: theme.spacing(1), })) diff --git a/packages/widget/src/components/StepActions/StepActions.tsx b/packages/widget/src/components/StepActions/StepActions.tsx index 07d143a8b..08af4b940 100644 --- a/packages/widget/src/components/StepActions/StepActions.tsx +++ b/packages/widget/src/components/StepActions/StepActions.tsx @@ -7,12 +7,13 @@ import type { StepIconProps } from '@mui/material' import { Box, Collapse, + Divider, Step as MuiStep, Stepper, Typography, } from '@mui/material' import type { MouseEventHandler } from 'react' -import { useState } from 'react' +import { Fragment, useState } from 'react' import { useTranslation } from 'react-i18next' import { useAvailableChains } from '../../hooks/useAvailableChains.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' @@ -42,7 +43,7 @@ export const StepActions: React.FC<{ const includedSteps = route.steps.flatMap((step) => step.includedSteps) return ( - + - + {cardExpanded ? ( ) : ( - + {includedSteps.map((includedStep, index) => ( - 0 ? 0 : -0.75, - width: 20, - height: 20, - mr: 0.5, - }} - > - {includedStep.toolDetails.name[0]} - + + {index > 0 ? ( + + ) : null} + + {includedStep.toolDetails.name[0]} + + ))} - + )} diff --git a/packages/widget/src/components/Timer/StepStatusTimer.style.tsx b/packages/widget/src/components/Timer/StepStatusTimer.style.tsx new file mode 100644 index 000000000..cfffe0d29 --- /dev/null +++ b/packages/widget/src/components/Timer/StepStatusTimer.style.tsx @@ -0,0 +1,50 @@ +import { + Box, + CircularProgress as MuiCircularProgress, + styled, + Typography, +} from '@mui/material' +import { iconCircleSize } from '../IconCircle/IconCircle.style.js' + +export const StatusCircle = styled(Box)({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: iconCircleSize, + height: iconCircleSize, + borderRadius: '50%', +}) + +export const RingContainer = styled(Box)({ + position: 'relative', + width: iconCircleSize, + height: iconCircleSize, +}) + +export const ProgressTrack = styled(MuiCircularProgress)(({ theme }) => ({ + position: 'absolute', + color: theme.vars.palette.divider, +})) + +export const TimerLabel = styled(Typography)({ + fontSize: 22, + fontWeight: 700, + fontVariantNumeric: 'tabular-nums', +}) + +export const IndeterminateRing: React.FC = () => ( + + + + +) diff --git a/packages/widget/src/components/Step/CircularProgressTimer.tsx b/packages/widget/src/components/Timer/StepStatusTimer.tsx similarity index 81% rename from packages/widget/src/components/Step/CircularProgressTimer.tsx rename to packages/widget/src/components/Timer/StepStatusTimer.tsx index 53247c0dd..a52e16013 100644 --- a/packages/widget/src/components/Step/CircularProgressTimer.tsx +++ b/packages/widget/src/components/Timer/StepStatusTimer.tsx @@ -4,13 +4,16 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useTimer } from '../../hooks/timer/useTimer.js' import { formatTimer } from '../../utils/timer.js' +import { iconCircleSize } from '../IconCircle/IconCircle.style.js' import { - circleSize, + IndeterminateRing, ProgressTrack, RingContainer, StatusCircle, TimerLabel, -} from './CircularProgress.style.js' +} from './StepStatusTimer.style.js' + +export { IndeterminateRing } const getExpiryTimestamp = (step: LiFiStepExtended) => { const execution = step?.execution @@ -22,23 +25,6 @@ const getExpiryTimestamp = (step: LiFiStepExtended) => { ) } -export const IndeterminateRing: React.FC = () => ( - - - - -) - export const TimerRing: React.FC<{ step: LiFiStepExtended }> = ({ step }) => { const { i18n } = useTranslation() const [isExpired, setExpired] = useState(false) @@ -68,13 +54,13 @@ export const TimerRing: React.FC<{ step: LiFiStepExtended }> = ({ step }) => { diff --git a/packages/widget/src/pages/TransactionPage/ExchangeRateBottomSheet.tsx b/packages/widget/src/pages/TransactionPage/ExchangeRateBottomSheet.tsx index 30e246af8..855766fd9 100644 --- a/packages/widget/src/pages/TransactionPage/ExchangeRateBottomSheet.tsx +++ b/packages/widget/src/pages/TransactionPage/ExchangeRateBottomSheet.tsx @@ -1,5 +1,4 @@ import type { ExchangeRateUpdateParams } from '@lifi/sdk' -import WarningRounded from '@mui/icons-material/WarningRounded' import { Box, Button, Typography } from '@mui/material' import type { RefObject } from 'react' import { @@ -12,9 +11,10 @@ import { import { useTranslation } from 'react-i18next' import { BottomSheet } from '../../components/BottomSheet/BottomSheet.js' import type { BottomSheetBase } from '../../components/BottomSheet/types.js' +import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { useSetContentHeight } from '../../hooks/useSetContentHeight.js' import { formatTokenAmount } from '../../utils/format.js' -import { CenterContainer, IconCircle } from './StatusBottomSheet.style.js' +import { CenterContainer } from './StatusBottomSheet.style.js' export interface ExchangeRateBottomSheetBase { isOpen(): void @@ -111,9 +111,7 @@ const ExchangeRateBottomSheetContent: React.FC< }} > - - - + { - switch (status) { - case RouteExecutionStatus.Done: - return { - // TODO: how to resolve new colors? - backgroundColor: '#D6FFE7', - color: theme.vars.palette.success.mainChannel, - alpha: 0.12, - lightDarken: 0, - darkDarken: 0, - } - case RouteExecutionStatus.Failed: - return { - color: theme.vars.palette.error.mainChannel, - alpha: 0.12, - lightDarken: 0, - darkDarken: 0, - } - case RouteExecutionStatus.Done | RouteExecutionStatus.Partial: - case RouteExecutionStatus.Done | RouteExecutionStatus.Refunded: - case 'warning': - return { - color: theme.vars.palette.warning.mainChannel, - alpha: 0.48, - lightDarken: 0.32, - darkDarken: 0, - } - default: - return { - color: theme.vars.palette.primary.mainChannel, - alpha: 0.12, - lightDarken: 0, - darkDarken: 0, - } - } -} export const CenterContainer = styled(Box)(() => ({ display: 'grid', placeItems: 'center', position: 'relative', })) - -export const IconCircle = styled(Box, { - shouldForwardProp: (prop) => prop !== 'status', -})<{ status: StatusColor }>(({ theme, status }) => { - const statusConfig = getStatusColor(status, theme) - - return { - backgroundColor: - 'backgroundColor' in statusConfig - ? statusConfig.backgroundColor - : `rgba(${statusConfig.color} / ${statusConfig.alpha})`, - borderRadius: '50%', - width: 90, - height: 90, - display: 'grid', - position: 'relative', - placeItems: 'center', - '& > svg': { - color: `color-mix(in srgb, rgb(${statusConfig.color}) ${(1 - statusConfig.lightDarken) * 100}%, black)`, - width: 48, - height: 48, - }, - ...theme.applyStyles('dark', { - '& > svg': { - color: `color-mix(in srgb, rgb(${statusConfig.color}) ${(1 - statusConfig.darkDarken) * 100}%, black)`, - width: 48, - height: 48, - }, - }), - } -}) diff --git a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx index 91420fe85..2278cc1dc 100644 --- a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx +++ b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx @@ -1,7 +1,3 @@ -import Done from '@mui/icons-material/Done' -import ErrorRounded from '@mui/icons-material/ErrorRounded' -import InfoRounded from '@mui/icons-material/InfoRounded' -import WarningRounded from '@mui/icons-material/WarningRounded' import { Box, Button, Typography } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import { useCallback, useEffect, useRef } from 'react' @@ -10,6 +6,8 @@ import { BottomSheet } from '../../components/BottomSheet/BottomSheet.js' import type { BottomSheetBase } from '../../components/BottomSheet/types.js' import { Card } from '../../components/Card/Card.js' import { CardTitle } from '../../components/Card/CardTitle.js' +import type { StatusColor } from '../../components/IconCircle/IconCircle.js' +import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { Token } from '../../components/Token/Token.js' import { WalletAddressBadge } from '../../components/WalletAddressBadge/WalletAddressBadge.js' import { useAvailableChains } from '../../hooks/useAvailableChains.js' @@ -25,7 +23,23 @@ import { hasEnumFlag } from '../../utils/enum.js' import { formatTokenAmount } from '../../utils/format.js' import { getErrorMessage } from '../../utils/getErrorMessage.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' -import { CenterContainer, IconCircle } from './StatusBottomSheet.style.js' +import { CenterContainer } from './StatusBottomSheet.style.js' + +const mapRouteStatus = (status: RouteExecutionStatus): StatusColor => { + if (hasEnumFlag(status, RouteExecutionStatus.Partial)) { + return 'warning' + } + if (hasEnumFlag(status, RouteExecutionStatus.Refunded)) { + return 'warning' + } + if (hasEnumFlag(status, RouteExecutionStatus.Failed)) { + return 'error' + } + if (status === RouteExecutionStatus.Done) { + return 'success' + } + return 'info' +} interface StatusBottomSheetContentProps extends RouteExecution { onClose(): void @@ -215,21 +229,7 @@ const StatusBottomSheetContent: React.FC = ({ > {!showContractComponent ? ( - - {status === RouteExecutionStatus.Idle ? ( - - ) : null} - {status === RouteExecutionStatus.Done ? ( - - ) : null} - {hasEnumFlag(status, RouteExecutionStatus.Partial) || - hasEnumFlag(status, RouteExecutionStatus.Refunded) ? ( - - ) : null} - {hasEnumFlag(status, RouteExecutionStatus.Failed) ? ( - - ) : null} - + ) : null} diff --git a/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx b/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx index 94ea473ed..552c2cbe1 100644 --- a/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx +++ b/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx @@ -1,5 +1,4 @@ import type { Route } from '@lifi/sdk' -import WarningRounded from '@mui/icons-material/WarningRounded' import { Button } from '@mui/material' import type { RefObject } from 'react' import { forwardRef, useRef } from 'react' @@ -7,9 +6,10 @@ import { useTranslation } from 'react-i18next' import { BottomSheet } from '../../components/BottomSheet/BottomSheet.js' import type { BottomSheetBase } from '../../components/BottomSheet/types.js' import { FeeBreakdownTooltip } from '../../components/FeeBreakdownTooltip.js' +import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { useSetContentHeight } from '../../hooks/useSetContentHeight.js' import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js' -import { CenterContainer, IconCircle } from './StatusBottomSheet.style.js' +import { CenterContainer } from './StatusBottomSheet.style.js' import { ButtonRow, ContentContainer, @@ -63,10 +63,7 @@ const TokenValueBottomSheetContent: React.FC = ({ return ( - {/* TODO: how to resolve colors? */} - - - + {t('warning.title.highValueLoss')} {t('warning.message.highValueLoss')} diff --git a/packages/widget/src/utils/getStatusColor.ts b/packages/widget/src/utils/getStatusColor.ts deleted file mode 100644 index af2582730..000000000 --- a/packages/widget/src/utils/getStatusColor.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { ExecutionActionStatus, Substatus } from '@lifi/sdk' -import type { Theme } from '@mui/material' - -/** - * Gets the background color for a status circle based on execution status - * @param theme - Material-UI theme - * @param status - Execution status - * @param substatus - Optional substatus for DONE status - * @returns RGBA color string - */ -export const getStatusColor = ( - theme: Theme, - status?: ExecutionActionStatus, - substatus?: Substatus -): string => { - switch (status) { - case 'ACTION_REQUIRED': - case 'MESSAGE_REQUIRED': - case 'RESET_REQUIRED': - return `rgba(${theme.vars.palette.info.mainChannel} / 0.12)` - case 'DONE': - if (substatus === 'PARTIAL' || substatus === 'REFUNDED') { - return `rgba(${theme.vars.palette.warning.mainChannel} / 0.12)` - } - // TODO: how to resolve new colors? - return '#D6FFE7' - // return `rgba(${theme.vars.palette.success.mainChannel} / 0.12)` - case 'FAILED': - return `rgba(${theme.vars.palette.error.mainChannel} / 0.12)` - default: - return `rgba(${theme.vars.palette.info.mainChannel} / 0.12)` - } -} From 08a127b3172a9483553c174408e39da6c5bf57cd Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Tue, 10 Mar 2026 15:34:06 +0000 Subject: [PATCH 08/22] refactor: transaction history --- .../IconCircle/IconCircle.style.tsx | 51 +++-- .../src/components/IconCircle/IconCircle.tsx | 15 +- .../src/components/Step/RouteTransactions.tsx | 4 +- .../components/Step/TransactionLink.style.tsx | 2 +- .../Timer/StepStatusTimer.style.tsx | 4 +- .../src/components/Timer/StepStatusTimer.tsx | 4 +- packages/widget/src/i18n/en.json | 2 + .../TransactionDetailsPage.tsx | 49 +---- .../ActiveTransactionCard.style.tsx | 28 +-- .../ActiveTransactionCard.tsx | 28 ++- .../TransactionPage/TransactionCompleted.tsx | 62 ++++++ .../TransactionPage/TransactionContent.tsx | 52 +++++ .../TransactionPage/TransactionFailed.tsx | 57 +++++ .../pages/TransactionPage/TransactionPage.tsx | 205 ++---------------- .../TransactionPage/TransactionPending.tsx | 23 ++ .../TransactionPage/TransactionReview.tsx | 164 ++++++++++++++ 16 files changed, 454 insertions(+), 296 deletions(-) create mode 100644 packages/widget/src/pages/TransactionPage/TransactionCompleted.tsx create mode 100644 packages/widget/src/pages/TransactionPage/TransactionContent.tsx create mode 100644 packages/widget/src/pages/TransactionPage/TransactionFailed.tsx create mode 100644 packages/widget/src/pages/TransactionPage/TransactionPending.tsx create mode 100644 packages/widget/src/pages/TransactionPage/TransactionReview.tsx diff --git a/packages/widget/src/components/IconCircle/IconCircle.style.tsx b/packages/widget/src/components/IconCircle/IconCircle.style.tsx index f2bfbd37f..be0bb3238 100644 --- a/packages/widget/src/components/IconCircle/IconCircle.style.tsx +++ b/packages/widget/src/components/IconCircle/IconCircle.style.tsx @@ -49,26 +49,33 @@ export const getStatusColor = ( } } +export const iconSizeRatio = 48 / 90 + export const IconCircleRoot = styled(Box, { - shouldForwardProp: (prop) => prop !== 'colorConfig', -})<{ colorConfig: StatusColorConfig }>(({ theme, colorConfig }) => ({ - backgroundColor: `rgba(${colorConfig.color} / ${colorConfig.alpha})`, - borderRadius: '50%', - width: iconCircleSize, - height: iconCircleSize, - display: 'grid', - position: 'relative', - placeItems: 'center', - '& > svg': { - color: `color-mix(in srgb, rgb(${colorConfig.color}) ${(1 - colorConfig.lightDarken) * 100}%, black)`, - width: 48, - height: 48, - }, - ...theme.applyStyles('dark', { - '& > svg': { - color: `color-mix(in srgb, rgb(${colorConfig.color}) ${(1 - colorConfig.darkDarken) * 100}%, black)`, - width: 48, - height: 48, - }, - }), -})) + shouldForwardProp: (prop) => prop !== 'colorConfig' && prop !== 'circleSize', +})<{ colorConfig: StatusColorConfig; circleSize: number }>( + ({ theme, colorConfig, circleSize }) => { + const svgSize = Math.round(circleSize * iconSizeRatio) + return { + backgroundColor: `rgba(${colorConfig.color} / ${colorConfig.alpha})`, + borderRadius: '50%', + width: circleSize, + height: circleSize, + display: 'grid', + position: 'relative', + placeItems: 'center', + '& > svg': { + color: `color-mix(in srgb, rgb(${colorConfig.color}) ${(1 - colorConfig.lightDarken) * 100}%, black)`, + width: svgSize, + height: svgSize, + }, + ...theme.applyStyles('dark', { + '& > svg': { + color: `color-mix(in srgb, rgb(${colorConfig.color}) ${(1 - colorConfig.darkDarken) * 100}%, black)`, + width: svgSize, + height: svgSize, + }, + }), + } + } +) diff --git a/packages/widget/src/components/IconCircle/IconCircle.tsx b/packages/widget/src/components/IconCircle/IconCircle.tsx index 0938fafcb..93d3091d9 100644 --- a/packages/widget/src/components/IconCircle/IconCircle.tsx +++ b/packages/widget/src/components/IconCircle/IconCircle.tsx @@ -1,20 +1,29 @@ import type { BoxProps } from '@mui/material' import { useTheme } from '@mui/material' import type { StatusColor } from './IconCircle.style.js' -import { getStatusColor, IconCircleRoot } from './IconCircle.style.js' +import { + getStatusColor, + IconCircleRoot, + iconCircleSize, +} from './IconCircle.style.js' import { statusIcons } from './statusIcons.js' interface IconCircleProps extends Omit { status: StatusColor + size?: number } -export const IconCircle: React.FC = ({ status, ...rest }) => { +export const IconCircle: React.FC = ({ + status, + size = iconCircleSize, + ...rest +}) => { const theme = useTheme() const colorConfig = getStatusColor(status, theme) const Icon = statusIcons[status] return ( - + ) diff --git a/packages/widget/src/components/Step/RouteTransactions.tsx b/packages/widget/src/components/Step/RouteTransactions.tsx index ac8328d39..949fc38cc 100644 --- a/packages/widget/src/components/Step/RouteTransactions.tsx +++ b/packages/widget/src/components/Step/RouteTransactions.tsx @@ -42,11 +42,11 @@ export const RouteTransactions: React.FC<{ : undefined return ( - + {route.steps.map((step) => ( {prepareActions(step.execution?.actions ?? []).map( (actionsGroup, index) => ( diff --git a/packages/widget/src/components/Step/TransactionLink.style.tsx b/packages/widget/src/components/Step/TransactionLink.style.tsx index c4a8e32ad..f5d04f201 100644 --- a/packages/widget/src/components/Step/TransactionLink.style.tsx +++ b/packages/widget/src/components/Step/TransactionLink.style.tsx @@ -27,7 +27,7 @@ export const StatusIconCircle = styled(Box, { export const TransactionLinkLabel = styled(Typography)(() => ({ flex: 1, fontSize: 12, - fontWeight: 400, + fontWeight: 500, color: 'inherit', })) diff --git a/packages/widget/src/components/Timer/StepStatusTimer.style.tsx b/packages/widget/src/components/Timer/StepStatusTimer.style.tsx index cfffe0d29..c139fcb60 100644 --- a/packages/widget/src/components/Timer/StepStatusTimer.style.tsx +++ b/packages/widget/src/components/Timer/StepStatusTimer.style.tsx @@ -38,12 +38,12 @@ export const IndeterminateRing: React.FC = () => ( variant="determinate" value={100} size={iconCircleSize} - thickness={3} + thickness={2} /> diff --git a/packages/widget/src/components/Timer/StepStatusTimer.tsx b/packages/widget/src/components/Timer/StepStatusTimer.tsx index a52e16013..eb4b017c9 100644 --- a/packages/widget/src/components/Timer/StepStatusTimer.tsx +++ b/packages/widget/src/components/Timer/StepStatusTimer.tsx @@ -55,13 +55,13 @@ export const TimerRing: React.FC<{ step: LiFiStepExtended }> = ({ step }) => { variant="determinate" value={100} size={iconCircleSize} - thickness={3} + thickness={2} /> diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json index 76261b90b..16df285fe 100644 --- a/packages/widget/src/i18n/en.json +++ b/packages/widget/src/i18n/en.json @@ -39,6 +39,7 @@ "max": "MAX", "ok": "Ok", "options": "Options", + "remove": "Remove", "removeTransaction": "Remove transaction", "reset": "Reset", "resetSettings": "Reset settings", @@ -49,6 +50,7 @@ "swap": "Swap", "swapReview": "Review swap", "system": "System", + "retry": "Retry", "tryAgain": "Try again", "viewCoverage": "View coverage", "viewOnExplorer": "View on explorer" diff --git a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx index 4364b38ff..3115c4cd3 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx @@ -1,13 +1,8 @@ import type { FullStatusData } from '@lifi/sdk' -import { Typography } from '@mui/material' import { useLocation, useNavigate } from '@tanstack/react-router' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { Card } from '../../components/Card/Card.js' import { PageContainer } from '../../components/PageContainer.js' -import { RouteCardEssentials } from '../../components/RouteCard/RouteCardEssentials.js' -import { RouteTokens } from '../../components/Step/RouteTokens.js' -import { RouteTransactions } from '../../components/Step/RouteTransactions.js' import { internalExplorerUrl } from '../../config/constants.js' import { useExplorer } from '../../hooks/useExplorer.js' import { useHeader } from '../../hooks/useHeader.js' @@ -18,11 +13,11 @@ import { useRouteExecutionStore } from '../../stores/routes/RouteExecutionStore. import { getSourceTxHash } from '../../stores/routes/utils.js' import { buildRouteFromTxHistory } from '../../utils/converters.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' +import { TransactionCompleted } from '../TransactionPage/TransactionCompleted.js' import { TransactionDetailsSkeleton } from './TransactionDetailsSkeleton.js' -import { TransferIdCard } from './TransferIdCard.js' export const TransactionDetailsPage: React.FC = () => { - const { t, i18n } = useTranslation() + const { t } = useTranslation() const navigate = useNavigate() const { subvariant, subvariantOptions, explorerUrls } = useWidgetConfig() const { search }: any = useLocation() @@ -103,40 +98,12 @@ export const TransactionDetailsPage: React.FC = () => { bottomGutters sx={{ display: 'flex', flexDirection: 'column', gap: 2 }} > - - - - {startedAt.toLocaleString(i18n.language, { - dateStyle: 'long', - })} - - - {startedAt.toLocaleString(i18n.language, { - timeStyle: 'short', - })} - - - - - - - - {t('main.receipts')} - - - - + ) } diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx index 294e5fb2a..ca8114ecd 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx @@ -7,35 +7,21 @@ export const CardContent = styled(Box)({ export const StatusBar = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', - gap: 12, - marginBottom: 12, - paddingLeft: 16, - paddingRight: 16, - paddingTop: 12, - paddingBottom: 12, - borderRadius: theme.vars.shape.borderRadiusSecondary, + gap: theme.spacing(1.5), + padding: theme.spacing(1), + marginBottom: theme.spacing(1.5), + borderRadius: theme.vars.shape.borderRadiusTertiary, backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, })) -export const ErrorIconCircle = styled(Box)({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: 32, - height: 32, - borderRadius: '50%', - backgroundColor: 'rgba(211, 47, 47, 0.12)', - flexShrink: 0, -}) - export const StatusTitle = styled(Typography)({ - fontSize: 14, - fontWeight: 600, + fontSize: 12, + fontWeight: 500, flex: 1, }) export const StatusMessage = styled(Typography)({ - fontSize: 14, + fontSize: 12, fontWeight: 500, flex: 1, }) diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx index 90f048799..151962759 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx @@ -1,10 +1,10 @@ -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline' -import ErrorRoundedIcon from '@mui/icons-material/ErrorRounded' +import DeleteIcon from '@mui/icons-material/Delete' import { Button, CircularProgress, IconButton } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import type { MouseEvent } from 'react' import { useTranslation } from 'react-i18next' import { Card } from '../../components/Card/Card.js' +import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' import { StepTimer } from '../../components/Timer/StepTimer.js' import { useActionMessage } from '../../hooks/useActionMessage.js' @@ -13,7 +13,6 @@ import { RouteExecutionStatus } from '../../stores/routes/types.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { CardContent, - ErrorIconCircle, StatusBar, StatusMessage, StatusTitle, @@ -62,12 +61,19 @@ export const ActiveTransactionCard: React.FC<{ {isFailed ? ( - - - + {t('error.title.transactionFailed')} - - + + ) : null} diff --git a/packages/widget/src/pages/TransactionPage/TransactionCompleted.tsx b/packages/widget/src/pages/TransactionPage/TransactionCompleted.tsx new file mode 100644 index 000000000..74e7e2ba9 --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/TransactionCompleted.tsx @@ -0,0 +1,62 @@ +import type { RouteExtended } from '@lifi/sdk' +import { Typography } from '@mui/material' +import { useTranslation } from 'react-i18next' +import { Card } from '../../components/Card/Card.js' +import { RouteCardEssentials } from '../../components/RouteCard/RouteCardEssentials.js' +import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { RouteTransactions } from '../../components/Step/RouteTransactions.js' +import { TransferIdCard } from '../TransactionDetailsPage/TransferIdCard.js' + +interface TransactionCompletedProps { + route: RouteExtended + startedAt: Date + transferId?: string + txLink?: string +} + +export const TransactionCompleted: React.FC = ({ + route, + startedAt, + transferId, + txLink, +}) => { + const { t, i18n } = useTranslation() + + return ( + <> + + + + {startedAt.toLocaleString(i18n.language, { + dateStyle: 'long', + })} + + + {startedAt.toLocaleString(i18n.language, { + timeStyle: 'short', + })} + + + + + + + + {t('main.receipts')} + + + + {transferId ? ( + + ) : null} + + ) +} diff --git a/packages/widget/src/pages/TransactionPage/TransactionContent.tsx b/packages/widget/src/pages/TransactionPage/TransactionContent.tsx new file mode 100644 index 000000000..aae36186b --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/TransactionContent.tsx @@ -0,0 +1,52 @@ +import type { RouteExtended } from '@lifi/sdk' +import { RouteExecutionStatus } from '../../stores/routes/types.js' +import { hasEnumFlag } from '../../utils/enum.js' +import { TransactionCompleted } from './TransactionCompleted.js' +import { TransactionFailed } from './TransactionFailed.js' +import { TransactionPending } from './TransactionPending.js' +import { TransactionReview } from './TransactionReview.js' + +interface TransactionContentProps { + route: RouteExtended + status: RouteExecutionStatus + executeRoute: () => void + restartRoute: () => void + deleteRoute: () => void + routeRefreshing: boolean +} + +export const TransactionContent: React.FC = ({ + route, + status, + executeRoute, + restartRoute, + deleteRoute, + routeRefreshing, +}) => { + if (hasEnumFlag(status, RouteExecutionStatus.Done)) { + const startedAt = new Date(route.steps[0].execution?.startedAt ?? 0) + return + } + + if (status === RouteExecutionStatus.Failed) { + return ( + + ) + } + + if (status === RouteExecutionStatus.Pending) { + return + } + + return ( + + ) +} diff --git a/packages/widget/src/pages/TransactionPage/TransactionFailed.tsx b/packages/widget/src/pages/TransactionPage/TransactionFailed.tsx new file mode 100644 index 000000000..0e52ad2c4 --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/TransactionFailed.tsx @@ -0,0 +1,57 @@ +import type { RouteExtended } from '@lifi/sdk' +import { Box, Button } from '@mui/material' +import { useTranslation } from 'react-i18next' +import { Card } from '../../components/Card/Card.js' +import { WarningMessages } from '../../components/Messages/WarningMessages.js' +import { ExecutionProgress } from '../../components/Step/ExecutionProgress.js' +import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { RouteTransactions } from '../../components/Step/RouteTransactions.js' +import { useNavigateBack } from '../../hooks/useNavigateBack.js' +import { StartTransactionButton } from './StartTransactionButton.js' + +interface TransactionFailedProps { + route: RouteExtended + restartRoute: () => void + deleteRoute: () => void +} + +export const TransactionFailed: React.FC = ({ + route, + restartRoute, + deleteRoute, +}) => { + const { t } = useTranslation() + const navigateBack = useNavigateBack() + + const handleRemoveRoute = () => { + navigateBack() + deleteRoute() + } + + return ( + <> + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx index 280c2c045..ee41c30bc 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx @@ -1,58 +1,30 @@ import type { ExchangeRateUpdateParams } from '@lifi/sdk' -import Delete from '@mui/icons-material/Delete' -import { Box, Button, Tooltip } from '@mui/material' -import { useLocation, useNavigate } from '@tanstack/react-router' +import { useLocation } from '@tanstack/react-router' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import type { BottomSheetBase } from '../../components/BottomSheet/types.js' -import { Card } from '../../components/Card/Card.js' -import { WarningMessages } from '../../components/Messages/WarningMessages.js' import { PageContainer } from '../../components/PageContainer.js' -import { RouteCardEssentials } from '../../components/RouteCard/RouteCardEssentials.js' -import { ExecutionProgress } from '../../components/Step/ExecutionProgress.js' -import { RouteTokens } from '../../components/Step/RouteTokens.js' -import { RouteTransactions } from '../../components/Step/RouteTransactions.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 { useHeaderStore } from '../../stores/header/useHeaderStore.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 { navigationRoutes } from '../../utils/navigationRoutes.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' +import { TransactionContent } from './TransactionContent.js' export const TransactionPage = () => { const { t } = useTranslation() - const { setFieldValue } = useFieldActions() const emitter = useWidgetEvents() - const setBackAction = useHeaderStore((state) => state.setBackAction) - const navigate = useNavigate() - const navigateBack = useNavigateBack() - const { subvariant, subvariantOptions, hiddenUI } = useWidgetConfig() + const { subvariant, subvariantOptions } = useWidgetConfig() const { search }: any = useLocation() const stateRouteId = search?.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, @@ -67,13 +39,6 @@ export const TransactionPage = () => { onAcceptExchangeRateUpdate, }) - const { - toAddress, - hasActivity, - isLoading: isLoadingAddressActivity, - isFetched: isActivityAddressFetched, - } = useAddressActivity(route?.toChainId) - const getHeaderTitle = () => { if (subvariant === 'custom') { return t(`header.${subvariantOptions?.custom ?? 'checkout'}`) @@ -110,169 +75,25 @@ export const TransactionPage = () => { } }, []) - if (!route) { + if (!route || !status) { 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', '') - } - // Once transaction is started, set the back action to navigate to the home page - setBackAction(() => { - navigate({ to: navigationRoutes.home, replace: true }) - }) - } - - 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' - ? 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 ( - {status !== RouteExecutionStatus.Idle ? ( - - - - - ) : null} - - - - - {status === RouteExecutionStatus.Idle || - status === RouteExecutionStatus.Failed ? ( - <> - - - - {status === RouteExecutionStatus.Failed ? ( - - - - ) : null} - - - ) : null} - {status ? : null} - {subvariant !== 'custom' ? ( - - ) : null} + + - {!hiddenUI?.includes(HiddenUI.LowAddressActivityConfirmation) ? ( - - ) : null} ) } diff --git a/packages/widget/src/pages/TransactionPage/TransactionPending.tsx b/packages/widget/src/pages/TransactionPage/TransactionPending.tsx new file mode 100644 index 000000000..656a69e18 --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/TransactionPending.tsx @@ -0,0 +1,23 @@ +import type { RouteExtended } from '@lifi/sdk' +import { Card } from '../../components/Card/Card.js' +import { ExecutionProgress } from '../../components/Step/ExecutionProgress.js' +import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { RouteTransactions } from '../../components/Step/RouteTransactions.js' + +interface TransactionPendingProps { + route: RouteExtended +} + +export const TransactionPending: React.FC = ({ + route, +}) => ( + <> + + + + + + + + +) diff --git a/packages/widget/src/pages/TransactionPage/TransactionReview.tsx b/packages/widget/src/pages/TransactionPage/TransactionReview.tsx new file mode 100644 index 000000000..2b2756403 --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/TransactionReview.tsx @@ -0,0 +1,164 @@ +import type { RouteExtended } from '@lifi/sdk' +import { Box } from '@mui/material' +import { useNavigate } from '@tanstack/react-router' +import { useRef } from 'react' +import { useTranslation } from 'react-i18next' +import type { BottomSheetBase } from '../../components/BottomSheet/types.js' +import { Card } from '../../components/Card/Card.js' +import { WarningMessages } from '../../components/Messages/WarningMessages.js' +import { RouteCardEssentials } from '../../components/RouteCard/RouteCardEssentials.js' +import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { useAddressActivity } from '../../hooks/useAddressActivity.js' +import { useWidgetEvents } from '../../hooks/useWidgetEvents.js' +import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { useFieldActions } from '../../stores/form/useFieldActions.js' +import { useHeaderStore } from '../../stores/header/useHeaderStore.js' +import { WidgetEvent } from '../../types/events.js' +import { HiddenUI } from '../../types/widget.js' +import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js' +import { navigationRoutes } from '../../utils/navigationRoutes.js' +import { ConfirmToAddressSheet } from './ConfirmToAddressSheet.js' +import { StartTransactionButton } from './StartTransactionButton.js' +import { TokenValueBottomSheet } from './TokenValueBottomSheet.js' +import { + calculateValueLossPercentage, + getTokenValueLossThreshold, +} from './utils.js' + +interface TransactionReviewProps { + route: RouteExtended + executeRoute: () => void + routeRefreshing: boolean +} + +export const TransactionReview: React.FC = ({ + route, + executeRoute, + routeRefreshing, +}) => { + const { t } = useTranslation() + const navigate = useNavigate() + const { setFieldValue } = useFieldActions() + const emitter = useWidgetEvents() + const setBackAction = useHeaderStore((state) => state.setBackAction) + const { subvariant, subvariantOptions, hiddenUI } = useWidgetConfig() + + const tokenValueBottomSheetRef = useRef(null) + const confirmToAddressSheetRef = useRef(null) + + const { + toAddress, + hasActivity, + isLoading: isLoadingAddressActivity, + isFetched: isActivityAddressFetched, + } = useAddressActivity(route.toChainId) + + 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', '') + } + setBackAction(() => { + navigate({ to: navigationRoutes.home, replace: true }) + }) + } + + const handleStartClick = () => { + 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() + } + } + + const getButtonText = (): string => { + 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}`) + } + } + } + + return ( + <> + + + + + + + + + + + {subvariant !== 'custom' ? ( + + ) : null} + {!hiddenUI?.includes(HiddenUI.LowAddressActivityConfirmation) ? ( + + ) : null} + + ) +} From 488ec8ada0907ba6df8749b9410ee04f07f40f2d Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Wed, 11 Mar 2026 09:54:49 +0000 Subject: [PATCH 09/22] refactor: component structure --- .../components/ActionRow/ActionRow.style.tsx | 27 ++++ .../src/components/ActionRow/ActionRow.tsx | 44 +++++++ .../components/AmountInput/AmountInput.tsx | 46 +------ .../AmountInput/AmountInputHeaderBadge.tsx | 49 +++++++ .../components/DateLabel/DateLabel.style.tsx | 11 ++ .../src/components/DateLabel/DateLabel.tsx | 21 +++ .../Header/TransactionHistoryButton.style.tsx | 28 +++- .../Header/TransactionHistoryButton.tsx | 33 +---- .../IconCircle/IconCircle.style.tsx | 1 - .../components/RouteCard/RouteCard.style.ts | 2 +- .../src/components/Step/RouteTokens.tsx | 13 +- .../src/components/Step/RouteTransactions.tsx | 88 ------------- .../src/components/Step/StepActionRow.tsx | 52 ++++++++ .../components/Step/TransactionLink.style.tsx | 46 ------- .../src/components/Step/TransactionLink.tsx | 76 ----------- .../components/StepActions/StepActions.tsx | 2 +- .../Timer/StepStatusTimer.style.tsx | 5 +- .../src/components/Token/PriceImpactLabel.tsx | 44 +++++++ .../widget/src/components/Token/Token.tsx | 121 ++---------------- .../src/components/Token/TokenStepLabel.tsx | 59 +++++++++ .../src/components/Token/TokenSymbolLabel.tsx | 17 +++ .../ActiveTransactionCard.style.tsx | 48 ++++--- .../ActiveTransactionCard.tsx | 90 ++++++------- .../TransactionHistoryItem.tsx | 28 +--- .../pages/TransactionPage/Receipts.style.tsx | 21 +++ .../src/pages/TransactionPage/Receipts.tsx | 82 ++++++++++++ .../TokenValueBottomSheet.style.tsx | 32 ++--- .../TransactionPage/TransactionCompleted.tsx | 42 ++---- .../TransactionPage/TransactionFailed.tsx | 20 ++- .../TransactionPage/TransactionPending.tsx | 20 ++- .../stores/routes/useExecutingRoutesIds.ts | 39 +++--- .../src/stores/routes/useHasFailedRoutes.ts | 16 --- .../routes/useRouteExecutionIndicators.ts | 33 +++++ 33 files changed, 669 insertions(+), 587 deletions(-) create mode 100644 packages/widget/src/components/ActionRow/ActionRow.style.tsx create mode 100644 packages/widget/src/components/ActionRow/ActionRow.tsx create mode 100644 packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx create mode 100644 packages/widget/src/components/DateLabel/DateLabel.style.tsx create mode 100644 packages/widget/src/components/DateLabel/DateLabel.tsx delete mode 100644 packages/widget/src/components/Step/RouteTransactions.tsx create mode 100644 packages/widget/src/components/Step/StepActionRow.tsx delete mode 100644 packages/widget/src/components/Step/TransactionLink.style.tsx delete mode 100644 packages/widget/src/components/Step/TransactionLink.tsx create mode 100644 packages/widget/src/components/Token/PriceImpactLabel.tsx create mode 100644 packages/widget/src/components/Token/TokenStepLabel.tsx create mode 100644 packages/widget/src/components/Token/TokenSymbolLabel.tsx create mode 100644 packages/widget/src/pages/TransactionPage/Receipts.style.tsx create mode 100644 packages/widget/src/pages/TransactionPage/Receipts.tsx delete mode 100644 packages/widget/src/stores/routes/useHasFailedRoutes.ts create mode 100644 packages/widget/src/stores/routes/useRouteExecutionIndicators.ts diff --git a/packages/widget/src/components/ActionRow/ActionRow.style.tsx b/packages/widget/src/components/ActionRow/ActionRow.style.tsx new file mode 100644 index 000000000..54c669222 --- /dev/null +++ b/packages/widget/src/components/ActionRow/ActionRow.style.tsx @@ -0,0 +1,27 @@ +import { Box, styled, Typography } from '@mui/material' + +export const ActionRowContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), + padding: theme.spacing(1), + borderRadius: theme.vars.shape.borderRadiusTertiary, + backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, +})) + +export const ActionIconCircle = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 24, + height: 24, + borderRadius: '50%', + backgroundColor: `rgba(${theme.vars.palette.success.mainChannel} / 0.12)`, +})) + +export const ActionRowLabel = styled(Typography)(({ theme }) => ({ + flex: 1, + fontSize: 12, + fontWeight: 500, + color: theme.vars.palette.text.primary, +})) diff --git a/packages/widget/src/components/ActionRow/ActionRow.tsx b/packages/widget/src/components/ActionRow/ActionRow.tsx new file mode 100644 index 000000000..98502377c --- /dev/null +++ b/packages/widget/src/components/ActionRow/ActionRow.tsx @@ -0,0 +1,44 @@ +import Wallet from '@mui/icons-material/Wallet' +import { CircularProgress } from '@mui/material' +import type { FC, ReactNode } from 'react' +import { IconCircle } from '../IconCircle/IconCircle.js' +import { + ActionIconCircle, + ActionRowContainer, + ActionRowLabel, +} from './ActionRow.style.js' + +export type ActionRowVariant = 'success' | 'error' | 'wallet' | 'pending' + +interface ActionRowProps { + variant: ActionRowVariant + message: string + endAdornment?: ReactNode +} + +const startIcons: Record = { + success: () => , + error: () => , + wallet: () => ( + + + + ), + pending: () => , +} + +export const ActionRow: FC = ({ + variant, + message, + endAdornment, +}) => { + const StartIcon = startIcons[variant] + + return ( + + + {message} + {endAdornment} + + ) +} diff --git a/packages/widget/src/components/AmountInput/AmountInput.tsx b/packages/widget/src/components/AmountInput/AmountInput.tsx index 86cef8ff4..1c8823e95 100644 --- a/packages/widget/src/components/AmountInput/AmountInput.tsx +++ b/packages/widget/src/components/AmountInput/AmountInput.tsx @@ -1,7 +1,5 @@ import type { Token } from '@lifi/sdk' -import { useAccount } from '@lifi/wallet-management' import type { CardProps } from '@mui/material' -import { useNavigate } from '@tanstack/react-router' import type { ChangeEvent, ReactNode } from 'react' import { useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -12,7 +10,7 @@ import { FormKeyHelper, type FormTypeProps } from '../../stores/form/types.js' import { useFieldActions } from '../../stores/form/useFieldActions.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' import { useInputModeStore } from '../../stores/inputMode/useInputModeStore.js' -import { DisabledUI, HiddenUI } from '../../types/widget.js' +import { DisabledUI } from '../../types/widget.js' import { formatInputAmount, formatTokenPrice, @@ -20,10 +18,8 @@ import { usdDecimals, } from '../../utils/format.js' import { fitInputText } from '../../utils/input.js' -import { navigationRoutes } from '../../utils/navigationRoutes.js' import { AvatarBadgedDefault } from '../Avatar/Avatar.js' import { TokenAvatar } from '../Avatar/TokenAvatar.js' -import { WalletAddressBadge } from '../WalletAddressBadge/WalletAddressBadge.js' import { AmountInputCard, AmountInputCardHeader, @@ -36,6 +32,7 @@ import { minInputFontSize, } from './AmountInput.style.js' import { AmountInputEndAdornment } from './AmountInputEndAdornment.js' +import { AmountInputHeaderBadge } from './AmountInputHeaderBadge.js' import { PriceFormHelperText } from './PriceFormHelperText.js' export const AmountInput: React.FC = ({ @@ -75,33 +72,19 @@ const AmountInputBase: React.FC< } > = ({ formType, token, startAdornment, endAdornment, disabled, ...props }) => { const { t } = useTranslation() - const navigate = useNavigate() - const { subvariant, subvariantOptions, toAddresses, disabledUI, hiddenUI } = - useWidgetConfig() + const { subvariant, subvariantOptions } = useWidgetConfig() const ref = useRef(null) const isEditingRef = useRef(false) const [formattedPriceInput, setFormattedPriceInput] = useState('') const amountKey = FormKeyHelper.getAmountKey(formType) - const [chainId, , toAddress] = useFieldValues( - FormKeyHelper.getChainKey(formType), - FormKeyHelper.getTokenKey(formType), - 'toAddress' - ) + const [chainId] = useFieldValues(FormKeyHelper.getChainKey(formType)) const [value] = useFieldValues(amountKey) const { setFieldValue } = useFieldActions() const { inputMode } = useInputModeStore() const { chain } = useChain(chainId) - const { account } = useAccount() - - const hiddenToAddress = hiddenUI?.includes(HiddenUI.ToAddress) - const disabledToAddress = disabledUI?.includes(DisabledUI.ToAddress) - const showWalletBadge = - !hiddenToAddress && - !(disabledToAddress && !toAddress) && - account.isConnected const currentInputMode = inputMode[formType] let displayValue: string @@ -190,26 +173,7 @@ const AmountInputBase: React.FC< {title} - {showWalletBadge ? ( - - navigate({ - to: toAddresses?.length - ? navigationRoutes.configuredWallets - : navigationRoutes.sendToWallet, - }) - } - /> - ) : null} + diff --git a/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx new file mode 100644 index 000000000..81b92183a --- /dev/null +++ b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx @@ -0,0 +1,49 @@ +import { useAccount } from '@lifi/wallet-management' +import { useNavigate } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' +import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { useFieldValues } from '../../stores/form/useFieldValues.js' +import { DisabledUI, HiddenUI } from '../../types/widget.js' +import { navigationRoutes } from '../../utils/navigationRoutes.js' +import { WalletAddressBadge } from '../WalletAddressBadge/WalletAddressBadge.js' + +export const AmountInputHeaderBadge: React.FC = () => { + const { t } = useTranslation() + const navigate = useNavigate() + const { subvariant, subvariantOptions, toAddresses, disabledUI, hiddenUI } = + useWidgetConfig() + const { account } = useAccount() + const [toAddress] = useFieldValues('toAddress') + + const hiddenToAddress = hiddenUI?.includes(HiddenUI.ToAddress) + const disabledToAddress = disabledUI?.includes(DisabledUI.ToAddress) + const showWalletBadge = + !hiddenToAddress && + !(disabledToAddress && !toAddress) && + account.isConnected + + if (!showWalletBadge) { + return null + } + + const handleClick = disabledToAddress + ? undefined + : () => + navigate({ + to: toAddresses?.length + ? navigationRoutes.configuredWallets + : navigationRoutes.sendToWallet, + }) + + return ( + + ) +} diff --git a/packages/widget/src/components/DateLabel/DateLabel.style.tsx b/packages/widget/src/components/DateLabel/DateLabel.style.tsx new file mode 100644 index 000000000..2047284f3 --- /dev/null +++ b/packages/widget/src/components/DateLabel/DateLabel.style.tsx @@ -0,0 +1,11 @@ +import { Box, styled, Typography } from '@mui/material' + +export const DateLabelContainer = styled(Box)({ + display: 'flex', + justifyContent: 'space-between', +}) + +export const DateLabelText = styled(Typography)({ + fontSize: 12, + fontWeight: 500, +}) diff --git a/packages/widget/src/components/DateLabel/DateLabel.tsx b/packages/widget/src/components/DateLabel/DateLabel.tsx new file mode 100644 index 000000000..506547b9e --- /dev/null +++ b/packages/widget/src/components/DateLabel/DateLabel.tsx @@ -0,0 +1,21 @@ +import { useTranslation } from 'react-i18next' +import { DateLabelContainer, DateLabelText } from './DateLabel.style.js' + +interface DateLabelProps { + date: Date +} + +export const DateLabel: React.FC = ({ date }) => { + const { i18n } = useTranslation() + + return ( + + + {date.toLocaleString(i18n.language, { dateStyle: 'long' })} + + + {date.toLocaleString(i18n.language, { timeStyle: 'short' })} + + + ) +} diff --git a/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx b/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx index 45e9e8eb2..dca73b121 100644 --- a/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx +++ b/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx @@ -1,17 +1,17 @@ -import { Badge, Box, styled } from '@mui/material' +import { Badge, Box, CircularProgress, styled } from '@mui/material' -export const ErrorBadge = styled(Badge)({ +export const ErrorBadge = styled(Badge)(({ theme }) => ({ '& .MuiBadge-badge': { padding: 0, minWidth: 'unset', width: 16, height: 16, borderRadius: '50%', - backgroundColor: 'white', - top: '0px', - left: '8px', + backgroundColor: theme.vars.palette.background.paper, + top: 0, + left: 8, }, -}) +})) export const ProgressContainer = styled(Box)({ position: 'relative', @@ -19,3 +19,19 @@ export const ProgressContainer = styled(Box)({ alignItems: 'center', justifyContent: 'center', }) + +export const ProgressTrack = styled(CircularProgress)(({ theme }) => ({ + position: 'absolute', + color: theme.vars.palette.grey[300], + ...theme.applyStyles('dark', { + color: theme.vars.palette.grey[800], + }), +})) + +export const ProgressFill = styled(CircularProgress)(({ theme }) => ({ + position: 'absolute', + color: theme.vars.palette.primary.main, + ...theme.applyStyles('dark', { + color: theme.vars.palette.primary.light, + }), +})) diff --git a/packages/widget/src/components/Header/TransactionHistoryButton.tsx b/packages/widget/src/components/Header/TransactionHistoryButton.tsx index 0813a5e3b..f6bc70154 100644 --- a/packages/widget/src/components/Header/TransactionHistoryButton.tsx +++ b/packages/widget/src/components/Header/TransactionHistoryButton.tsx @@ -1,38 +1,21 @@ import ErrorRounded from '@mui/icons-material/ErrorRounded' import ReceiptLong from '@mui/icons-material/ReceiptLong' -import { CircularProgress, IconButton, Tooltip } from '@mui/material' +import { IconButton, Tooltip } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' -import { useExecutingRoutesIds } from '../../stores/routes/useExecutingRoutesIds.js' -import { useHasFailedRoutes } from '../../stores/routes/useHasFailedRoutes.js' +import { useRouteExecutionIndicators } from '../../stores/routes/useRouteExecutionIndicators.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { ErrorBadge, ProgressContainer, + ProgressFill, + ProgressTrack, } from './TransactionHistoryButton.style.js' -const progressTrackSx = (theme: import('@mui/material').Theme) => ({ - position: 'absolute' as const, - color: theme.vars.palette.grey[300], - ...theme.applyStyles('dark', { - color: theme.vars.palette.grey[800], - }), -}) - -const progressFillSx = (theme: import('@mui/material').Theme) => ({ - position: 'absolute' as const, - color: theme.vars.palette.primary.main, - ...theme.applyStyles('dark', { - color: theme.vars.palette.primary.light, - }), -}) - export const TransactionHistoryButton = () => { const { t } = useTranslation() const navigate = useNavigate() - const hasFailedRoutes = useHasFailedRoutes() - const executingRouteIds = useExecutingRoutesIds() - const hasActiveRoutes = executingRouteIds.length > 0 + const { hasActiveRoutes, hasFailedRoutes } = useRouteExecutionIndicators() return ( @@ -51,19 +34,17 @@ export const TransactionHistoryButton = () => { {hasActiveRoutes ? ( <> - - ) : null} diff --git a/packages/widget/src/components/IconCircle/IconCircle.style.tsx b/packages/widget/src/components/IconCircle/IconCircle.style.tsx index be0bb3238..c13e2dd2d 100644 --- a/packages/widget/src/components/IconCircle/IconCircle.style.tsx +++ b/packages/widget/src/components/IconCircle/IconCircle.style.tsx @@ -39,7 +39,6 @@ export const getStatusColor = ( darkDarken: 0, } case 'info': - default: return { color: theme.vars.palette.info.mainChannel, alpha: 0.12, diff --git a/packages/widget/src/components/RouteCard/RouteCard.style.ts b/packages/widget/src/components/RouteCard/RouteCard.style.ts index d91ca16d2..862273717 100644 --- a/packages/widget/src/components/RouteCard/RouteCard.style.ts +++ b/packages/widget/src/components/RouteCard/RouteCard.style.ts @@ -3,5 +3,5 @@ import { Box, styled } from '@mui/material' export const TokenContainer = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', - gap: theme.spacing(3), + gap: theme.spacing(1), })) diff --git a/packages/widget/src/components/Step/RouteTokens.tsx b/packages/widget/src/components/Step/RouteTokens.tsx index 07c406dd4..5ff4e2546 100644 --- a/packages/widget/src/components/Step/RouteTokens.tsx +++ b/packages/widget/src/components/Step/RouteTokens.tsx @@ -26,30 +26,25 @@ export const RouteTokens: React.FC<{ : BigInt(route.steps[lastStepIndex].estimate.toAmount), } - const impactToken = { - ...route.steps[0].action.fromToken, - amount: BigInt(route.steps[0].action.fromAmount), - } - return ( {fromToken ? : null} - + {toToken ? ( ) : null} diff --git a/packages/widget/src/components/Step/RouteTransactions.tsx b/packages/widget/src/components/Step/RouteTransactions.tsx deleted file mode 100644 index 949fc38cc..000000000 --- a/packages/widget/src/components/Step/RouteTransactions.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import type { RouteExtended } from '@lifi/sdk' -import ContentCopyRounded from '@mui/icons-material/ContentCopyRounded' -import OpenInNew from '@mui/icons-material/OpenInNew' -import Wallet from '@mui/icons-material/Wallet' -import { Box, IconButton } from '@mui/material' -import type { MouseEvent } from 'react' -import { useTranslation } from 'react-i18next' -import { useExplorer } from '../../hooks/useExplorer.js' -import { prepareActions } from '../../utils/prepareActions.js' -import { shortenAddress } from '../../utils/wallet.js' -import { StepTransactionLink } from './TransactionLink.js' -import { - ExternalLinkIcon, - StatusIconCircle, - TransactionLinkContainer, - TransactionLinkLabel, -} from './TransactionLink.style.js' - -const isRouteCompleted = (route: RouteExtended) => { - const lastStep = route.steps.at(-1) - const lastAction = lastStep?.execution?.actions?.at(-1) - return lastAction?.status === 'DONE' -} - -export const RouteTransactions: React.FC<{ - route: RouteExtended -}> = ({ route }) => { - const { t } = useTranslation() - const { getAddressLink } = useExplorer() - const completed = isRouteCompleted(route) - const toAddress = route.toAddress - - const handleCopy = (e: MouseEvent) => { - e.stopPropagation() - if (toAddress) { - navigator.clipboard.writeText(toAddress) - } - } - - const addressLink = toAddress - ? getAddressLink(toAddress, route.toChainId) - : undefined - - return ( - - {route.steps.map((step) => ( - - {prepareActions(step.execution?.actions ?? []).map( - (actionsGroup, index) => ( - - ) - )} - - ))} - {completed && toAddress ? ( - - - - - - {t('main.sentToWallet', { - address: shortenAddress(toAddress), - })} - - - - - {addressLink ? ( - - - - ) : null} - - ) : null} - - ) -} diff --git a/packages/widget/src/components/Step/StepActionRow.tsx b/packages/widget/src/components/Step/StepActionRow.tsx new file mode 100644 index 000000000..e330276f5 --- /dev/null +++ b/packages/widget/src/components/Step/StepActionRow.tsx @@ -0,0 +1,52 @@ +import type { ExecutionAction, LiFiStepExtended } from '@lifi/sdk' +import OpenInNew from '@mui/icons-material/OpenInNew' +import type React from 'react' +import { useActionMessage } from '../../hooks/useActionMessage.js' +import { useExplorer } from '../../hooks/useExplorer.js' +import { ExternalLink } from '../../pages/TransactionPage/Receipts.style.js' +import { ActionRow } from '../ActionRow/ActionRow.js' + +export const StepActionRow: React.FC<{ + step: LiFiStepExtended + actionsGroup: ExecutionAction[] + receiptsOnly?: boolean +}> = ({ step, actionsGroup, receiptsOnly }) => { + const action = actionsGroup.at(-1) + const { title } = useActionMessage(step, action) + const { getTransactionLink } = useExplorer() + + const isDone = action?.status === 'DONE' + const isFailed = action?.status === 'FAILED' + + if (!isDone && !isFailed) { + return null + } + + const transactionLink = action.txHash + ? getTransactionLink({ txHash: action.txHash, chain: action.chainId }) + : action.txLink + ? getTransactionLink({ txLink: action.txLink, chain: action.chainId }) + : undefined + + if (receiptsOnly && (!isDone || !transactionLink)) { + return null + } + + return ( + + + + ) : undefined + } + /> + ) +} diff --git a/packages/widget/src/components/Step/TransactionLink.style.tsx b/packages/widget/src/components/Step/TransactionLink.style.tsx deleted file mode 100644 index f5d04f201..000000000 --- a/packages/widget/src/components/Step/TransactionLink.style.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Box, Link, styled, Typography } from '@mui/material' - -export const TransactionLinkContainer = styled(Box)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - padding: theme.spacing(1), - borderRadius: theme.spacing(4), - color: theme.vars.palette.text.primary, - backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, -})) - -export const StatusIconCircle = styled(Box, { - shouldForwardProp: (prop) => prop !== 'failed', -})<{ failed?: boolean }>(({ theme, failed }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: 24, - height: 24, - borderRadius: '50%', - backgroundColor: failed - ? `rgba(${theme.vars.palette.error.mainChannel} / 0.12)` - : `rgba(${theme.vars.palette.success.mainChannel} / 0.12)`, - marginRight: theme.spacing(1), -})) - -export const TransactionLinkLabel = styled(Typography)(() => ({ - flex: 1, - fontSize: 12, - fontWeight: 500, - color: 'inherit', -})) - -export const ExternalLinkIcon = styled(Link)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: 24, - height: 24, - borderRadius: '50%', - textDecoration: 'none', - color: 'inherit', - '&:hover': { - backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, - }, -})) diff --git a/packages/widget/src/components/Step/TransactionLink.tsx b/packages/widget/src/components/Step/TransactionLink.tsx deleted file mode 100644 index c143ac925..000000000 --- a/packages/widget/src/components/Step/TransactionLink.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import type { ExecutionAction, LiFiStepExtended } from '@lifi/sdk' -import Done from '@mui/icons-material/Done' -import ErrorRounded from '@mui/icons-material/ErrorRounded' -import OpenInNew from '@mui/icons-material/OpenInNew' -import type React from 'react' -import { useActionMessage } from '../../hooks/useActionMessage.js' -import { useExplorer } from '../../hooks/useExplorer.js' -import { - ExternalLinkIcon, - StatusIconCircle, - TransactionLinkContainer, - TransactionLinkLabel, -} from './TransactionLink.style.js' - -export interface TransactionLinkProps { - label: string - href?: string - failed?: boolean -} - -export const TransactionLink: React.FC = ({ - label, - href, - failed, -}) => { - return ( - - - {failed ? ( - - ) : ( - - )} - - {label} - {href ? ( - - - - ) : null} - - ) -} - -export const StepTransactionLink: React.FC<{ - step: LiFiStepExtended - actionsGroup: ExecutionAction[] -}> = ({ step, actionsGroup }) => { - const action = actionsGroup.at(-1) - const { title } = useActionMessage(step, action) - const { getTransactionLink } = useExplorer() - - if (!action || (action.status !== 'DONE' && action.status !== 'FAILED')) { - return null - } - - const transactionLink = action.txHash - ? getTransactionLink({ - txHash: action.txHash, - chain: action.chainId, - }) - : action.txLink - ? getTransactionLink({ - txLink: action.txLink, - chain: action.chainId, - }) - : undefined - - return ( - - ) -} diff --git a/packages/widget/src/components/StepActions/StepActions.tsx b/packages/widget/src/components/StepActions/StepActions.tsx index 08af4b940..c1bc8fa0f 100644 --- a/packages/widget/src/components/StepActions/StepActions.tsx +++ b/packages/widget/src/components/StepActions/StepActions.tsx @@ -63,7 +63,7 @@ export const StepActions: React.FC<{ {cardExpanded ? ( diff --git a/packages/widget/src/components/Timer/StepStatusTimer.style.tsx b/packages/widget/src/components/Timer/StepStatusTimer.style.tsx index c139fcb60..afe5d1bf2 100644 --- a/packages/widget/src/components/Timer/StepStatusTimer.style.tsx +++ b/packages/widget/src/components/Timer/StepStatusTimer.style.tsx @@ -23,7 +23,10 @@ export const RingContainer = styled(Box)({ export const ProgressTrack = styled(MuiCircularProgress)(({ theme }) => ({ position: 'absolute', - color: theme.vars.palette.divider, + color: theme.vars.palette.grey[300], + ...theme.applyStyles('dark', { + color: theme.vars.palette.grey[800], + }), })) export const TimerLabel = styled(Typography)({ diff --git a/packages/widget/src/components/Token/PriceImpactLabel.tsx b/packages/widget/src/components/Token/PriceImpactLabel.tsx new file mode 100644 index 000000000..a52afb4a0 --- /dev/null +++ b/packages/widget/src/components/Token/PriceImpactLabel.tsx @@ -0,0 +1,44 @@ +import type { TokenAmount } from '@lifi/sdk' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { getPriceImpact } from '../../utils/getPriceImpact.js' +import { TextSecondary } from './Token.style.js' + +interface PriceImpactLabelProps { + token: TokenAmount + impactToken: TokenAmount +} + +export const PriceImpactLabel: FC = ({ + token, + impactToken, +}) => { + const { t, i18n } = useTranslation() + + const priceImpact = getPriceImpact({ + fromToken: impactToken, + fromAmount: impactToken.amount, + toToken: token, + toAmount: token.amount, + }) + + const formatted = t('format.percent', { + value: priceImpact, + usePlusSign: true, + }) + + return ( + <> + + • + + + {formatted} + + + ) +} diff --git a/packages/widget/src/components/Token/Token.tsx b/packages/widget/src/components/Token/Token.tsx index 466e6b60f..c7eff430f 100644 --- a/packages/widget/src/components/Token/Token.tsx +++ b/packages/widget/src/components/Token/Token.tsx @@ -1,22 +1,22 @@ -import type { ExtendedChain, LiFiStep, TokenAmount } from '@lifi/sdk' +import type { LiFiStep, TokenAmount } from '@lifi/sdk' import type { BoxProps } from '@mui/material' -import { Box, Grow, Skeleton, Tooltip } from '@mui/material' +import { Box, Skeleton } from '@mui/material' import type { FC } from 'react' import { useTranslation } from 'react-i18next' import { useChain } from '../../hooks/useChain.js' import { useToken } from '../../hooks/useToken.js' import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js' -import { getPriceImpact } from '../../utils/getPriceImpact.js' import { AvatarBadgedSkeleton } from '../Avatar/Avatar.js' -import { SmallAvatar } from '../Avatar/SmallAvatar.js' import { TokenAvatar } from '../Avatar/TokenAvatar.js' import { TextFitter } from '../TextFitter/TextFitter.js' +import { PriceImpactLabel } from './PriceImpactLabel.js' import { TextSecondary, TextSecondaryContainer } from './Token.style.js' +import { TokenStepLabel } from './TokenStepLabel.js' +import { TokenSymbolLabel } from './TokenSymbolLabel.js' interface TokenProps { token: TokenAmount impactToken?: TokenAmount - enableImpactTokenTooltip?: boolean step?: LiFiStep stepVisible?: boolean disableDescription?: boolean @@ -52,14 +52,13 @@ const TokenFallback: FC = ({ const TokenBase: FC = ({ token, impactToken, - enableImpactTokenTooltip, step, stepVisible, disableDescription, isLoading, ...other }) => { - const { t, i18n } = useTranslation() + const { t } = useTranslation() const { chain } = useChain(token?.chainId) if (isLoading) { @@ -80,18 +79,6 @@ const TokenBase: FC = ({ token.decimals ) - let priceImpact: number | undefined - let priceImpactPercent: number | undefined - if (impactToken) { - priceImpact = getPriceImpact({ - fromToken: impactToken, - fromAmount: impactToken.amount, - toToken: token, - toAmount: token.amount, - }) - priceImpactPercent = priceImpact * 100 - } - return ( = ({ - {t('format.currency', { - value: tokenPrice, - })} + {t('format.currency', { value: tokenPrice })} - - • - - {token.symbol} - {impactToken ? ( - - • - - ) : null} + {impactToken ? ( - enableImpactTokenTooltip ? ( - - - {t('format.percent', { - value: priceImpact, - usePlusSign: true, - })} - - - ) : ( - - {t('format.percent', { value: priceImpact, usePlusSign: true })} - - ) + ) : null} {!disableDescription ? ( - - • - - ) : null} - {!disableDescription ? ( - + ) : null} @@ -184,58 +143,6 @@ const TokenBase: FC = ({ ) } -const IconLabel: FC<{ src?: string; name?: string }> = ({ src, name }) => ( - <> - - - {name?.[0]} - - - {name} - -) - -interface TokenStepProps extends Partial { - chain?: ExtendedChain -} - -const TokenStep: FC = ({ step, stepVisible, chain }) => { - return ( - - - - - - - - - - - - - ) -} - export const TokenSkeleton: FC & BoxProps> = ({ step, disableDescription, diff --git a/packages/widget/src/components/Token/TokenStepLabel.tsx b/packages/widget/src/components/Token/TokenStepLabel.tsx new file mode 100644 index 000000000..8ce5b4f22 --- /dev/null +++ b/packages/widget/src/components/Token/TokenStepLabel.tsx @@ -0,0 +1,59 @@ +import type { ExtendedChain, LiFiStep } from '@lifi/sdk' +import { Box, Grow } from '@mui/material' +import type { FC } from 'react' +import { SmallAvatar } from '../Avatar/SmallAvatar.js' +import { TextSecondary } from './Token.style.js' + +interface TokenStepLabelProps { + step?: LiFiStep + stepVisible?: boolean + chain?: ExtendedChain +} + +export const TokenStepLabel: FC = ({ + step, + stepVisible, + chain, +}) => { + const items = [ + { visible: !stepVisible, src: chain?.logoURI, name: chain?.name }, + { + visible: stepVisible, + src: step?.toolDetails.logoURI, + name: step?.toolDetails.name, + }, + ] + + return ( + <> + + • + + + {items.map(({ visible, src, name }, index) => ( + + + + {name?.[0]} + + {name} + + + ))} + + + ) +} diff --git a/packages/widget/src/components/Token/TokenSymbolLabel.tsx b/packages/widget/src/components/Token/TokenSymbolLabel.tsx new file mode 100644 index 000000000..24f8dc70b --- /dev/null +++ b/packages/widget/src/components/Token/TokenSymbolLabel.tsx @@ -0,0 +1,17 @@ +import type { FC } from 'react' +import { TextSecondary } from './Token.style.js' + +interface TokenSymbolLabelProps { + symbol: string +} + +export const TokenSymbolLabel: FC = ({ symbol }) => { + return ( + <> + + • + + {symbol} + + ) +} diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx index ca8114ecd..841eb2133 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx @@ -1,33 +1,31 @@ -import { Box, styled, Typography } from '@mui/material' +import { Box, Button, IconButton, styled, Typography } from '@mui/material' -export const CardContent = styled(Box)({ - padding: 24, -}) - -export const StatusBar = styled(Box)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - gap: theme.spacing(1.5), - padding: theme.spacing(1), - marginBottom: theme.spacing(1.5), - borderRadius: theme.vars.shape.borderRadiusTertiary, - backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, +export const CardContent = styled(Box)(({ theme }) => ({ + padding: theme.spacing(3), })) -export const StatusTitle = styled(Typography)({ - fontSize: 12, - fontWeight: 500, - flex: 1, -}) - -export const StatusMessage = styled(Typography)({ - fontSize: 12, - fontWeight: 500, - flex: 1, -}) - export const TimerText = styled(Typography)({ fontSize: 14, fontWeight: 700, fontVariantNumeric: 'tabular-nums', }) + +export const DeleteButton = styled(IconButton)(({ theme }) => ({ + padding: theme.spacing(0.5), + backgroundColor: theme.vars.palette.background.paper, + width: 24, + height: 24, +})) + +export const RetryButton = styled(Button)(({ theme }) => ({ + fontWeight: 700, + fontSize: 10, + height: 24, + minWidth: 'auto', + padding: theme.spacing(0.5, 1), + color: theme.vars.palette.text.primary, + backgroundColor: theme.vars.palette.background.paper, + '&:hover': { + backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, + }, +})) diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx index 151962759..04131340b 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx @@ -1,10 +1,10 @@ import DeleteIcon from '@mui/icons-material/Delete' -import { Button, CircularProgress, IconButton } from '@mui/material' +import { Box } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import type { MouseEvent } from 'react' import { useTranslation } from 'react-i18next' +import { ActionRow } from '../../components/ActionRow/ActionRow.js' import { Card } from '../../components/Card/Card.js' -import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' import { StepTimer } from '../../components/Timer/StepTimer.js' import { useActionMessage } from '../../hooks/useActionMessage.js' @@ -13,9 +13,8 @@ import { RouteExecutionStatus } from '../../stores/routes/types.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { CardContent, - StatusBar, - StatusMessage, - StatusTitle, + DeleteButton, + RetryButton, TimerText, } from './ActiveTransactionCard.style.js' @@ -59,52 +58,41 @@ export const ActiveTransactionCard: React.FC<{ return ( - {isFailed ? ( - - - {t('error.title.transactionFailed')} - - - - - - ) : null} - {!isFailed && title ? ( - - - {title} - {lastStep ? ( - - - - ) : null} - - ) : null} + + {isFailed ? ( + + + + + + {t('button.retry')} + + + } + /> + ) : undefined} + {!isFailed && title ? ( + + + + ) : undefined + } + /> + ) : undefined} + diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx index fcd684e6b..24d44606c 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx @@ -1,9 +1,9 @@ import type { FullStatusData, StatusResponse } from '@lifi/sdk' -import { Box, Typography } from '@mui/material' +import { Box } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' import { Card } from '../../components/Card/Card.js' +import { DateLabel } from '../../components/DateLabel/DateLabel.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' import { useTools } from '../../hooks/useTools.js' import { buildRouteFromTxHistory } from '../../utils/converters.js' @@ -12,7 +12,6 @@ import { navigationRoutes } from '../../utils/navigationRoutes.js' export const TransactionHistoryItem: React.FC<{ transaction: StatusResponse }> = ({ transaction }) => { - const { i18n } = useTranslation() const navigate = useNavigate() const { tools } = useTools() @@ -39,26 +38,9 @@ export const TransactionHistoryItem: React.FC<{ ) return ( - - - - - {startedAt.toLocaleString(i18n.language, { dateStyle: 'long' })} - - - {startedAt.toLocaleString(i18n.language, { timeStyle: 'short' })} - - + + + diff --git a/packages/widget/src/pages/TransactionPage/Receipts.style.tsx b/packages/widget/src/pages/TransactionPage/Receipts.style.tsx new file mode 100644 index 000000000..572658e82 --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/Receipts.style.tsx @@ -0,0 +1,21 @@ +import { Box, Link, styled } from '@mui/material' + +export const TransactionList = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1.5), +})) + +export const ExternalLink = styled(Link)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: 24, + height: 24, + borderRadius: '50%', + textDecoration: 'none', + color: theme.vars.palette.text.primary, + '&:hover': { + backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, + }, +})) diff --git a/packages/widget/src/pages/TransactionPage/Receipts.tsx b/packages/widget/src/pages/TransactionPage/Receipts.tsx new file mode 100644 index 000000000..43bf28864 --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/Receipts.tsx @@ -0,0 +1,82 @@ +import type { RouteExtended } from '@lifi/sdk' +import ContentCopyRounded from '@mui/icons-material/ContentCopyRounded' +import OpenInNew from '@mui/icons-material/OpenInNew' +import { IconButton, Typography } from '@mui/material' +import type { MouseEvent } from 'react' +import { useTranslation } from 'react-i18next' +import { ActionRow } from '../../components/ActionRow/ActionRow.js' +import { Card } from '../../components/Card/Card.js' +import { StepActionRow } from '../../components/Step/StepActionRow.js' +import { useExplorer } from '../../hooks/useExplorer.js' +import { prepareActions } from '../../utils/prepareActions.js' +import { shortenAddress } from '../../utils/wallet.js' +import { ExternalLink, TransactionList } from './Receipts.style.js' + +interface ReceiptsProps { + route: RouteExtended +} + +export const Receipts: React.FC = ({ route }) => { + const { t } = useTranslation() + const { getAddressLink } = useExplorer() + const toAddress = route.toAddress + + const handleCopy = (e: MouseEvent) => { + e.stopPropagation() + if (toAddress) { + navigator.clipboard.writeText(toAddress) + } + } + + const addressLink = toAddress + ? getAddressLink(toAddress, route.toChainId) + : undefined + + return ( + + + {t('main.receipts')} + + + {route.steps.map((step) => ( + + {prepareActions(step.execution?.actions ?? []).map( + (actionsGroup, index) => ( + + ) + )} + + ))} + {toAddress ? ( + + + + + {addressLink ? ( + + + + ) : undefined} + + } + /> + ) : undefined} + + + ) +} diff --git a/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.style.tsx b/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.style.tsx index c96642094..5d9db0b84 100644 --- a/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.style.tsx +++ b/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.style.tsx @@ -1,29 +1,29 @@ import { Box, styled, Typography } from '@mui/material' -export const ContentContainer = styled(Box)({ - padding: 24, -}) +export const ContentContainer = styled(Box)(({ theme }) => ({ + padding: theme.spacing(3), +})) -export const WarningTitle = styled(Typography)({ - paddingTop: 8, - paddingBottom: 8, +export const WarningTitle = styled(Typography)(({ theme }) => ({ + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), fontSize: 18, fontWeight: 700, -}) +})) export const WarningMessage = styled(Typography)(({ theme }) => ({ - paddingBottom: 16, + paddingBottom: theme.spacing(2), color: theme.vars.palette.text.secondary, fontSize: 14, })) -export const DetailRow = styled(Box)({ +export const DetailRow = styled(Box)(({ theme }) => ({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', - paddingTop: 6, - paddingBottom: 6, -}) + paddingTop: theme.spacing(0.75), + paddingBottom: theme.spacing(0.75), +})) export const DetailLabel = styled(Typography)(({ theme }) => ({ fontSize: 14, @@ -36,8 +36,8 @@ export const DetailValue = styled(Typography)({ fontWeight: 700, }) -export const ButtonRow = styled(Box)({ +export const ButtonRow = styled(Box)(({ theme }) => ({ display: 'flex', - marginTop: 24, - gap: 12, -}) + marginTop: theme.spacing(3), + gap: theme.spacing(1.5), +})) diff --git a/packages/widget/src/pages/TransactionPage/TransactionCompleted.tsx b/packages/widget/src/pages/TransactionPage/TransactionCompleted.tsx index 74e7e2ba9..84711fa0a 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionCompleted.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionCompleted.tsx @@ -1,11 +1,11 @@ import type { RouteExtended } from '@lifi/sdk' -import { Typography } from '@mui/material' -import { useTranslation } from 'react-i18next' +import { Box } from '@mui/material' import { Card } from '../../components/Card/Card.js' +import { DateLabel } from '../../components/DateLabel/DateLabel.js' import { RouteCardEssentials } from '../../components/RouteCard/RouteCardEssentials.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' -import { RouteTransactions } from '../../components/Step/RouteTransactions.js' import { TransferIdCard } from '../TransactionDetailsPage/TransferIdCard.js' +import { Receipts } from './Receipts.js' interface TransactionCompletedProps { route: RouteExtended @@ -20,40 +20,16 @@ export const TransactionCompleted: React.FC = ({ transferId, txLink, }) => { - const { t, i18n } = useTranslation() - return ( <> - - - {startedAt.toLocaleString(i18n.language, { - dateStyle: 'long', - })} - - - {startedAt.toLocaleString(i18n.language, { - timeStyle: 'short', - })} - - - - - - - - {t('main.receipts')} - - + + + + + + {transferId ? ( ) : null} diff --git a/packages/widget/src/pages/TransactionPage/TransactionFailed.tsx b/packages/widget/src/pages/TransactionPage/TransactionFailed.tsx index 0e52ad2c4..b52cf8cc4 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionFailed.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionFailed.tsx @@ -5,8 +5,10 @@ import { Card } from '../../components/Card/Card.js' import { WarningMessages } from '../../components/Messages/WarningMessages.js' import { ExecutionProgress } from '../../components/Step/ExecutionProgress.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' -import { RouteTransactions } from '../../components/Step/RouteTransactions.js' +import { StepActionRow } from '../../components/Step/StepActionRow.js' import { useNavigateBack } from '../../hooks/useNavigateBack.js' +import { prepareActions } from '../../utils/prepareActions.js' +import { TransactionList } from './Receipts.style.js' import { StartTransactionButton } from './StartTransactionButton.js' interface TransactionFailedProps { @@ -32,7 +34,21 @@ export const TransactionFailed: React.FC = ({ <> - + + {route.steps.map((step) => ( + + {prepareActions(step.execution?.actions ?? []).map( + (actionsGroup, index) => ( + + ) + )} + + ))} + diff --git a/packages/widget/src/pages/TransactionPage/TransactionPending.tsx b/packages/widget/src/pages/TransactionPage/TransactionPending.tsx index 656a69e18..2f6ba8aa6 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionPending.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionPending.tsx @@ -2,7 +2,9 @@ import type { RouteExtended } from '@lifi/sdk' import { Card } from '../../components/Card/Card.js' import { ExecutionProgress } from '../../components/Step/ExecutionProgress.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' -import { RouteTransactions } from '../../components/Step/RouteTransactions.js' +import { StepActionRow } from '../../components/Step/StepActionRow.js' +import { prepareActions } from '../../utils/prepareActions.js' +import { TransactionList } from './Receipts.style.js' interface TransactionPendingProps { route: RouteExtended @@ -14,7 +16,21 @@ export const TransactionPending: React.FC = ({ <> - + + {route.steps.map((step) => ( + + {prepareActions(step.execution?.actions ?? []).map( + (actionsGroup, index) => ( + + ) + )} + + ))} + diff --git a/packages/widget/src/stores/routes/useExecutingRoutesIds.ts b/packages/widget/src/stores/routes/useExecutingRoutesIds.ts index 7b08a423f..e63fa244e 100644 --- a/packages/widget/src/stores/routes/useExecutingRoutesIds.ts +++ b/packages/widget/src/stores/routes/useExecutingRoutesIds.ts @@ -1,24 +1,31 @@ import { useAccount } from '@lifi/wallet-management' +import { useCallback, useMemo } from 'react' import { useRouteExecutionStore } from './RouteExecutionStore.js' -import type { RouteExecution } from './types.js' +import type { RouteExecution, RouteExecutionState } from './types.js' import { RouteExecutionStatus } from './types.js' export const useExecutingRoutesIds = () => { const { accounts } = useAccount() - const accountAddresses = accounts.map((account) => account.address) - return useRouteExecutionStore((state) => - (Object.values(state.routes) as RouteExecution[]) - .filter( - (item) => - accountAddresses.includes(item.route.fromAddress) && - (item.status === RouteExecutionStatus.Pending || - item.status === RouteExecutionStatus.Failed) - ) - .sort( - (a, b) => - (b?.route.steps[0].execution?.startedAt ?? 0) - - (a?.route.steps[0].execution?.startedAt ?? 0) - ) - .map(({ route }) => route.id) + const accountAddresses = useMemo( + () => accounts.map((account) => account.address), + [accounts] ) + const selector = useCallback( + (state: RouteExecutionState) => + (Object.values(state.routes) as RouteExecution[]) + .filter( + (item) => + accountAddresses.includes(item.route.fromAddress) && + (item.status === RouteExecutionStatus.Pending || + item.status === RouteExecutionStatus.Failed) + ) + .sort( + (a, b) => + (b?.route.steps[0].execution?.startedAt ?? 0) - + (a?.route.steps[0].execution?.startedAt ?? 0) + ) + .map(({ route }) => route.id), + [accountAddresses] + ) + return useRouteExecutionStore(selector) } diff --git a/packages/widget/src/stores/routes/useHasFailedRoutes.ts b/packages/widget/src/stores/routes/useHasFailedRoutes.ts deleted file mode 100644 index 3cb6ddfba..000000000 --- a/packages/widget/src/stores/routes/useHasFailedRoutes.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useAccount } from '@lifi/wallet-management' -import { useRouteExecutionStore } from './RouteExecutionStore.js' -import type { RouteExecution } from './types.js' -import { RouteExecutionStatus } from './types.js' - -export const useHasFailedRoutes = () => { - const { accounts } = useAccount() - const accountAddresses = accounts.map((account) => account.address) - return useRouteExecutionStore((state) => - (Object.values(state.routes) as RouteExecution[]).some( - (item) => - accountAddresses.includes(item.route.fromAddress) && - item.status === RouteExecutionStatus.Failed - ) - ) -} diff --git a/packages/widget/src/stores/routes/useRouteExecutionIndicators.ts b/packages/widget/src/stores/routes/useRouteExecutionIndicators.ts new file mode 100644 index 000000000..47e7af394 --- /dev/null +++ b/packages/widget/src/stores/routes/useRouteExecutionIndicators.ts @@ -0,0 +1,33 @@ +import { useAccount } from '@lifi/wallet-management' +import { useCallback, useMemo } from 'react' +import { useRouteExecutionStore } from './RouteExecutionStore.js' +import type { RouteExecution, RouteExecutionState } from './types.js' +import { RouteExecutionStatus } from './types.js' + +export const useRouteExecutionIndicators = () => { + const { accounts } = useAccount() + const accountAddresses = useMemo( + () => accounts.map((account) => account.address), + [accounts] + ) + const selector = useCallback( + (state: RouteExecutionState) => { + const routes = Object.values(state.routes) as RouteExecution[] + const ownedRoutes = routes.filter((route) => + accountAddresses.includes(route.route.fromAddress) + ) + return { + hasActiveRoutes: ownedRoutes.some((r) => + [RouteExecutionStatus.Pending, RouteExecutionStatus.Failed].includes( + r.status + ) + ), + hasFailedRoutes: ownedRoutes.some( + (r) => r.status === RouteExecutionStatus.Failed + ), + } + }, + [accountAddresses] + ) + return useRouteExecutionStore(selector) +} From d6cb622f7d9493bda254098dae69469a2c2a65b5 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Wed, 11 Mar 2026 18:09:13 +0000 Subject: [PATCH 10/22] refactor: revert token selectors --- .../widget/src/components/Card/CardHeader.tsx | 26 +++++ .../src/components/FeeBreakdownTooltip.tsx | 1 - .../ReverseTokensButton.style.tsx | 11 ++- .../ReverseTokensButton.tsx | 11 +-- .../src/components/RouteCard/RouteCard.tsx | 3 - .../RouteCard/RouteCardEssentials.tsx | 2 +- .../src/components/SelectChainAndToken.tsx | 34 ++----- .../SelectTokenButton.style.tsx | 98 +++++++++---------- .../SelectTokenButton/SelectTokenButton.tsx | 78 +++++++-------- .../components/Step/RouteDetails.style.tsx | 1 - .../src/components/Timer/TimerContent.tsx | 3 +- packages/widget/src/i18n/en.json | 7 +- .../ActiveTransactionCard.style.tsx | 6 +- .../ActiveTransactionCard.tsx | 73 +++++++------- .../TransactionHistoryItem.tsx | 2 +- .../TransactionHistorySkeleton.tsx | 4 +- .../TransactionPage/TransactionFailed.tsx | 2 +- .../TransactionPage/TransactionPending.tsx | 2 +- .../TransactionPage/TransactionReview.tsx | 2 +- .../routes/createRouteExecutionStore.ts | 22 ----- packages/widget/src/stores/routes/types.ts | 1 - 21 files changed, 162 insertions(+), 227 deletions(-) create mode 100644 packages/widget/src/components/Card/CardHeader.tsx diff --git a/packages/widget/src/components/Card/CardHeader.tsx b/packages/widget/src/components/Card/CardHeader.tsx new file mode 100644 index 000000000..71a48fe46 --- /dev/null +++ b/packages/widget/src/components/Card/CardHeader.tsx @@ -0,0 +1,26 @@ +import { + cardHeaderClasses, + CardHeader as MuiCardHeader, + styled, +} from '@mui/material' + +export const CardHeader = styled(MuiCardHeader)(({ theme }) => ({ + [`.${cardHeaderClasses.action}`]: { + marginTop: -2, + alignSelf: 'center', + }, + [`.${cardHeaderClasses.title}`]: { + fontWeight: 600, + fontSize: 18, + lineHeight: 1.3334, + color: theme.vars.palette.text.primary, + textAlign: 'left', + }, + [`.${cardHeaderClasses.subheader}`]: { + fontWeight: 500, + fontSize: 12, + lineHeight: 1.3334, + color: theme.vars.palette.text.secondary, + textAlign: 'left', + }, +})) diff --git a/packages/widget/src/components/FeeBreakdownTooltip.tsx b/packages/widget/src/components/FeeBreakdownTooltip.tsx index 35181e6cd..652a2543b 100644 --- a/packages/widget/src/components/FeeBreakdownTooltip.tsx +++ b/packages/widget/src/components/FeeBreakdownTooltip.tsx @@ -42,7 +42,6 @@ export const FeeBreakdownTooltip: React.FC = ({ ) : null} } - sx={{ cursor: 'help' }} > {children} diff --git a/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.style.tsx b/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.style.tsx index 6868969a0..e6c86613b 100644 --- a/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.style.tsx +++ b/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.style.tsx @@ -2,8 +2,8 @@ import { Box, styled } from '@mui/material' import { Card } from '../Card/Card.js' export const IconCard = styled(Card)(({ theme }) => ({ - height: 40, - width: 40, + height: 32, + width: 32, fontSize: 16, display: 'flex', alignItems: 'center', @@ -17,12 +17,13 @@ export const ReverseContainer = styled(Box)(({ theme }) => { display: 'flex', justifyContent: 'center', alignItems: 'center', - margin: theme.spacing(-2.5), + margin: theme.spacing(-1), } }) -export const ReverseTokensButtonEmpty = styled(Box)({ +export const ReverseTokensButtonEmpty = styled(Box)(({ theme }) => ({ display: 'flex', justifyContent: 'center', alignItems: 'center', -}) + margin: theme.spacing(1), +})) diff --git a/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.tsx b/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.tsx index d2f776532..25a34f19c 100644 --- a/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.tsx +++ b/packages/widget/src/components/ReverseTokensButton/ReverseTokensButton.tsx @@ -1,4 +1,3 @@ -import ArrowDownward from '@mui/icons-material/ArrowDownward' import ArrowForward from '@mui/icons-material/ArrowForward' import { useAvailableChains } from '../../hooks/useAvailableChains.js' import { useToAddressAutoPopulate } from '../../hooks/useToAddressAutoPopulate.js' @@ -6,9 +5,7 @@ import { useToAddressReset } from '../../hooks/useToAddressReset.js' import { useFieldActions } from '../../stores/form/useFieldActions.js' import { IconCard, ReverseContainer } from './ReverseTokensButton.style.js' -export const ReverseTokensButton: React.FC<{ vertical?: boolean }> = ({ - vertical, -}) => { +export const ReverseTokensButton = () => { const { setFieldValue, getFieldValues } = useFieldActions() const { getChainById } = useAvailableChains() const { tryResetToAddress } = useToAddressReset() @@ -53,11 +50,7 @@ export const ReverseTokensButton: React.FC<{ vertical?: boolean }> = ({ return ( - {vertical ? ( - - ) : ( - - )} + ) diff --git a/packages/widget/src/components/RouteCard/RouteCard.tsx b/packages/widget/src/components/RouteCard/RouteCard.tsx index e65cf40de..7292f4780 100644 --- a/packages/widget/src/components/RouteCard/RouteCard.tsx +++ b/packages/widget/src/components/RouteCard/RouteCard.tsx @@ -123,9 +123,6 @@ export const RouteCard: React.FC< type={active ? 'selected' : 'default'} selectionColor="secondary" indented - sx={{ - padding: 3, - }} {...other} > {cardContent} diff --git a/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx b/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx index 89f88ff32..ef193841e 100644 --- a/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx +++ b/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx @@ -69,7 +69,7 @@ export const RouteCardEssentials: React.FC = ({ {showDuration && ( - + = (props) => { - const prefersNarrowView = useMediaQuery((theme: Theme) => - theme.breakpoints.down('sm') - ) const { disabledUI, hiddenUI, subvariant } = useWidgetConfig() const hiddenReverse = @@ -24,37 +21,22 @@ export const SelectChainAndToken: React.FC = (props) => { const hiddenToToken = subvariant === 'custom' || hiddenUI?.includes(HiddenUI.ToToken) - const isCompact = !prefersNarrowView && !hiddenToToken && !hiddenFromToken + const showReverseButton = !hiddenToToken && !hiddenFromToken return ( - + {!hiddenFromToken ? ( - + ) : null} - {!hiddenToToken && !hiddenFromToken ? ( + {showReverseButton ? ( !hiddenReverse ? ( - + ) : ( ) ) : null} {!hiddenToToken ? ( - + ) : null} ) diff --git a/packages/widget/src/components/SelectTokenButton/SelectTokenButton.style.tsx b/packages/widget/src/components/SelectTokenButton/SelectTokenButton.style.tsx index 95a05d0d1..703f7ed62 100644 --- a/packages/widget/src/components/SelectTokenButton/SelectTokenButton.style.tsx +++ b/packages/widget/src/components/SelectTokenButton/SelectTokenButton.style.tsx @@ -1,18 +1,51 @@ import { - Box, + cardHeaderClasses, CardContent as MuiCardContent, styled, - Typography, } from '@mui/material' import type { FormType } from '../../stores/form/types.js' import { Card } from '../Card/Card.js' +import { CardHeader } from '../Card/CardHeader.js' + +export const SelectTokenCardHeader = styled(CardHeader, { + shouldForwardProp: (prop) => !['selected'].includes(prop as string), +})<{ selected?: boolean }>(({ theme, selected }) => ({ + padding: theme.spacing(2), + [`.${cardHeaderClasses.title}`]: { + color: theme.vars.palette.text.secondary, + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + overflow: 'hidden', + width: 96, + fontSize: !selected ? 16 : 18, + fontWeight: 500, + [theme.breakpoints.down(theme.breakpoints.values.xs)]: { + fontSize: 16, + }, + }, + [`.${cardHeaderClasses.subheader}`]: { + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + overflow: 'hidden', + width: 96, + }, + variants: [ + { + props: ({ selected }) => selected, + style: { + [`.${cardHeaderClasses.title}`]: { + color: theme.vars.palette.text.primary, + fontWeight: 600, + }, + }, + }, + ], +})) export const SelectTokenCard = styled(Card)(({ theme }) => { const cardVariant = theme.components?.MuiCard?.defaultProps?.variant return { flex: 1, - display: 'flex', - flexDirection: 'column', ...(cardVariant !== 'outlined' && { background: 'none', '&:hover': { @@ -24,26 +57,21 @@ export const SelectTokenCard = styled(Card)(({ theme }) => { }) export const CardContent = styled(MuiCardContent, { - shouldForwardProp: (prop) => - !['formType', 'compact', 'mask'].includes(prop as string), -})<{ formType: FormType; compact: boolean; mask?: boolean }>( - ({ theme, formType, compact, mask = true }) => { + shouldForwardProp: (prop) => !['formType', 'mask'].includes(prop as string), +})<{ formType: FormType; mask?: boolean }>( + ({ theme, formType, mask = true }) => { const cardVariant = theme.components?.MuiCard?.defaultProps?.variant const direction = formType === 'to' ? '-8px' : 'calc(100% + 8px)' - const horizontal = compact ? direction : '50%' - const vertical = compact ? '50%' : direction + const horizontal = direction + const vertical = '50%' return { - display: 'flex', - flexDirection: 'column', - gap: theme.spacing(1.5), - padding: theme.spacing(3), - flex: 1, + padding: 0, transition: theme.transitions.create(['background-color'], { duration: theme.transitions.duration.enteringScreen, easing: theme.transitions.easing.easeOut, }), '&:last-child': { - paddingBottom: theme.spacing(3), + paddingBottom: 0, }, ...(cardVariant !== 'outlined' && { backgroundColor: theme.vars.palette.background.paper, @@ -63,41 +91,3 @@ export const CardContent = styled(MuiCardContent, { } } ) - -export const AvatarItemRow = styled(Box)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - gap: theme.spacing(2), -})) - -export const TokenLabelColumn = styled(Box)(() => ({ - display: 'flex', - flexDirection: 'column', - minWidth: 0, - flex: 1, - textAlign: 'left', -})) - -export const TokenNameText = styled(Typography, { - shouldForwardProp: (prop) => prop !== 'selected', -})<{ selected?: boolean }>(({ theme, selected }) => ({ - fontSize: 18, - fontWeight: selected ? 700 : 500, - lineHeight: 1.3333, - color: selected - ? theme.vars.palette.text.primary - : theme.vars.palette.text.secondary, - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', -})) - -export const ChainNameText = styled(Typography)(({ theme }) => ({ - fontSize: 14, - fontWeight: 500, - lineHeight: 1.2857, - color: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.48)`, - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - overflow: 'hidden', -})) diff --git a/packages/widget/src/components/SelectTokenButton/SelectTokenButton.tsx b/packages/widget/src/components/SelectTokenButton/SelectTokenButton.tsx index 7b6090d9f..87894d9c1 100644 --- a/packages/widget/src/components/SelectTokenButton/SelectTokenButton.tsx +++ b/packages/widget/src/components/SelectTokenButton/SelectTokenButton.tsx @@ -2,7 +2,6 @@ import { Skeleton } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { useChain } from '../../hooks/useChain.js' -import { useSwapOnly } from '../../hooks/useSwapOnly.js' import { useToken } from '../../hooks/useToken.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { useChainOrderStore } from '../../stores/chains/ChainOrderStore.js' @@ -10,30 +9,24 @@ import type { ChainOrderState } from '../../stores/chains/types.js' import type { FormTypeProps } from '../../stores/form/types.js' import { FormKeyHelper } from '../../stores/form/types.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' -import { HiddenUI } from '../../types/widget.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { AvatarBadgedDefault, AvatarBadgedSkeleton } from '../Avatar/Avatar.js' import { TokenAvatar } from '../Avatar/TokenAvatar.js' import { CardTitle } from '../Card/CardTitle.js' import { - AvatarItemRow, CardContent, - ChainNameText, SelectTokenCard, - TokenLabelColumn, - TokenNameText, + SelectTokenCardHeader, } from './SelectTokenButton.style.js' export const SelectTokenButton: React.FC< FormTypeProps & { - compact: boolean hiddenReverse?: boolean } -> = ({ formType, compact, hiddenReverse }) => { +> = ({ formType, hiddenReverse }) => { const { t } = useTranslation() const navigate = useNavigate() - const { disabledUI, subvariant, hiddenUI } = useWidgetConfig() - const swapOnly = useSwapOnly() + const { disabledUI, subvariant } = useWidgetConfig() const tokenKey = FormKeyHelper.getTokenKey(formType) const [chainId, tokenAddress] = useFieldValues( FormKeyHelper.getChainKey(formType), @@ -59,49 +52,44 @@ export const SelectTokenButton: React.FC< const isSelected = !!(chain && token) const onClick = !disabledUI?.includes(tokenKey) ? handleClick : undefined - const defaultPlaceholder = - formType === 'to' && subvariant === 'refuel' - ? t('main.selectChain') - : (formType === 'to' && swapOnly) || - hiddenUI?.includes(HiddenUI.ChainSelect) - ? t('main.selectToken') - : t('main.selectChainAndToken') + const defaultPlaceholder = `${t('main.select')}...` const cardTitle: string = formType === 'from' && subvariant === 'custom' ? t('header.payWith') : t(`main.${formType}`) - return ( - - {cardTitle} + + {cardTitle} {chainId && tokenAddress && (isChainLoading || isTokenLoading) ? ( - - - - - - - + } + title={} + subheader={} + /> ) : ( - - {isSelected ? ( - - ) : ( - - )} - - - {isSelected ? token.symbol : defaultPlaceholder} - - {isSelected ? ( - {chain.name} - ) : null} - - + + ) : ( + + ) + } + title={isSelected ? token.symbol : defaultPlaceholder} + slotProps={{ + title: { + title: isSelected ? token.symbol : defaultPlaceholder, + }, + subheader: { + title: isSelected ? chain.name : undefined, + }, + }} + subheader={isSelected ? chain.name : null} + selected={isSelected} + /> )} diff --git a/packages/widget/src/components/Step/RouteDetails.style.tsx b/packages/widget/src/components/Step/RouteDetails.style.tsx index a275b8b55..ba1c864d4 100644 --- a/packages/widget/src/components/Step/RouteDetails.style.tsx +++ b/packages/widget/src/components/Step/RouteDetails.style.tsx @@ -30,5 +30,4 @@ export const DetailValue = styled(Typography)(() => ({ export const DetailInfoIcon = { fontSize: 16, color: 'text.secondary', - cursor: 'help', } as const diff --git a/packages/widget/src/components/Timer/TimerContent.tsx b/packages/widget/src/components/Timer/TimerContent.tsx index 2c2eb5728..31f258c3b 100644 --- a/packages/widget/src/components/Timer/TimerContent.tsx +++ b/packages/widget/src/components/Timer/TimerContent.tsx @@ -7,7 +7,7 @@ import { IconTypography } from '../IconTypography.js' export const TimerContent: FC = ({ children }) => { const { t } = useTranslation() return ( - + = ({ children }) => { component="span" sx={{ fontVariantNumeric: 'tabular-nums', - cursor: 'help', }} > {children} diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json index 16df285fe..d2760e93e 100644 --- a/packages/widget/src/i18n/en.json +++ b/packages/widget/src/i18n/en.json @@ -56,7 +56,6 @@ "viewOnExplorer": "View on explorer" }, "header": { - "activeTransactions": "Active transactions", "amount": "Amount", "bookmarkedWallets": "Bookmarked wallets", "bridge": "Bridge", @@ -125,7 +124,6 @@ "message": { "accountNotDeployedMessage": "Smart contract account doesn't exist on the destination chain. Sending funds to a non-existent smart contract account can result in permanent loss.", "lowAddressActivity": "This address has low activity on {{chainName}} network. Please verify you're sending to the correct address and network to prevent potential loss of funds.", - "deleteActiveTransactions": "Active transactions are only stored locally and can't be recovered if you delete them.", "deleteTransactionHistory": "Transaction history is only stored locally and can't be recovered if you delete it.", "fundsLossPrevention": "Always ensure smart contract accounts are properly set up on the destination chain and avoid direct transfers to exchanges to prevent fund loss.", "highValueLoss": "The value of the received tokens is significantly lower than the exchanged tokens and transaction cost.", @@ -139,7 +137,6 @@ "slippageUnderRecommendedLimits": "Low slippage tolerance may cause transaction delays or failures." }, "title": { - "deleteActiveTransactions": "Delete all active transactions?", "deleteTransaction": "Delete this transaction?", "deleteTransactionHistory": "Delete transaction history?", "highValueLoss": "High value loss", @@ -290,9 +287,7 @@ "receiving": "Receiving", "refuelStepDetails": "Get gas via {{tool}}", "route": "Route", - "selectChain": "Select chain", - "selectChainAndToken": "Select chain and token", - "selectToken": "Select token", + "select": "Select", "sendToAddress": "Send to {{address}}", "sendToWallet": "Send to a different wallet", "sending": "Sending", diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx index 841eb2133..a09ff3305 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx @@ -1,8 +1,4 @@ -import { Box, Button, IconButton, styled, Typography } from '@mui/material' - -export const CardContent = styled(Box)(({ theme }) => ({ - padding: theme.spacing(3), -})) +import { Button, IconButton, styled, Typography } from '@mui/material' export const TimerText = styled(Typography)({ fontSize: 14, diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx index 04131340b..605e8e8eb 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx @@ -12,7 +12,6 @@ import { useRouteExecution } from '../../hooks/useRouteExecution.js' import { RouteExecutionStatus } from '../../stores/routes/types.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { - CardContent, DeleteButton, RetryButton, TimerText, @@ -56,45 +55,39 @@ export const ActiveTransactionCard: React.FC<{ } return ( - - - - {isFailed ? ( - - - - - - {t('button.retry')} - - - } - /> - ) : undefined} - {!isFailed && title ? ( - - - - ) : undefined - } - /> - ) : undefined} - - - + + + {isFailed ? ( + + + + + + {t('button.retry')} + + + } + /> + ) : undefined} + {!isFailed && title ? ( + + + + ) : undefined + } + /> + ) : undefined} + + ) } diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx index 24d44606c..a3e9f8490 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx @@ -38,7 +38,7 @@ export const TransactionHistoryItem: React.FC<{ ) return ( - + diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistorySkeleton.tsx b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistorySkeleton.tsx index 5c8174cbf..6a4e1572e 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistorySkeleton.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistorySkeleton.tsx @@ -5,8 +5,8 @@ import { TokenSkeleton } from '../../components/Token/Token.js' export const TransactionHistoryItemSkeleton = () => { return ( - - + + = ({ ))} - + diff --git a/packages/widget/src/pages/TransactionPage/TransactionPending.tsx b/packages/widget/src/pages/TransactionPage/TransactionPending.tsx index 2f6ba8aa6..cf4b02e5d 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionPending.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionPending.tsx @@ -32,7 +32,7 @@ export const TransactionPending: React.FC = ({ ))} - + diff --git a/packages/widget/src/pages/TransactionPage/TransactionReview.tsx b/packages/widget/src/pages/TransactionPage/TransactionReview.tsx index 2b2756403..f77f728a7 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionReview.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionReview.tsx @@ -129,7 +129,7 @@ export const TransactionReview: React.FC = ({ return ( <> - + diff --git a/packages/widget/src/stores/routes/createRouteExecutionStore.ts b/packages/widget/src/stores/routes/createRouteExecutionStore.ts index d58adcc96..01978020a 100644 --- a/packages/widget/src/stores/routes/createRouteExecutionStore.ts +++ b/packages/widget/src/stores/routes/createRouteExecutionStore.ts @@ -96,28 +96,6 @@ export const createRouteExecutionStore = ({ namePrefix }: PersistStoreProps) => }) } }, - deleteRoutes: (type) => - set((state: RouteExecutionState) => { - const routes = { ...state.routes } - Object.keys(routes) - .filter((routeId) => - type === 'completed' - ? hasEnumFlag( - routes[routeId]?.status ?? 0, - RouteExecutionStatus.Done - ) - : !hasEnumFlag( - routes[routeId]?.status ?? 0, - RouteExecutionStatus.Done - ) - ) - .forEach((routeId) => { - delete routes[routeId] - }) - return { - routes, - } - }), }), { name: `${namePrefix || 'li.fi'}-widget-routes`, diff --git a/packages/widget/src/stores/routes/types.ts b/packages/widget/src/stores/routes/types.ts index 5a935761d..353aa9e23 100644 --- a/packages/widget/src/stores/routes/types.ts +++ b/packages/widget/src/stores/routes/types.ts @@ -10,7 +10,6 @@ export interface RouteExecutionState { setExecutableRoute: (route: Route, observableRouteIds?: string[]) => void updateRoute: (route: Route) => void deleteRoute: (routeId: string) => void - deleteRoutes: (type: 'completed' | 'active') => void } export enum RouteExecutionStatus { From cebcd0d62543702e2c24acca1e1c849cc0c55b38 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Wed, 11 Mar 2026 20:50:49 +0000 Subject: [PATCH 11/22] refactor: partially revert amount styling --- .../AmountInput/AmountInput.style.tsx | 91 ++++------------- .../components/AmountInput/AmountInput.tsx | 98 +++++++++---------- .../AmountInputAdornment.style.tsx | 10 +- .../AmountInput/AmountInputEndAdornment.tsx | 29 ++++-- .../AmountInput/AmountInputHeaderBadge.tsx | 26 +++-- .../AmountInput/AmountInputStartAdornment.tsx | 26 +++++ .../AmountInput/PriceFormHelperText.tsx | 77 +++++++++++---- .../WalletAddressBadge.style.tsx | 10 -- .../WalletAddressBadge/WalletAddressBadge.tsx | 38 ------- .../TransactionPage/StatusBottomSheet.tsx | 4 - 10 files changed, 192 insertions(+), 217 deletions(-) create mode 100644 packages/widget/src/components/AmountInput/AmountInputStartAdornment.tsx delete mode 100644 packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.style.tsx delete mode 100644 packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx diff --git a/packages/widget/src/components/AmountInput/AmountInput.style.tsx b/packages/widget/src/components/AmountInput/AmountInput.style.tsx index abbae656a..f15ad05c6 100644 --- a/packages/widget/src/components/AmountInput/AmountInput.style.tsx +++ b/packages/widget/src/components/AmountInput/AmountInput.style.tsx @@ -4,33 +4,30 @@ import { inputBaseClasses, FormControl as MuiFormControl, styled, - Typography, } from '@mui/material' import { CardTitle } from '../Card/CardTitle.js' -import { InputCard } from '../Card/InputCard.js' -export const AmountInputCard = styled(InputCard)(({ theme }) => ({ +export const maxInputFontSize = 24 +export const minInputFontSize = 14 + +export const FormContainer = styled(Box)(({ theme }) => ({ display: 'flex', - flexDirection: 'column', - gap: theme.spacing(1.5), - padding: theme.spacing(3), + alignItems: 'center', + padding: theme.spacing(2), })) -export const maxInputFontSize = 40 -export const minInputFontSize = 14 - export const FormControl = styled(MuiFormControl)(() => ({ - flex: 1, + height: 40, })) -export const Input = styled(InputBase)(() => ({ - fontSize: 40, +export const Input = styled(InputBase)(({ theme }) => ({ + fontSize: 24, fontWeight: 700, boxShadow: 'none', - lineHeight: 1.4, + lineHeight: 1.5, [`.${inputBaseClasses.input}`]: { - padding: 0, - height: 'auto', + height: 24, + padding: theme.spacing(0, 0, 0, 2), }, '& input[type="number"]::-webkit-outer-spin-button, & input[type="number"]::-webkit-inner-spin-button': { @@ -48,68 +45,14 @@ export const Input = styled(InputBase)(() => ({ }, })) -export const AmountInputCardTitle = styled(CardTitle)(() => ({ - padding: 0, - fontSize: 14, - fontWeight: 700, - lineHeight: 1.4286, +export const AmountInputCardTitle = styled(CardTitle)(({ theme }) => ({ + padding: theme.spacing(0), })) -export const AmountInputCardHeader = styled(Box)(() => ({ +export const AmountInputCardHeader = styled(Box)(({ theme }) => ({ + padding: theme.spacing(2, 2, 0, 2), display: 'flex', justifyContent: 'space-between', alignItems: 'center', - width: '100%', -})) - -export const TokenAmountRow = styled(Box)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - gap: theme.spacing(2), - width: '100%', -})) - -export const LabelRow = styled(Box)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - gap: theme.spacing(2), - width: '100%', -})) - -export const LabelDescriptionColumn = styled(Box)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - flex: 1, - minWidth: 0, - gap: theme.spacing(0.5), -})) - -export const DescriptionRow = styled(Box)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - gap: theme.spacing(1), - width: '100%', -})) - -export const DescriptionText = styled(Typography)(({ theme }) => ({ - fontSize: 12, - fontWeight: 500, - lineHeight: 1.3333, - color: theme.vars.palette.text.secondary, - whiteSpace: 'nowrap', -})) - -export const BalanceText = styled(Typography)(({ theme }) => ({ - fontSize: 12, - fontWeight: 500, - lineHeight: 1.3333, - color: theme.vars.palette.text.primary, - whiteSpace: 'nowrap', -})) - -export const PercentageRow = styled(Box)(({ theme }) => ({ - display: 'flex', - gap: theme.spacing(1), - width: '100%', + height: 30, })) diff --git a/packages/widget/src/components/AmountInput/AmountInput.tsx b/packages/widget/src/components/AmountInput/AmountInput.tsx index 1c8823e95..1ae140dc3 100644 --- a/packages/widget/src/components/AmountInput/AmountInput.tsx +++ b/packages/widget/src/components/AmountInput/AmountInput.tsx @@ -3,7 +3,6 @@ import type { CardProps } from '@mui/material' import type { ChangeEvent, ReactNode } from 'react' import { useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useChain } from '../../hooks/useChain.js' import { useToken } from '../../hooks/useToken.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { FormKeyHelper, type FormTypeProps } from '../../stores/form/types.js' @@ -18,21 +17,19 @@ import { usdDecimals, } from '../../utils/format.js' import { fitInputText } from '../../utils/input.js' -import { AvatarBadgedDefault } from '../Avatar/Avatar.js' -import { TokenAvatar } from '../Avatar/TokenAvatar.js' +import { InputCard } from '../Card/InputCard.js' import { - AmountInputCard, AmountInputCardHeader, AmountInputCardTitle, + FormContainer, FormControl, Input, - LabelDescriptionColumn, - LabelRow, maxInputFontSize, minInputFontSize, } from './AmountInput.style.js' import { AmountInputEndAdornment } from './AmountInputEndAdornment.js' import { AmountInputHeaderBadge } from './AmountInputHeaderBadge.js' +import { AmountInputStartAdornment } from './AmountInputStartAdornment.js' import { PriceFormHelperText } from './PriceFormHelperText.js' export const AmountInput: React.FC = ({ @@ -53,9 +50,8 @@ export const AmountInput: React.FC = ({ : undefined - } + endAdornment={} + bottomAdornment={} disabled={disabled} {...props} /> @@ -68,9 +64,18 @@ const AmountInputBase: React.FC< token?: Token startAdornment?: ReactNode endAdornment?: ReactNode + bottomAdornment?: ReactNode disabled?: boolean } -> = ({ formType, token, startAdornment, endAdornment, disabled, ...props }) => { +> = ({ + formType, + token, + startAdornment, + endAdornment, + bottomAdornment, + disabled, + ...props +}) => { const { t } = useTranslation() const { subvariant, subvariantOptions } = useWidgetConfig() const ref = useRef(null) @@ -79,13 +84,10 @@ const AmountInputBase: React.FC< const [formattedPriceInput, setFormattedPriceInput] = useState('') const amountKey = FormKeyHelper.getAmountKey(formType) - const [chainId] = useFieldValues(FormKeyHelper.getChainKey(formType)) const [value] = useFieldValues(amountKey) const { setFieldValue } = useFieldActions() const { inputMode } = useInputModeStore() - const { chain } = useChain(chainId) - const currentInputMode = inputMode[formType] let displayValue: string if (isEditingRef.current) { @@ -167,47 +169,41 @@ const AmountInputBase: React.FC< : t('header.youPay') : t('header.send') - const isSelected = !!(chain && token) - return ( - + {title} - + {endAdornment} - - - {startAdornment ?? - (isSelected ? ( - - ) : ( - - ))} - - - - - - - {endAdornment} - + + + + + {bottomAdornment} + + + {!disabled ? : undefined} + ) } diff --git a/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx b/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx index f91766435..3cc03d95f 100644 --- a/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx @@ -4,17 +4,17 @@ import { ButtonTertiary } from '../ButtonTertiary.js' export const ButtonContainer = styled(Box)(({ theme }) => ({ display: 'flex', gap: theme.spacing(1), + padding: theme.spacing(0, 2, 2, 2), })) -export const PercentagePill = styled(ButtonTertiary)(({ theme }) => ({ - padding: theme.spacing(1, 0.75), - lineHeight: 1.3333, - fontSize: '0.75rem', +export const AmountInputButton = styled(ButtonTertiary)(({ theme }) => ({ + padding: theme.spacing(0.75, 1), + lineHeight: 1, + fontSize: 12, fontWeight: 700, minWidth: 'unset', height: 'auto', flex: 1, - borderRadius: 12, backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, '&:hover, &:active': { backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.08)`, diff --git a/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx b/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx index 4b3f25975..8cdc48881 100644 --- a/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx @@ -9,8 +9,8 @@ import { FormKeyHelper } from '../../stores/form/types.js' import { useFieldActions } from '../../stores/form/useFieldActions.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' import { + AmountInputButton, ButtonContainer, - PercentagePill, } from './AmountInputAdornment.style.js' export const AmountInputEndAdornment = memo(({ formType }: FormTypeProps) => { @@ -23,7 +23,10 @@ export const AmountInputEndAdornment = memo(({ formType }: FormTypeProps) => { FormKeyHelper.getTokenKey(formType) ) + // We get gas recommendations for the source chain to make sure that after pressing the Max button + // the user will have enough funds remaining to cover gas costs const { data } = useGasRecommendation(chainId) + const { token } = useTokenAddressBalance(chainId, tokenAddress) const getMaxAmount = () => { @@ -48,7 +51,9 @@ export const AmountInputEndAdornment = memo(({ formType }: FormTypeProps) => { setFieldValue( FormKeyHelper.getAmountKey(formType), formatUnits(percentageAmount, token.decimals), - { isTouched: true } + { + isTouched: true, + } ) } } @@ -59,7 +64,9 @@ export const AmountInputEndAdornment = memo(({ formType }: FormTypeProps) => { setFieldValue( FormKeyHelper.getAmountKey(formType), formatUnits(maxAmount, token.decimals), - { isTouched: true } + { + isTouched: true, + } ) } } @@ -70,10 +77,18 @@ export const AmountInputEndAdornment = memo(({ formType }: FormTypeProps) => { return ( - handlePercentage(25)}>25% - handlePercentage(50)}>50% - handlePercentage(75)}>75% - {t('button.max')} + handlePercentage(25)}> + 25% + + handlePercentage(50)}> + 50% + + handlePercentage(75)}> + 75% + + + {t('button.max')} + ) }) diff --git a/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx index 81b92183a..6f8b88ed1 100644 --- a/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx @@ -1,11 +1,14 @@ import { useAccount } from '@lifi/wallet-management' +import Wallet from '@mui/icons-material/Wallet' +import { Typography } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' import { DisabledUI, HiddenUI } from '../../types/widget.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' -import { WalletAddressBadge } from '../WalletAddressBadge/WalletAddressBadge.js' +import { shortenAddress } from '../../utils/wallet.js' +import { AmountInputButton } from './AmountInputAdornment.style.js' export const AmountInputHeaderBadge: React.FC = () => { const { t } = useTranslation() @@ -26,6 +29,11 @@ export const AmountInputHeaderBadge: React.FC = () => { return null } + const label = + subvariant === 'custom' && subvariantOptions?.custom === 'deposit' + ? t('header.depositTo') + : t('header.sendToWallet') + const handleClick = disabledToAddress ? undefined : () => @@ -36,14 +44,14 @@ export const AmountInputHeaderBadge: React.FC = () => { }) return ( - + sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flex: 'none' }} + > + + + {toAddress ? shortenAddress(toAddress) : label} + + ) } diff --git a/packages/widget/src/components/AmountInput/AmountInputStartAdornment.tsx b/packages/widget/src/components/AmountInput/AmountInputStartAdornment.tsx new file mode 100644 index 000000000..c06ba1632 --- /dev/null +++ b/packages/widget/src/components/AmountInput/AmountInputStartAdornment.tsx @@ -0,0 +1,26 @@ +import { useChain } from '../../hooks/useChain.js' +import { useToken } from '../../hooks/useToken.js' +import type { FormTypeProps } from '../../stores/form/types.js' +import { FormKeyHelper } from '../../stores/form/types.js' +import { useFieldValues } from '../../stores/form/useFieldValues.js' +import { AvatarBadgedDefault } from '../Avatar/Avatar.js' +import { TokenAvatar } from '../Avatar/TokenAvatar.js' + +export const AmountInputStartAdornment: React.FC = ({ + formType, +}) => { + const [chainId, tokenAddress] = useFieldValues( + FormKeyHelper.getChainKey(formType), + FormKeyHelper.getTokenKey(formType) + ) + + const { chain } = useChain(chainId) + const { token } = useToken(chainId, tokenAddress) + const isSelected = !!(chain && token) + + return isSelected ? ( + + ) : ( + + ) +} diff --git a/packages/widget/src/components/AmountInput/PriceFormHelperText.tsx b/packages/widget/src/components/AmountInput/PriceFormHelperText.tsx index 6db587711..8155418e8 100644 --- a/packages/widget/src/components/AmountInput/PriceFormHelperText.tsx +++ b/packages/widget/src/components/AmountInput/PriceFormHelperText.tsx @@ -1,6 +1,6 @@ import type { TokenAmount } from '@lifi/sdk' import SwapVertIcon from '@mui/icons-material/SwapVert' -import { Skeleton } from '@mui/material' +import { FormHelperText, Skeleton, Typography } from '@mui/material' import { memo } from 'react' import { useTranslation } from 'react-i18next' import { useTokenAddressBalance } from '../../hooks/useTokenAddressBalance.js' @@ -9,11 +9,6 @@ import { FormKeyHelper } from '../../stores/form/types.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' import { useInputModeStore } from '../../stores/inputMode/useInputModeStore.js' import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js' -import { - BalanceText, - DescriptionRow, - DescriptionText, -} from './AmountInput.style.js' import { InputPriceButton } from './PriceFormHelperText.style.js' export const PriceFormHelperText = memo(({ formType }) => { @@ -58,8 +53,9 @@ const PriceFormHelperTextBase: React.FC< token?.decimals ) return t('format.currency', { value: tokenPrice }) + } else { + return t('format.tokenAmount', { value: amount || '0' }) } - return t('format.tokenAmount', { value: amount || '0' }) } const handleToggleMode = (e: React.MouseEvent) => { @@ -68,27 +64,70 @@ const PriceFormHelperTextBase: React.FC< } return ( - + - {getPriceAmountDisplayValue()} + + {getPriceAmountDisplayValue()} + {currentInputMode === 'price' && token?.symbol ? ( - {token.symbol} - ) : null} - {token?.priceUSD ? ( - + + {token.symbol} + ) : null} + {token?.priceUSD && } {isLoading && tokenAddress ? ( ) : token?.amount ? ( - - {`/ ${t('format.tokenAmount', { value: tokenAmount })}`} - + + {`/ ${t('format.tokenAmount', { + value: tokenAmount, + })}`} + ) : null} - + ) } diff --git a/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.style.tsx b/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.style.tsx deleted file mode 100644 index 8d086deaa..000000000 --- a/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.style.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Box, styled } from '@mui/material' - -export const BadgeRoot = styled(Box)(({ theme }) => ({ - display: 'inline-flex', - alignItems: 'center', - gap: theme.spacing(0.5), - padding: theme.spacing(0.5, 0.75), - borderRadius: 12, - backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, -})) diff --git a/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx b/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx deleted file mode 100644 index 74eb7d3aa..000000000 --- a/packages/widget/src/components/WalletAddressBadge/WalletAddressBadge.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import Wallet from '@mui/icons-material/Wallet' -import { Typography } from '@mui/material' -import { shortenAddress } from '../../utils/wallet.js' -import { BadgeRoot } from './WalletAddressBadge.style.js' - -interface WalletAddressBadgeProps { - address?: string - label?: string - onClick?: React.MouseEventHandler -} - -export const WalletAddressBadge: React.FC = ({ - address, - label, - onClick, -}) => { - return ( - - - - {address ? shortenAddress(address) : label} - - - ) -} diff --git a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx index 2278cc1dc..efb6d5e1f 100644 --- a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx +++ b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx @@ -9,7 +9,6 @@ import { CardTitle } from '../../components/Card/CardTitle.js' import type { StatusColor } from '../../components/IconCircle/IconCircle.js' import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { Token } from '../../components/Token/Token.js' -import { WalletAddressBadge } from '../../components/WalletAddressBadge/WalletAddressBadge.js' import { useAvailableChains } from '../../hooks/useAvailableChains.js' import { useSetContentHeight } from '../../hooks/useSetContentHeight.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' @@ -283,9 +282,6 @@ const StatusBottomSheetContent: React.FC = ({ ? t('header.refunded') : t('header.received')} - {route.toAddress ? ( - - ) : null} {primaryMessage && ( From 7c77c153f28f4c4b6a1fe159444c6a11a1cdbdb1 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Thu, 12 Mar 2026 15:35:53 +0000 Subject: [PATCH 12/22] refactor: replace status bottom sheets --- .../AmountInput/AmountInput.style.tsx | 4 +- .../components/AmountInput/AmountInput.tsx | 9 +- .../AmountInputAdornment.style.tsx | 2 +- .../widget/src/components/Card/CardLabel.tsx | 6 +- .../components/Messages/GasRefuelMessage.tsx | 4 +- .../src/components/RouteCard/RouteCard.tsx | 4 +- ...outeCard.style.ts => RouteToken.style.tsx} | 0 .../RouteToken.tsx} | 12 +- .../widget/src/components/Routes/Routes.tsx | 5 +- .../src/components/SelectChainAndToken.tsx | 5 +- .../src/components/Step/ExecutionProgress.tsx | 7 +- .../components/Step/RouteDetails.style.tsx | 8 +- .../src/components/Step/RouteTokens.tsx | 4 +- .../src/components/Step/StepActionRow.tsx | 29 ++-- .../widget/src/components/Timer/StepTimer.tsx | 25 +-- .../src/components/Token/PriceImpactLabel.tsx | 44 ------ .../widget/src/components/Token/Token.tsx | 148 ++++++++++++++++-- .../src/components/Token/TokenStepLabel.tsx | 59 ------- .../src/components/Token/TokenSymbolLabel.tsx | 17 -- packages/widget/src/i18n/en.json | 1 - .../widget/src/pages/MainPage/MainPage.tsx | 24 +-- .../TransactionDetailsPage.tsx | 39 ++++- .../ActiveTransactionCard.style.tsx | 2 +- .../ActiveTransactionCard.tsx | 13 +- .../TransactionHistoryPage.tsx | 75 +++++++-- .../ExecutionProgressCards.tsx | 47 ++++++ .../src/pages/TransactionPage/Receipts.tsx | 1 - .../TransactionPage/StatusBottomSheet.tsx | 43 +---- .../TransactionPage/TransactionCompleted.tsx | 38 ----- .../TransactionPage/TransactionContent.tsx | 41 +++-- .../TransactionPage/TransactionFailed.tsx | 73 --------- .../TransactionFailedButtons.tsx | 40 +++++ .../pages/TransactionPage/TransactionPage.tsx | 11 +- .../TransactionPage/TransactionPending.tsx | 39 ----- .../routes/useRouteExecutionIndicators.ts | 11 +- 35 files changed, 428 insertions(+), 462 deletions(-) rename packages/widget/src/components/RouteCard/{RouteCard.style.ts => RouteToken.style.tsx} (100%) rename packages/widget/src/components/{Step/TokenWithExpansion.tsx => RouteCard/RouteToken.tsx} (86%) delete mode 100644 packages/widget/src/components/Token/PriceImpactLabel.tsx delete mode 100644 packages/widget/src/components/Token/TokenStepLabel.tsx delete mode 100644 packages/widget/src/components/Token/TokenSymbolLabel.tsx create mode 100644 packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx delete mode 100644 packages/widget/src/pages/TransactionPage/TransactionCompleted.tsx delete mode 100644 packages/widget/src/pages/TransactionPage/TransactionFailed.tsx create mode 100644 packages/widget/src/pages/TransactionPage/TransactionFailedButtons.tsx delete mode 100644 packages/widget/src/pages/TransactionPage/TransactionPending.tsx diff --git a/packages/widget/src/components/AmountInput/AmountInput.style.tsx b/packages/widget/src/components/AmountInput/AmountInput.style.tsx index f15ad05c6..14278a1ce 100644 --- a/packages/widget/src/components/AmountInput/AmountInput.style.tsx +++ b/packages/widget/src/components/AmountInput/AmountInput.style.tsx @@ -50,9 +50,9 @@ export const AmountInputCardTitle = styled(CardTitle)(({ theme }) => ({ })) export const AmountInputCardHeader = styled(Box)(({ theme }) => ({ - padding: theme.spacing(2, 2, 0, 2), + margin: theme.spacing(2, 2, 0, 2), display: 'flex', justifyContent: 'space-between', alignItems: 'center', - height: 30, + height: 28, })) diff --git a/packages/widget/src/components/AmountInput/AmountInput.tsx b/packages/widget/src/components/AmountInput/AmountInput.tsx index 1ae140dc3..b7155b125 100644 --- a/packages/widget/src/components/AmountInput/AmountInput.tsx +++ b/packages/widget/src/components/AmountInput/AmountInput.tsx @@ -32,10 +32,7 @@ import { AmountInputHeaderBadge } from './AmountInputHeaderBadge.js' import { AmountInputStartAdornment } from './AmountInputStartAdornment.js' import { PriceFormHelperText } from './PriceFormHelperText.js' -export const AmountInput: React.FC = ({ - formType, - ...props -}) => { +export const AmountInput: React.FC = ({ formType }) => { const { disabledUI } = useWidgetConfig() const [chainId, tokenAddress] = useFieldValues( @@ -53,7 +50,6 @@ export const AmountInput: React.FC = ({ endAdornment={} bottomAdornment={} disabled={disabled} - {...props} /> ) } @@ -74,7 +70,6 @@ const AmountInputBase: React.FC< endAdornment, bottomAdornment, disabled, - ...props }) => { const { t } = useTranslation() const { subvariant, subvariantOptions } = useWidgetConfig() @@ -170,7 +165,7 @@ const AmountInputBase: React.FC< : t('header.send') return ( - + {title} {endAdornment} diff --git a/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx b/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx index 3cc03d95f..b41bacb91 100644 --- a/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx @@ -8,7 +8,7 @@ export const ButtonContainer = styled(Box)(({ theme }) => ({ })) export const AmountInputButton = styled(ButtonTertiary)(({ theme }) => ({ - padding: theme.spacing(0.75, 1), + padding: theme.spacing(0.75, 1.5), lineHeight: 1, fontSize: 12, fontWeight: 700, diff --git a/packages/widget/src/components/Card/CardLabel.tsx b/packages/widget/src/components/Card/CardLabel.tsx index e48bf94dc..1e560f1a3 100644 --- a/packages/widget/src/components/Card/CardLabel.tsx +++ b/packages/widget/src/components/Card/CardLabel.tsx @@ -45,10 +45,10 @@ export const CardLabel = styled(Box, { export const CardLabelTypography = styled(Typography, { shouldForwardProp: (prop) => prop !== 'type', })<{ type?: 'icon' }>(({ theme }) => ({ - padding: theme.spacing(0.75, 1), - fontSize: 10, + padding: theme.spacing(0.75, 1.5), + fontSize: 12, lineHeight: 1, - fontWeight: '700', + fontWeight: 600, variants: [ { props: { diff --git a/packages/widget/src/components/Messages/GasRefuelMessage.tsx b/packages/widget/src/components/Messages/GasRefuelMessage.tsx index 406d8c885..67437b4e7 100644 --- a/packages/widget/src/components/Messages/GasRefuelMessage.tsx +++ b/packages/widget/src/components/Messages/GasRefuelMessage.tsx @@ -1,5 +1,4 @@ import EvStation from '@mui/icons-material/EvStation' -import type { BoxProps } from '@mui/material' import { Box, Collapse, Typography } from '@mui/material' import type { ChangeEvent } from 'react' import { useTranslation } from 'react-i18next' @@ -9,7 +8,7 @@ import { useSettingsActions } from '../../stores/settings/useSettingsActions.js' import { AlertMessage } from './AlertMessage.js' import { InfoMessageSwitch } from './GasRefuelMessage.style.js' -export const GasRefuelMessage: React.FC = (props) => { +export const GasRefuelMessage = () => { const { t } = useTranslation() const { setValue } = useSettingsActions() @@ -55,7 +54,6 @@ export const GasRefuelMessage: React.FC = (props) => { /> } - {...props} > ) : null} - { +}: RouteTokenProps) => { const { hiddenUI } = useWidgetConfig() const [cardExpanded, setCardExpanded] = useState(defaultExpanded) @@ -46,7 +46,7 @@ export const TokenWithExpansion = ({ /> {!defaultExpanded ? ( diff --git a/packages/widget/src/components/Routes/Routes.tsx b/packages/widget/src/components/Routes/Routes.tsx index 0434c4476..fc6937dfd 100644 --- a/packages/widget/src/components/Routes/Routes.tsx +++ b/packages/widget/src/components/Routes/Routes.tsx @@ -5,7 +5,6 @@ import { useRoutes } from '../../hooks/useRoutes.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { ButtonTertiary } from '../ButtonTertiary.js' -import type { CardProps } from '../Card/Card.js' import { Card } from '../Card/Card.js' import { CardTitle } from '../Card/CardTitle.js' import { ProgressToNextUpdate } from '../ProgressToNextUpdate.js' @@ -13,7 +12,7 @@ import { RouteCard } from '../RouteCard/RouteCard.js' import { RouteCardSkeleton } from '../RouteCard/RouteCardSkeleton.js' import { RouteNotFoundCard } from '../RouteCard/RouteNotFoundCard.js' -export const Routes: React.FC = (props) => { +export const Routes = () => { const { t } = useTranslation() const navigate = useNavigate() const { subvariant, subvariantOptions, useRecommendedRoute } = @@ -51,7 +50,7 @@ export const Routes: React.FC = (props) => { : t('header.receive') return ( - + {title} = (props) => { +export const SelectChainAndToken = () => { const { disabledUI, hiddenUI, subvariant } = useWidgetConfig() const hiddenReverse = @@ -24,7 +23,7 @@ export const SelectChainAndToken: React.FC = (props) => { const showReverseButton = !hiddenToToken && !hiddenFromToken return ( - + {!hiddenFromToken ? ( ) : null} diff --git a/packages/widget/src/components/Step/ExecutionProgress.tsx b/packages/widget/src/components/Step/ExecutionProgress.tsx index 071c1ef27..10f50ad29 100644 --- a/packages/widget/src/components/Step/ExecutionProgress.tsx +++ b/packages/widget/src/components/Step/ExecutionProgress.tsx @@ -1,11 +1,14 @@ import type { RouteExtended } from '@lifi/sdk' import { Box, Typography } from '@mui/material' import { useActionMessage } from '../../hooks/useActionMessage.js' +import { StatusBottomSheet } from '../../pages/TransactionPage/StatusBottomSheet.js' +import type { RouteExecutionStatus } from '../../stores/routes/types.js' import { StepStatusIndicator } from './StepStatusIndicator.js' export const ExecutionProgress: React.FC<{ route: RouteExtended -}> = ({ route }) => { + status: RouteExecutionStatus +}> = ({ route, status }) => { const lastStep = route.steps.at(-1) const lastAction = lastStep?.execution?.actions?.at(-1) const { title, message } = useActionMessage(lastStep, lastAction) @@ -42,6 +45,8 @@ export const ExecutionProgress: React.FC<{ {message} ) : null} + {/* TODO: Remove this once the logic is merged */} + ) } diff --git a/packages/widget/src/components/Step/RouteDetails.style.tsx b/packages/widget/src/components/Step/RouteDetails.style.tsx index ba1c864d4..e2c570d45 100644 --- a/packages/widget/src/components/Step/RouteDetails.style.tsx +++ b/packages/widget/src/components/Step/RouteDetails.style.tsx @@ -1,16 +1,16 @@ import { Box, styled, Typography } from '@mui/material' -export const DetailRow = styled(Box)(() => ({ +export const DetailRow = styled(Box)(({ theme }) => ({ display: 'flex', justifyContent: 'space-between', alignItems: 'center', - gap: 8, + gap: theme.spacing(1), })) -export const DetailLabelContainer = styled(Box)(() => ({ +export const DetailLabelContainer = styled(Box)(({ theme }) => ({ display: 'flex', alignItems: 'center', - gap: 4, + gap: theme.spacing(0.5), })) export const DetailLabel = styled(Typography)(({ theme }) => ({ diff --git a/packages/widget/src/components/Step/RouteTokens.tsx b/packages/widget/src/components/Step/RouteTokens.tsx index 5ff4e2546..ee2fb5d5a 100644 --- a/packages/widget/src/components/Step/RouteTokens.tsx +++ b/packages/widget/src/components/Step/RouteTokens.tsx @@ -2,8 +2,8 @@ import type { RouteExtended } from '@lifi/sdk' import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward' import { Box } from '@mui/material' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { RouteToken } from '../RouteCard/RouteToken.js' import { Token } from '../Token/Token.js' -import { TokenWithExpansion } from './TokenWithExpansion.js' export const RouteTokens: React.FC<{ route: RouteExtended @@ -41,7 +41,7 @@ export const RouteTokens: React.FC<{ {toToken ? ( - = ({ step, actionsGroup, receiptsOnly }) => { +}> = ({ step, actionsGroup }) => { const action = actionsGroup.at(-1) const { title } = useActionMessage(step, action) const { getTransactionLink } = useExplorer() @@ -18,17 +17,13 @@ export const StepActionRow: React.FC<{ const isDone = action?.status === 'DONE' const isFailed = action?.status === 'FAILED' - if (!isDone && !isFailed) { - return null - } - - const transactionLink = action.txHash + const transactionLink = action?.txHash ? getTransactionLink({ txHash: action.txHash, chain: action.chainId }) - : action.txLink + : action?.txLink ? getTransactionLink({ txLink: action.txLink, chain: action.chainId }) : undefined - if (receiptsOnly && (!isDone || !transactionLink)) { + if (!isDone && !isFailed && !transactionLink) { return null } @@ -37,15 +32,13 @@ export const StepActionRow: React.FC<{ variant={isFailed ? 'error' : 'success'} message={title ?? ''} endAdornment={ - transactionLink ? ( - - - - ) : undefined + + + } /> ) diff --git a/packages/widget/src/components/Timer/StepTimer.tsx b/packages/widget/src/components/Timer/StepTimer.tsx index 7bbdf9420..d48a807b0 100644 --- a/packages/widget/src/components/Timer/StepTimer.tsx +++ b/packages/widget/src/components/Timer/StepTimer.tsx @@ -18,8 +18,7 @@ const getExpiryTimestamp = (step: LiFiStepExtended) => { export const StepTimer: React.FC<{ step: LiFiStepExtended - hideInProgress?: boolean -}> = ({ step, hideInProgress }) => { +}> = ({ step }) => { if ( step.execution?.status === 'DONE' || step.execution?.status === 'FAILED' @@ -31,22 +30,11 @@ export const StepTimer: React.FC<{ return null } - return ( - - ) + return } -const ExecutionTimer = ({ - expiryTimestamp, - hideInProgress, -}: { - expiryTimestamp: Date - hideInProgress?: boolean -}) => { - const { t, i18n } = useTranslation() +const ExecutionTimer = ({ expiryTimestamp }: { expiryTimestamp: Date }) => { + const { i18n } = useTranslation() const [isExpired, setExpired] = useState(false) @@ -59,10 +47,7 @@ const ExecutionTimer = ({ const isTimerExpired = isExpired || (!minutes && !seconds) if (isTimerExpired) { - if (hideInProgress) { - return null - } - return t('main.inProgress') + return null } return ( diff --git a/packages/widget/src/components/Token/PriceImpactLabel.tsx b/packages/widget/src/components/Token/PriceImpactLabel.tsx deleted file mode 100644 index a52afb4a0..000000000 --- a/packages/widget/src/components/Token/PriceImpactLabel.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { TokenAmount } from '@lifi/sdk' -import type { FC } from 'react' -import { useTranslation } from 'react-i18next' -import { getPriceImpact } from '../../utils/getPriceImpact.js' -import { TextSecondary } from './Token.style.js' - -interface PriceImpactLabelProps { - token: TokenAmount - impactToken: TokenAmount -} - -export const PriceImpactLabel: FC = ({ - token, - impactToken, -}) => { - const { t, i18n } = useTranslation() - - const priceImpact = getPriceImpact({ - fromToken: impactToken, - fromAmount: impactToken.amount, - toToken: token, - toAmount: token.amount, - }) - - const formatted = t('format.percent', { - value: priceImpact, - usePlusSign: true, - }) - - return ( - <> - - • - - - {formatted} - - - ) -} diff --git a/packages/widget/src/components/Token/Token.tsx b/packages/widget/src/components/Token/Token.tsx index c7eff430f..579698270 100644 --- a/packages/widget/src/components/Token/Token.tsx +++ b/packages/widget/src/components/Token/Token.tsx @@ -1,22 +1,22 @@ import type { LiFiStep, TokenAmount } from '@lifi/sdk' import type { BoxProps } from '@mui/material' -import { Box, Skeleton } from '@mui/material' -import type { FC } from 'react' +import { Box, Grow, Skeleton, Tooltip } from '@mui/material' +import type { FC, PropsWithChildren, ReactElement } from 'react' import { useTranslation } from 'react-i18next' import { useChain } from '../../hooks/useChain.js' import { useToken } from '../../hooks/useToken.js' import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js' +import { getPriceImpact } from '../../utils/getPriceImpact.js' import { AvatarBadgedSkeleton } from '../Avatar/Avatar.js' +import { SmallAvatar } from '../Avatar/SmallAvatar.js' import { TokenAvatar } from '../Avatar/TokenAvatar.js' import { TextFitter } from '../TextFitter/TextFitter.js' -import { PriceImpactLabel } from './PriceImpactLabel.js' import { TextSecondary, TextSecondaryContainer } from './Token.style.js' -import { TokenStepLabel } from './TokenStepLabel.js' -import { TokenSymbolLabel } from './TokenSymbolLabel.js' interface TokenProps { token: TokenAmount impactToken?: TokenAmount + enableImpactTokenTooltip?: boolean step?: LiFiStep stepVisible?: boolean disableDescription?: boolean @@ -52,13 +52,14 @@ const TokenFallback: FC = ({ const TokenBase: FC = ({ token, impactToken, + enableImpactTokenTooltip, step, stepVisible, disableDescription, isLoading, ...other }) => { - const { t } = useTranslation() + const { t, i18n } = useTranslation() const { chain } = useChain(token?.chainId) if (isLoading) { @@ -79,6 +80,27 @@ const TokenBase: FC = ({ token.decimals ) + let priceImpact: number | undefined + let priceImpactPercent: number | undefined + if (impactToken) { + priceImpact = getPriceImpact({ + fromToken: impactToken, + fromAmount: impactToken.amount, + toToken: token, + toAmount: token.amount, + }) + priceImpactPercent = priceImpact * 100 + } + + const tokenOnChain = !disableDescription ? ( + + {t('main.tokenOnChain', { + tokenSymbol: token.symbol, + chainName: chain?.name, + })} + + ) : null + return ( = ({ - {t('format.currency', { value: tokenPrice })} + {t('format.currency', { + value: tokenPrice, + })} - {impactToken ? ( - + + • + + ) : null} + {impactToken ? ( + enableImpactTokenTooltip ? ( + + + {t('format.percent', { + value: priceImpact, + usePlusSign: true, + })} + + + ) : ( + + {t('format.percent', { value: priceImpact, usePlusSign: true })} + + ) ) : null} {!disableDescription ? ( - + • + + ) : null} + {!disableDescription && step ? ( + - ) : null} + disableDescription={disableDescription} + > + {tokenOnChain} + + ) : ( + tokenOnChain + )} ) } +const TokenStep: FC>> = ({ + step, + stepVisible, + disableDescription, + children, +}) => { + return ( + + + + {children as ReactElement} + + + + + + + {step?.toolDetails.name[0]} + + + {step?.toolDetails.name} + + + + ) +} + export const TokenSkeleton: FC & BoxProps> = ({ step, disableDescription, diff --git a/packages/widget/src/components/Token/TokenStepLabel.tsx b/packages/widget/src/components/Token/TokenStepLabel.tsx deleted file mode 100644 index 8ce5b4f22..000000000 --- a/packages/widget/src/components/Token/TokenStepLabel.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import type { ExtendedChain, LiFiStep } from '@lifi/sdk' -import { Box, Grow } from '@mui/material' -import type { FC } from 'react' -import { SmallAvatar } from '../Avatar/SmallAvatar.js' -import { TextSecondary } from './Token.style.js' - -interface TokenStepLabelProps { - step?: LiFiStep - stepVisible?: boolean - chain?: ExtendedChain -} - -export const TokenStepLabel: FC = ({ - step, - stepVisible, - chain, -}) => { - const items = [ - { visible: !stepVisible, src: chain?.logoURI, name: chain?.name }, - { - visible: stepVisible, - src: step?.toolDetails.logoURI, - name: step?.toolDetails.name, - }, - ] - - return ( - <> - - • - - - {items.map(({ visible, src, name }, index) => ( - - - - {name?.[0]} - - {name} - - - ))} - - - ) -} diff --git a/packages/widget/src/components/Token/TokenSymbolLabel.tsx b/packages/widget/src/components/Token/TokenSymbolLabel.tsx deleted file mode 100644 index 24f8dc70b..000000000 --- a/packages/widget/src/components/Token/TokenSymbolLabel.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { FC } from 'react' -import { TextSecondary } from './Token.style.js' - -interface TokenSymbolLabelProps { - symbol: string -} - -export const TokenSymbolLabel: FC = ({ symbol }) => { - return ( - <> - - • - - {symbol} - - ) -} diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json index d2760e93e..0bee03d23 100644 --- a/packages/widget/src/i18n/en.json +++ b/packages/widget/src/i18n/en.json @@ -230,7 +230,6 @@ "provider": "Provider fee" }, "from": "From", - "inProgress": "in progress", "maxSlippage": "Max. slippage", "minReceived": "Min. received", "myTokens": "My tokens", diff --git a/packages/widget/src/pages/MainPage/MainPage.tsx b/packages/widget/src/pages/MainPage/MainPage.tsx index edf8a4c8e..dae5537ae 100644 --- a/packages/widget/src/pages/MainPage/MainPage.tsx +++ b/packages/widget/src/pages/MainPage/MainPage.tsx @@ -40,20 +40,20 @@ export const MainPage: React.FC = () => { useHeader(title) - const marginSx = { marginBottom: 2 } - return ( - {custom ? ( - {contractComponent} - ) : null} - - {!custom || subvariantOptions?.custom === 'deposit' ? ( - - ) : null} - {!wideVariant ? : null} - {showGasRefuelMessage ? : null} - + + {custom ? ( + {contractComponent} + ) : null} + + {!custom || subvariantOptions?.custom === 'deposit' ? ( + + ) : null} + {!wideVariant ? : null} + {showGasRefuelMessage ? : null} + + { const { t } = useTranslation() const navigate = useNavigate() - const { subvariant, subvariantOptions, explorerUrls } = useWidgetConfig() + const { + subvariant, + subvariantOptions, + contractSecondaryComponent, + explorerUrls, + } = useWidgetConfig() const { search }: any = useLocation() const { tools } = useTools() const { getTransactionLink } = useExplorer() @@ -98,12 +110,23 @@ export const TransactionDetailsPage: React.FC = () => { bottomGutters sx={{ display: 'flex', flexDirection: 'column', gap: 2 }} > - + + + + + + + + + {subvariant === 'custom' && contractSecondaryComponent ? ( + {contractSecondaryComponent} + ) : null} + {supportId ? ( + + ) : null} ) } diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx index a09ff3305..077fda473 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx @@ -15,7 +15,7 @@ export const DeleteButton = styled(IconButton)(({ theme }) => ({ export const RetryButton = styled(Button)(({ theme }) => ({ fontWeight: 700, - fontSize: 10, + fontSize: 12, height: 24, minWidth: 'auto', padding: theme.spacing(0.5, 1), diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx index 605e8e8eb..da27aef1f 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx @@ -1,4 +1,4 @@ -import DeleteIcon from '@mui/icons-material/Delete' +import DeleteOutline from '@mui/icons-material/DeleteOutline' import { Box } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import type { MouseEvent } from 'react' @@ -49,8 +49,9 @@ export const ActiveTransactionCard: React.FC<{ deleteRoute() } - const handleRetry = (e: MouseEvent) => { - e.stopPropagation() + const handleRetry = () => { + // NB: Do not stop propagation here: + // open the transaction execution page and retry the transaction simultaneously restartRoute() } @@ -63,12 +64,12 @@ export const ActiveTransactionCard: React.FC<{ message={t('error.title.transactionFailed')} endAdornment={ <> - - - {t('button.retry')} + + + } /> diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx index e6d700dc3..50efcde44 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx @@ -1,12 +1,18 @@ -import type { FullStatusData } from '@lifi/sdk' +import type { + ExtendedTransactionInfo, + FullStatusData, + StatusResponse, +} from '@lifi/sdk' import { Box, List } from '@mui/material' import { useVirtualizer } from '@tanstack/react-virtual' -import { useCallback, useRef } from 'react' +import { useCallback, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { PageContainer } from '../../components/PageContainer.js' import { useHeader } from '../../hooks/useHeader.js' import { useListHeight } from '../../hooks/useListHeight.js' import { useTransactionHistory } from '../../hooks/useTransactionHistory.js' +import { useRouteExecutionStore } from '../../stores/routes/RouteExecutionStore.js' +import type { RouteExecutionState } from '../../stores/routes/types.js' import { useExecutingRoutesIds } from '../../stores/routes/useExecutingRoutesIds.js' import { ActiveTransactionCard } from './ActiveTransactionCard.js' import { minTransactionListHeight } from './constants.js' @@ -14,11 +20,28 @@ import { TransactionHistoryEmpty } from './TransactionHistoryEmpty.js' import { TransactionHistoryItem } from './TransactionHistoryItem.js' import { TransactionHistoryItemSkeleton } from './TransactionHistorySkeleton.js' +type ActiveItem = { type: 'active'; routeId: string; startedAt: number } +type HistoryItem = { + type: 'history' + transaction: StatusResponse + startedAt: number +} +type TransactionListItem = ActiveItem | HistoryItem + +const routeStartedAtSelector = + (routeIds: string[]) => (state: RouteExecutionState) => + Object.fromEntries( + routeIds.map((id) => [ + id, + state.routes[id]?.route.steps[0]?.execution?.startedAt ?? 0, + ]) + ) + export const TransactionHistoryPage = () => { // Parent ref and useVirtualizer should be in one file to avoid blank page (0 virtual items) issue const parentRef = useRef(null) const { data: transactions, isLoading } = useTransactionHistory() - const executingRoutes = useExecutingRoutesIds() + const executingRouteIds = useExecutingRoutesIds() const { t } = useTranslation() useHeader(t('header.transactionHistory')) @@ -27,15 +50,40 @@ export const TransactionHistoryPage = () => { listParentRef: parentRef, }) + const startedAtByRouteId = useRouteExecutionStore( + routeStartedAtSelector(executingRouteIds) + ) + + const allItems = useMemo(() => { + const activeItems: ActiveItem[] = executingRouteIds.map((routeId) => ({ + type: 'active', + routeId, + startedAt: startedAtByRouteId[routeId] ?? 0, + })) + const historyItems: HistoryItem[] = transactions.map((transaction) => ({ + type: 'history', + transaction, + startedAt: + ((transaction as FullStatusData).sending as ExtendedTransactionInfo) + ?.timestamp ?? 0, + })) + return [...activeItems, ...historyItems].sort( + (a, b) => b.startedAt - a.startedAt + ) + }, [executingRouteIds, startedAtByRouteId, transactions]) + const getItemKey = useCallback( (index: number) => { - return `${(transactions[index] as FullStatusData).transactionId}-${index}` + const item = allItems[index] + return item.type === 'active' + ? `active-${item.routeId}` + : `history-${(item.transaction as FullStatusData).transactionId}-${index}` }, - [transactions] + [allItems] ) const { getVirtualItems, getTotalSize, measureElement } = useVirtualizer({ - count: transactions.length, + count: allItems.length, overscan: 3, paddingEnd: 12, getScrollElement: () => parentRef.current, @@ -43,7 +91,7 @@ export const TransactionHistoryPage = () => { getItemKey, }) - if (!transactions.length && !executingRoutes.length && !isLoading) { + if (!allItems.length && !isLoading) { return } @@ -62,9 +110,6 @@ export const TransactionHistoryPage = () => { paddingX: 3, }} > - {executingRoutes.map((routeId) => ( - - ))} {isLoading ? ( {Array.from({ length: 3 }).map((_, index) => ( @@ -81,7 +126,7 @@ export const TransactionHistoryPage = () => { disablePadding > {getVirtualItems().map((item) => { - const transaction = transactions[item.index] + const listItem = allItems[item.index] return (
  • { transform: `translateY(${item.start}px)`, }} > - + {listItem.type === 'active' ? ( + + ) : ( + + )}
  • ) })} diff --git a/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx new file mode 100644 index 000000000..e8c749d0d --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx @@ -0,0 +1,47 @@ +import type { RouteExtended } from '@lifi/sdk' +import { Box } from '@mui/material' +import { Card } from '../../components/Card/Card.js' +import { ExecutionProgress } from '../../components/Step/ExecutionProgress.js' +import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { StepActionRow } from '../../components/Step/StepActionRow.js' +import type { RouteExecutionStatus } from '../../stores/routes/types.js' +import { prepareActions } from '../../utils/prepareActions.js' +import { TransactionList } from './Receipts.style.js' + +interface ExecutionProgressCardsProps { + route: RouteExtended + status: RouteExecutionStatus +} + +export const ExecutionProgressCards: React.FC = ({ + route, + status, +}) => { + return ( + + + + + + {route.steps.map((step) => ( + + {prepareActions(step.execution?.actions ?? []).map( + (actionsGroup, index) => ( + + ) + )} + + ))} + + + + + + + + ) +} diff --git a/packages/widget/src/pages/TransactionPage/Receipts.tsx b/packages/widget/src/pages/TransactionPage/Receipts.tsx index 43bf28864..f57bc05bf 100644 --- a/packages/widget/src/pages/TransactionPage/Receipts.tsx +++ b/packages/widget/src/pages/TransactionPage/Receipts.tsx @@ -46,7 +46,6 @@ export const Receipts: React.FC = ({ route }) => { key={index} step={step} actionsGroup={actionsGroup} - receiptsOnly /> ) )} diff --git a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx index efb6d5e1f..f76c037f3 100644 --- a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx +++ b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx @@ -1,16 +1,12 @@ import { Box, Button, Typography } from '@mui/material' import { useNavigate } from '@tanstack/react-router' -import { useCallback, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { BottomSheet } from '../../components/BottomSheet/BottomSheet.js' -import type { BottomSheetBase } from '../../components/BottomSheet/types.js' import { Card } from '../../components/Card/Card.js' import { CardTitle } from '../../components/Card/CardTitle.js' import type { StatusColor } from '../../components/IconCircle/IconCircle.js' import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { Token } from '../../components/Token/Token.js' import { useAvailableChains } from '../../hooks/useAvailableChains.js' -import { useSetContentHeight } from '../../hooks/useSetContentHeight.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { useFieldActions } from '../../stores/form/useFieldActions.js' import { @@ -40,43 +36,23 @@ const mapRouteStatus = (status: RouteExecutionStatus): StatusColor => { return 'info' } -interface StatusBottomSheetContentProps extends RouteExecution { - onClose(): void -} - export const StatusBottomSheet: React.FC = ({ status, route, }) => { - const ref = useRef(null) + const hasSuccessFlag = hasEnumFlag(status, RouteExecutionStatus.Done) + const hasFailedFlag = hasEnumFlag(status, RouteExecutionStatus.Failed) - const onClose = useCallback(() => { - ref.current?.close() - }, []) - - useEffect(() => { - const hasSuccessFlag = hasEnumFlag(status, RouteExecutionStatus.Done) - const hasFailedFlag = hasEnumFlag(status, RouteExecutionStatus.Failed) - if ((hasSuccessFlag || hasFailedFlag) && !ref.current?.isOpen()) { - ref.current?.open() - } - }, [status]) + if (!hasSuccessFlag && !hasFailedFlag) { + return null + } - return ( - - - - ) + return } -const StatusBottomSheetContent: React.FC = ({ +const StatusBottomSheetContent: React.FC = ({ status, route, - onClose, }) => { const { t } = useTranslation() const navigate = useNavigate() @@ -90,9 +66,6 @@ const StatusBottomSheetContent: React.FC = ({ } = useWidgetConfig() const { getChainById } = useAvailableChains() - const ref = useRef(null) - useSetContentHeight(ref) - const toToken = { ...(route.steps.at(-1)?.execution?.toToken ?? route.toToken), amount: BigInt( @@ -138,7 +111,6 @@ const StatusBottomSheetContent: React.FC = ({ const handleClose = () => { cleanFields() - onClose() } const handleSeeDetails = () => { @@ -221,7 +193,6 @@ const StatusBottomSheetContent: React.FC = ({ return ( = ({ - route, - startedAt, - transferId, - txLink, -}) => { - return ( - <> - - - - - - - - - {transferId ? ( - - ) : null} - - ) -} diff --git a/packages/widget/src/pages/TransactionPage/TransactionContent.tsx b/packages/widget/src/pages/TransactionPage/TransactionContent.tsx index aae36186b..16c8619fe 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionContent.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionContent.tsx @@ -1,9 +1,9 @@ import type { RouteExtended } from '@lifi/sdk' +import { Box } from '@mui/material' +import { WarningMessages } from '../../components/Messages/WarningMessages.js' import { RouteExecutionStatus } from '../../stores/routes/types.js' -import { hasEnumFlag } from '../../utils/enum.js' -import { TransactionCompleted } from './TransactionCompleted.js' -import { TransactionFailed } from './TransactionFailed.js' -import { TransactionPending } from './TransactionPending.js' +import { ExecutionProgressCards } from './ExecutionProgressCards.js' +import { TransactionFailedButtons } from './TransactionFailedButtons.js' import { TransactionReview } from './TransactionReview.js' interface TransactionContentProps { @@ -23,30 +23,27 @@ export const TransactionContent: React.FC = ({ deleteRoute, routeRefreshing, }) => { - if (hasEnumFlag(status, RouteExecutionStatus.Done)) { - const startedAt = new Date(route.steps[0].execution?.startedAt ?? 0) - return - } - - if (status === RouteExecutionStatus.Failed) { + if (status === RouteExecutionStatus.Idle) { return ( - ) } - if (status === RouteExecutionStatus.Pending) { - return - } - return ( - + + + + {status === RouteExecutionStatus.Failed ? ( + + ) : null} + ) } diff --git a/packages/widget/src/pages/TransactionPage/TransactionFailed.tsx b/packages/widget/src/pages/TransactionPage/TransactionFailed.tsx deleted file mode 100644 index 567ebcff4..000000000 --- a/packages/widget/src/pages/TransactionPage/TransactionFailed.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type { RouteExtended } from '@lifi/sdk' -import { Box, Button } from '@mui/material' -import { useTranslation } from 'react-i18next' -import { Card } from '../../components/Card/Card.js' -import { WarningMessages } from '../../components/Messages/WarningMessages.js' -import { ExecutionProgress } from '../../components/Step/ExecutionProgress.js' -import { RouteTokens } from '../../components/Step/RouteTokens.js' -import { StepActionRow } from '../../components/Step/StepActionRow.js' -import { useNavigateBack } from '../../hooks/useNavigateBack.js' -import { prepareActions } from '../../utils/prepareActions.js' -import { TransactionList } from './Receipts.style.js' -import { StartTransactionButton } from './StartTransactionButton.js' - -interface TransactionFailedProps { - route: RouteExtended - restartRoute: () => void - deleteRoute: () => void -} - -export const TransactionFailed: React.FC = ({ - route, - restartRoute, - deleteRoute, -}) => { - const { t } = useTranslation() - const navigateBack = useNavigateBack() - - const handleRemoveRoute = () => { - navigateBack() - deleteRoute() - } - - return ( - <> - - - - {route.steps.map((step) => ( - - {prepareActions(step.execution?.actions ?? []).map( - (actionsGroup, index) => ( - - ) - )} - - ))} - - - - - - - - - - - - - - - - ) -} diff --git a/packages/widget/src/pages/TransactionPage/TransactionFailedButtons.tsx b/packages/widget/src/pages/TransactionPage/TransactionFailedButtons.tsx new file mode 100644 index 000000000..8e9c86157 --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/TransactionFailedButtons.tsx @@ -0,0 +1,40 @@ +import type { RouteExtended } from '@lifi/sdk' +import { Box, Button } from '@mui/material' +import { useTranslation } from 'react-i18next' +import { useNavigateBack } from '../../hooks/useNavigateBack.js' +import { StartTransactionButton } from './StartTransactionButton.js' + +interface TransactionFailedButtonsProps { + route: RouteExtended + restartRoute: () => void + deleteRoute: () => void +} + +export const TransactionFailedButtons: React.FC< + TransactionFailedButtonsProps +> = ({ route, restartRoute, deleteRoute }) => { + const { t } = useTranslation() + const navigateBack = useNavigateBack() + + const handleRemoveRoute = () => { + navigateBack() + deleteRoute() + } + + return ( + + + + + + + + + ) +} diff --git a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx index ee41c30bc..1864db78c 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx @@ -2,6 +2,7 @@ import type { ExchangeRateUpdateParams } from '@lifi/sdk' import { useLocation } from '@tanstack/react-router' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { ContractComponent } from '../../components/ContractComponent/ContractComponent.js' import { PageContainer } from '../../components/PageContainer.js' import { useHeader } from '../../hooks/useHeader.js' import { useRouteExecution } from '../../hooks/useRouteExecution.js' @@ -12,13 +13,13 @@ import { WidgetEvent } from '../../types/events.js' import type { ExchangeRateBottomSheetBase } from './ExchangeRateBottomSheet.js' import { ExchangeRateBottomSheet } from './ExchangeRateBottomSheet.js' import { RouteTracker } from './RouteTracker.js' -import { StatusBottomSheet } from './StatusBottomSheet.js' import { TransactionContent } from './TransactionContent.js' export const TransactionPage = () => { const { t } = useTranslation() const emitter = useWidgetEvents() - const { subvariant, subvariantOptions } = useWidgetConfig() + const { subvariant, subvariantOptions, contractSecondaryComponent } = + useWidgetConfig() const { search }: any = useLocation() const stateRouteId = search?.routeId const [routeId, setRouteId] = useState(stateRouteId) @@ -92,7 +93,11 @@ export const TransactionPage = () => { deleteRoute={deleteRoute} routeRefreshing={routeRefreshing} /> - + {subvariant === 'custom' && contractSecondaryComponent ? ( + + {contractSecondaryComponent} + + ) : null} ) diff --git a/packages/widget/src/pages/TransactionPage/TransactionPending.tsx b/packages/widget/src/pages/TransactionPage/TransactionPending.tsx deleted file mode 100644 index cf4b02e5d..000000000 --- a/packages/widget/src/pages/TransactionPage/TransactionPending.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { RouteExtended } from '@lifi/sdk' -import { Card } from '../../components/Card/Card.js' -import { ExecutionProgress } from '../../components/Step/ExecutionProgress.js' -import { RouteTokens } from '../../components/Step/RouteTokens.js' -import { StepActionRow } from '../../components/Step/StepActionRow.js' -import { prepareActions } from '../../utils/prepareActions.js' -import { TransactionList } from './Receipts.style.js' - -interface TransactionPendingProps { - route: RouteExtended -} - -export const TransactionPending: React.FC = ({ - route, -}) => ( - <> - - - - {route.steps.map((step) => ( - - {prepareActions(step.execution?.actions ?? []).map( - (actionsGroup, index) => ( - - ) - )} - - ))} - - - - - - -) diff --git a/packages/widget/src/stores/routes/useRouteExecutionIndicators.ts b/packages/widget/src/stores/routes/useRouteExecutionIndicators.ts index 47e7af394..4a23af1d0 100644 --- a/packages/widget/src/stores/routes/useRouteExecutionIndicators.ts +++ b/packages/widget/src/stores/routes/useRouteExecutionIndicators.ts @@ -4,6 +4,11 @@ import { useRouteExecutionStore } from './RouteExecutionStore.js' import type { RouteExecution, RouteExecutionState } from './types.js' import { RouteExecutionStatus } from './types.js' +const isRecentTransaction = (route: RouteExecution): boolean => { + const startedAt = route.route.steps[0]?.execution?.startedAt ?? 0 + return startedAt > 0 && Date.now() - startedAt < 1000 * 60 * 60 * 24 // 1 day +} + export const useRouteExecutionIndicators = () => { const { accounts } = useAccount() const accountAddresses = useMemo( @@ -13,8 +18,10 @@ export const useRouteExecutionIndicators = () => { const selector = useCallback( (state: RouteExecutionState) => { const routes = Object.values(state.routes) as RouteExecution[] - const ownedRoutes = routes.filter((route) => - accountAddresses.includes(route.route.fromAddress) + const ownedRoutes = routes.filter( + (route) => + accountAddresses.includes(route.route.fromAddress) && + isRecentTransaction(route) ) return { hasActiveRoutes: ownedRoutes.some((r) => From e346c69c5f258409dfd455a3af75558cf7ee7ae0 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Fri, 13 Mar 2026 11:06:59 +0000 Subject: [PATCH 13/22] refactor: merge status bottom sheet statuses and execution progress --- .../src/components/Step/ExecutionProgress.tsx | 61 ++-- .../src/components/Step/StepActionRow.tsx | 2 +- .../components/Step/StepStatusIndicator.tsx | 5 +- .../src/hooks/useRouteExecutionMessage.ts | 86 ++++++ .../TransactionDetailsPage.tsx | 4 +- .../TransactionDetailsPage/TransferIdCard.tsx | 2 +- .../TransactionPage/ExecutionDoneCard.tsx | 48 +++ .../ExecutionProgressCards.tsx | 21 +- ...eipts.style.tsx => ReceiptsCard.style.tsx} | 0 .../{Receipts.tsx => ReceiptsCard.tsx} | 15 +- .../TransactionPage/StatusBottomSheet.tsx | 292 ------------------ .../TransactionPage/TransactionContent.tsx | 4 + .../TransactionDoneButtons.tsx | 78 +++++ .../pages/TransactionPage/TransactionPage.tsx | 12 + 14 files changed, 296 insertions(+), 334 deletions(-) create mode 100644 packages/widget/src/hooks/useRouteExecutionMessage.ts create mode 100644 packages/widget/src/pages/TransactionPage/ExecutionDoneCard.tsx rename packages/widget/src/pages/TransactionPage/{Receipts.style.tsx => ReceiptsCard.style.tsx} (100%) rename packages/widget/src/pages/TransactionPage/{Receipts.tsx => ReceiptsCard.tsx} (85%) delete mode 100644 packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx create mode 100644 packages/widget/src/pages/TransactionPage/TransactionDoneButtons.tsx diff --git a/packages/widget/src/components/Step/ExecutionProgress.tsx b/packages/widget/src/components/Step/ExecutionProgress.tsx index 10f50ad29..d2351238d 100644 --- a/packages/widget/src/components/Step/ExecutionProgress.tsx +++ b/packages/widget/src/components/Step/ExecutionProgress.tsx @@ -1,37 +1,57 @@ import type { RouteExtended } from '@lifi/sdk' import { Box, Typography } from '@mui/material' -import { useActionMessage } from '../../hooks/useActionMessage.js' -import { StatusBottomSheet } from '../../pages/TransactionPage/StatusBottomSheet.js' -import type { RouteExecutionStatus } from '../../stores/routes/types.js' +import { useRouteExecutionMessage } from '../../hooks/useRouteExecutionMessage.js' +import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { RouteExecutionStatus } from '../../stores/routes/types.js' +import { hasEnumFlag } from '../../utils/enum.js' import { StepStatusIndicator } from './StepStatusIndicator.js' export const ExecutionProgress: React.FC<{ route: RouteExtended status: RouteExecutionStatus }> = ({ route, status }) => { + const { + feeConfig, + subvariant, + contractSecondaryComponent, + contractCompactComponent, + } = useWidgetConfig() const lastStep = route.steps.at(-1) - const lastAction = lastStep?.execution?.actions?.at(-1) - const { title, message } = useActionMessage(lastStep, lastAction) + const { title, message } = useRouteExecutionMessage(route, status) - if (!lastStep || !lastAction) { + if (!lastStep) { return null } + const showContractComponent = + subvariant === 'custom' && + hasEnumFlag(status, RouteExecutionStatus.Done) && + (contractCompactComponent || contractSecondaryComponent) + + const VcComponent = + status === RouteExecutionStatus.Done ? feeConfig?._vcComponent : undefined + return ( - - - - - {title} - + {!showContractComponent ? ( + + + + ) : ( + contractCompactComponent || contractSecondaryComponent + )} + {title && ( + + {title} + + )} {message ? ( ) : null} - {/* TODO: Remove this once the logic is merged */} - + {VcComponent ? : null} ) } diff --git a/packages/widget/src/components/Step/StepActionRow.tsx b/packages/widget/src/components/Step/StepActionRow.tsx index 2df783e61..29b604a7e 100644 --- a/packages/widget/src/components/Step/StepActionRow.tsx +++ b/packages/widget/src/components/Step/StepActionRow.tsx @@ -3,7 +3,7 @@ import OpenInNew from '@mui/icons-material/OpenInNew' import type React from 'react' import { useActionMessage } from '../../hooks/useActionMessage.js' import { useExplorer } from '../../hooks/useExplorer.js' -import { ExternalLink } from '../../pages/TransactionPage/Receipts.style.js' +import { ExternalLink } from '../../pages/TransactionPage/ReceiptsCard.style.js' import { ActionRow } from '../ActionRow/ActionRow.js' export const StepActionRow: React.FC<{ diff --git a/packages/widget/src/components/Step/StepStatusIndicator.tsx b/packages/widget/src/components/Step/StepStatusIndicator.tsx index c0dfe1b8b..0eb49528c 100644 --- a/packages/widget/src/components/Step/StepStatusIndicator.tsx +++ b/packages/widget/src/components/Step/StepStatusIndicator.tsx @@ -11,11 +11,10 @@ export const StepStatusIndicator: React.FC = ({ }) => { const lastAction = step.execution?.actions?.at(-1) - const status = lastAction?.status || 'PENDING' + const status = step.execution?.status || 'PENDING' const substatus = lastAction?.substatus switch (status) { - case 'STARTED': case 'PENDING': { if (!step.execution?.signedAt) { return @@ -23,8 +22,6 @@ export const StepStatusIndicator: React.FC = ({ return } case 'ACTION_REQUIRED': - case 'MESSAGE_REQUIRED': - case 'RESET_REQUIRED': return case 'DONE': if (substatus === 'PARTIAL' || substatus === 'REFUNDED') { diff --git a/packages/widget/src/hooks/useRouteExecutionMessage.ts b/packages/widget/src/hooks/useRouteExecutionMessage.ts new file mode 100644 index 000000000..d930d30e5 --- /dev/null +++ b/packages/widget/src/hooks/useRouteExecutionMessage.ts @@ -0,0 +1,86 @@ +import type { RouteExtended } from '@lifi/sdk' +import { useTranslation } from 'react-i18next' +import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js' +import { RouteExecutionStatus } from '../stores/routes/types.js' +import { getActionMessage } from '../utils/getActionMessage.js' +import { getErrorMessage } from '../utils/getErrorMessage.js' +import { useAvailableChains } from './useAvailableChains.js' + +export const useRouteExecutionMessage = ( + route: RouteExtended, + status: RouteExecutionStatus +) => { + const { subvariant, subvariantOptions } = useWidgetConfig() + const { t } = useTranslation() + const { getChainById } = useAvailableChains() + + const transactionType = + route.fromChainId === route.toChainId ? 'swap' : 'bridge' + + let title: string | undefined + let message: string | undefined + + switch (status) { + case RouteExecutionStatus.Pending: { + const lastStep = route.steps.at(-1) + const lastAction = lastStep?.execution?.actions?.at(-1) + if (!lastStep || !lastAction) { + break + } + const actionMessage = getActionMessage( + t, + lastStep, + lastAction.type, + lastAction.status, + lastAction.substatus, + subvariant, + subvariantOptions + ) + title = actionMessage.title + message = actionMessage.message + break + } + case RouteExecutionStatus.Done: { + title = + subvariant === 'custom' + ? t( + `success.title.${subvariantOptions?.custom ?? 'checkout'}Successful` + ) + : t(`success.title.${transactionType}Successful`) + break + } + case RouteExecutionStatus.Done | RouteExecutionStatus.Partial: { + title = t(`success.title.${transactionType}PartiallySuccessful`) + message = t('success.message.exchangePartiallySuccessful', { + tool: route.steps.at(-1)?.toolDetails.name, + tokenSymbol: route.steps.at(-1)?.action.toToken.symbol, + }) + break + } + case RouteExecutionStatus.Done | RouteExecutionStatus.Refunded: { + title = t('success.title.refundIssued') + message = t('success.message.exchangePartiallySuccessful', { + tool: route.steps.at(-1)?.toolDetails.name, + tokenSymbol: route.steps.at(-1)?.action.toToken.symbol, + }) + break + } + case RouteExecutionStatus.Failed: { + const step = route.steps.find( + (step) => step.execution?.status === 'FAILED' + ) + if (!step) { + break + } + const action = step.execution?.actions.find( + (action) => action.status === 'FAILED' + ) + const actionMessage = getErrorMessage(t, getChainById, step, action) + title = actionMessage.title + message = actionMessage.message + break + } + } + + return { title, message } +} diff --git a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx index 37fd40a73..2f1edc8bf 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx @@ -19,7 +19,7 @@ import { useRouteExecutionStore } from '../../stores/routes/RouteExecutionStore. import { getSourceTxHash } from '../../stores/routes/utils.js' import { buildRouteFromTxHistory } from '../../utils/converters.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' -import { Receipts } from '../TransactionPage/Receipts.js' +import { ReceiptsCard } from '../TransactionPage/ReceiptsCard.js' import { TransactionDetailsSkeleton } from './TransactionDetailsSkeleton.js' import { TransferIdCard } from './TransferIdCard.js' @@ -120,7 +120,7 @@ export const TransactionDetailsPage: React.FC = () => { /> - + {subvariant === 'custom' && contractSecondaryComponent ? ( {contractSecondaryComponent} ) : null} diff --git a/packages/widget/src/pages/TransactionDetailsPage/TransferIdCard.tsx b/packages/widget/src/pages/TransactionDetailsPage/TransferIdCard.tsx index b8ce5aaba..85bb1bd8f 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/TransferIdCard.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/TransferIdCard.tsx @@ -27,7 +27,7 @@ export const TransferIdCard = ({ transferId, txLink }: TransferIdCardProps) => { } return ( - + { + const { t } = useTranslation() + + const toToken = { + ...(route.steps.at(-1)?.execution?.toToken ?? route.toToken), + amount: BigInt( + route.steps.at(-1)?.execution?.toAmount ?? + route.steps.at(-1)?.estimate.toAmount ?? + route.toAmount + ), + } + + return ( + + + + {hasEnumFlag(status, RouteExecutionStatus.Refunded) + ? t('header.refunded') + : t('header.received')} + + + + + ) +} diff --git a/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx index e8c749d0d..f20b8e6a4 100644 --- a/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx +++ b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx @@ -4,9 +4,12 @@ import { Card } from '../../components/Card/Card.js' import { ExecutionProgress } from '../../components/Step/ExecutionProgress.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' import { StepActionRow } from '../../components/Step/StepActionRow.js' -import type { RouteExecutionStatus } from '../../stores/routes/types.js' +import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { RouteExecutionStatus } from '../../stores/routes/types.js' +import { hasEnumFlag } from '../../utils/enum.js' import { prepareActions } from '../../utils/prepareActions.js' -import { TransactionList } from './Receipts.style.js' +import { ExecutionDoneCard } from './ExecutionDoneCard.js' +import { TransactionList } from './ReceiptsCard.style.js' interface ExecutionProgressCardsProps { route: RouteExtended @@ -17,6 +20,9 @@ export const ExecutionProgressCards: React.FC = ({ route, status, }) => { + const { feeConfig } = useWidgetConfig() + const VcComponent = + status === RouteExecutionStatus.Done ? feeConfig?._vcComponent : undefined return ( @@ -39,9 +45,14 @@ export const ExecutionProgressCards: React.FC = ({ - - - + {hasEnumFlag(status, RouteExecutionStatus.Done) ? ( + + ) : ( + + + + )} + {VcComponent ? : null} ) } diff --git a/packages/widget/src/pages/TransactionPage/Receipts.style.tsx b/packages/widget/src/pages/TransactionPage/ReceiptsCard.style.tsx similarity index 100% rename from packages/widget/src/pages/TransactionPage/Receipts.style.tsx rename to packages/widget/src/pages/TransactionPage/ReceiptsCard.style.tsx diff --git a/packages/widget/src/pages/TransactionPage/Receipts.tsx b/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx similarity index 85% rename from packages/widget/src/pages/TransactionPage/Receipts.tsx rename to packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx index f57bc05bf..77dd8e205 100644 --- a/packages/widget/src/pages/TransactionPage/Receipts.tsx +++ b/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx @@ -1,22 +1,23 @@ import type { RouteExtended } from '@lifi/sdk' import ContentCopyRounded from '@mui/icons-material/ContentCopyRounded' import OpenInNew from '@mui/icons-material/OpenInNew' -import { IconButton, Typography } from '@mui/material' +import { IconButton } from '@mui/material' import type { MouseEvent } from 'react' import { useTranslation } from 'react-i18next' import { ActionRow } from '../../components/ActionRow/ActionRow.js' import { Card } from '../../components/Card/Card.js' +import { CardTitle } from '../../components/Card/CardTitle.js' import { StepActionRow } from '../../components/Step/StepActionRow.js' import { useExplorer } from '../../hooks/useExplorer.js' import { prepareActions } from '../../utils/prepareActions.js' import { shortenAddress } from '../../utils/wallet.js' -import { ExternalLink, TransactionList } from './Receipts.style.js' +import { ExternalLink, TransactionList } from './ReceiptsCard.style.js' -interface ReceiptsProps { +interface ReceiptsCardProps { route: RouteExtended } -export const Receipts: React.FC = ({ route }) => { +export const ReceiptsCard = ({ route }: ReceiptsCardProps) => { const { t } = useTranslation() const { getAddressLink } = useExplorer() const toAddress = route.toAddress @@ -33,10 +34,8 @@ export const Receipts: React.FC = ({ route }) => { : undefined return ( - - - {t('main.receipts')} - + + {t('main.receipts')} {route.steps.map((step) => ( diff --git a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx b/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx deleted file mode 100644 index f76c037f3..000000000 --- a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import { Box, Button, Typography } from '@mui/material' -import { useNavigate } from '@tanstack/react-router' -import { useTranslation } from 'react-i18next' -import { Card } from '../../components/Card/Card.js' -import { CardTitle } from '../../components/Card/CardTitle.js' -import type { StatusColor } from '../../components/IconCircle/IconCircle.js' -import { IconCircle } from '../../components/IconCircle/IconCircle.js' -import { Token } from '../../components/Token/Token.js' -import { useAvailableChains } from '../../hooks/useAvailableChains.js' -import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' -import { useFieldActions } from '../../stores/form/useFieldActions.js' -import { - type RouteExecution, - RouteExecutionStatus, -} from '../../stores/routes/types.js' -import { getSourceTxHash } from '../../stores/routes/utils.js' -import { hasEnumFlag } from '../../utils/enum.js' -import { formatTokenAmount } from '../../utils/format.js' -import { getErrorMessage } from '../../utils/getErrorMessage.js' -import { navigationRoutes } from '../../utils/navigationRoutes.js' -import { CenterContainer } from './StatusBottomSheet.style.js' - -const mapRouteStatus = (status: RouteExecutionStatus): StatusColor => { - if (hasEnumFlag(status, RouteExecutionStatus.Partial)) { - return 'warning' - } - if (hasEnumFlag(status, RouteExecutionStatus.Refunded)) { - return 'warning' - } - if (hasEnumFlag(status, RouteExecutionStatus.Failed)) { - return 'error' - } - if (status === RouteExecutionStatus.Done) { - return 'success' - } - return 'info' -} - -export const StatusBottomSheet: React.FC = ({ - status, - route, -}) => { - const hasSuccessFlag = hasEnumFlag(status, RouteExecutionStatus.Done) - const hasFailedFlag = hasEnumFlag(status, RouteExecutionStatus.Failed) - - if (!hasSuccessFlag && !hasFailedFlag) { - return null - } - - return -} - -const StatusBottomSheetContent: React.FC = ({ - status, - route, -}) => { - const { t } = useTranslation() - const navigate = useNavigate() - const { setFieldValue } = useFieldActions() - const { - subvariant, - subvariantOptions, - contractSecondaryComponent, - contractCompactComponent, - feeConfig, - } = useWidgetConfig() - const { getChainById } = useAvailableChains() - - const toToken = { - ...(route.steps.at(-1)?.execution?.toToken ?? route.toToken), - amount: BigInt( - route.steps.at(-1)?.execution?.toAmount ?? - route.steps.at(-1)?.estimate.toAmount ?? - route.toAmount - ), - } - - const cleanFields = () => { - setFieldValue('fromAmount', '') - setFieldValue('toAmount', '') - } - - const handleDone = () => { - cleanFields() - navigate({ to: navigationRoutes.home, replace: true }) - } - - const handlePartialDone = () => { - if ( - toToken.chainId !== route.toToken.chainId && - toToken.address !== route.toToken.address - ) { - setFieldValue( - 'fromAmount', - formatTokenAmount(toToken.amount, toToken.decimals), - { isTouched: true } - ) - setFieldValue('fromChain', toToken.chainId, { isTouched: true }) - setFieldValue('fromToken', toToken.address, { isTouched: true }) - setFieldValue('toChain', route.toToken.chainId, { - isTouched: true, - }) - setFieldValue('toToken', route.toToken.address, { - isTouched: true, - }) - } else { - cleanFields() - } - navigate({ to: navigationRoutes.home, replace: true }) - } - - const handleClose = () => { - cleanFields() - } - - const handleSeeDetails = () => { - handleClose() - - const transactionHash = getSourceTxHash(route) - - navigate({ - to: navigationRoutes.transactionDetails, - search: { - routeId: route.id, - transactionHash, - }, - replace: true, - }) - } - - const transactionType = - route.fromChainId === route.toChainId ? 'swap' : 'bridge' - - let title: string | undefined - let primaryMessage: string | undefined - let failedMessage: string | undefined - let handlePrimaryButton = handleDone - switch (status) { - case RouteExecutionStatus.Done: { - title = - subvariant === 'custom' - ? t( - `success.title.${subvariantOptions?.custom ?? 'checkout'}Successful` - ) - : t(`success.title.${transactionType}Successful`) - handlePrimaryButton = handleDone - break - } - case RouteExecutionStatus.Done | RouteExecutionStatus.Partial: { - title = t(`success.title.${transactionType}PartiallySuccessful`) - primaryMessage = t('success.message.exchangePartiallySuccessful', { - tool: route.steps.at(-1)?.toolDetails.name, - tokenSymbol: route.steps.at(-1)?.action.toToken.symbol, - }) - handlePrimaryButton = handlePartialDone - break - } - case RouteExecutionStatus.Done | RouteExecutionStatus.Refunded: { - title = t('success.title.refundIssued') - primaryMessage = t('success.message.exchangePartiallySuccessful', { - tool: route.steps.at(-1)?.toolDetails.name, - tokenSymbol: route.steps.at(-1)?.action.toToken.symbol, - }) - break - } - case RouteExecutionStatus.Failed: { - const step = route.steps.find( - (step) => step.execution?.status === 'FAILED' - ) - if (!step) { - break - } - const action = step.execution?.actions.find( - (action) => action.status === 'FAILED' - ) - const actionMessage = getErrorMessage(t, getChainById, step, action) - title = actionMessage.title - failedMessage = actionMessage.message - handlePrimaryButton = handleClose - break - } - default: - break - } - - const showContractComponent = - subvariant === 'custom' && - hasEnumFlag(status, RouteExecutionStatus.Done) && - (contractCompactComponent || contractSecondaryComponent) - - const VcComponent = - status === RouteExecutionStatus.Done ? feeConfig?._vcComponent : undefined - - return ( - - {!showContractComponent ? ( - - - - ) : null} - - - {title} - - - {showContractComponent ? ( - contractCompactComponent || contractSecondaryComponent - ) : hasEnumFlag(status, RouteExecutionStatus.Failed) && failedMessage ? ( - - {failedMessage} - - ) : hasEnumFlag(status, RouteExecutionStatus.Done) ? ( - - - - - {hasEnumFlag(status, RouteExecutionStatus.Refunded) - ? t('header.refunded') - : t('header.received')} - - - - {primaryMessage && ( - - {primaryMessage} - - )} - - {VcComponent ? : null} - - ) : null} - - {hasEnumFlag(status, RouteExecutionStatus.Done) ? ( - - ) : null} - - - - ) -} diff --git a/packages/widget/src/pages/TransactionPage/TransactionContent.tsx b/packages/widget/src/pages/TransactionPage/TransactionContent.tsx index 16c8619fe..a8d30e19b 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionContent.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionContent.tsx @@ -2,7 +2,9 @@ import type { RouteExtended } from '@lifi/sdk' import { Box } from '@mui/material' import { WarningMessages } from '../../components/Messages/WarningMessages.js' import { RouteExecutionStatus } from '../../stores/routes/types.js' +import { hasEnumFlag } from '../../utils/enum.js' import { ExecutionProgressCards } from './ExecutionProgressCards.js' +import { TransactionDoneButtons } from './TransactionDoneButtons.js' import { TransactionFailedButtons } from './TransactionFailedButtons.js' import { TransactionReview } from './TransactionReview.js' @@ -43,6 +45,8 @@ export const TransactionContent: React.FC = ({ restartRoute={restartRoute} deleteRoute={deleteRoute} /> + ) : hasEnumFlag(status, RouteExecutionStatus.Done) ? ( + ) : null} ) diff --git a/packages/widget/src/pages/TransactionPage/TransactionDoneButtons.tsx b/packages/widget/src/pages/TransactionPage/TransactionDoneButtons.tsx new file mode 100644 index 000000000..072b24a67 --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/TransactionDoneButtons.tsx @@ -0,0 +1,78 @@ +import type { RouteExtended } from '@lifi/sdk' +import { Button } from '@mui/material' +import { useNavigate } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' +import { useFieldActions } from '../../stores/form/useFieldActions' +import { RouteExecutionStatus } from '../../stores/routes/types' +import { hasEnumFlag } from '../../utils/enum' +import { formatTokenAmount } from '../../utils/format' +import { navigationRoutes } from '../../utils/navigationRoutes' + +interface TransactionDoneButtonsProps { + route: RouteExtended + status: RouteExecutionStatus +} + +export const TransactionDoneButtons = ({ + route, + status, +}: TransactionDoneButtonsProps) => { + const { t } = useTranslation() + const navigate = useNavigate() + const { setFieldValue } = useFieldActions() + + if (!hasEnumFlag(status, RouteExecutionStatus.Done)) { + return null + } + + const cleanFields = () => { + setFieldValue('fromAmount', '') + setFieldValue('toAmount', '') + } + + const handlePartialDone = () => { + const toToken = { + ...(route.steps.at(-1)?.execution?.toToken ?? route.toToken), + amount: BigInt( + route.steps.at(-1)?.execution?.toAmount ?? + route.steps.at(-1)?.estimate.toAmount ?? + route.toAmount + ), + } + if ( + toToken.chainId !== route.toToken.chainId && + toToken.address !== route.toToken.address + ) { + setFieldValue( + 'fromAmount', + formatTokenAmount(toToken.amount, toToken.decimals), + { isTouched: true } + ) + setFieldValue('fromChain', toToken.chainId, { isTouched: true }) + setFieldValue('fromToken', toToken.address, { isTouched: true }) + setFieldValue('toChain', route.toToken.chainId, { + isTouched: true, + }) + setFieldValue('toToken', route.toToken.address, { + isTouched: true, + }) + } else { + cleanFields() + } + } + + const handleClick = () => { + if (hasEnumFlag(status, RouteExecutionStatus.Partial)) { + handlePartialDone() + } else { + cleanFields() + } + navigate({ to: navigationRoutes.home, replace: true }) + } + + return ( + + ) +} diff --git a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx index 1864db78c..d3a156b62 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx @@ -8,6 +8,7 @@ import { useHeader } from '../../hooks/useHeader.js' import { useRouteExecution } from '../../hooks/useRouteExecution.js' import { useWidgetEvents } from '../../hooks/useWidgetEvents.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { useFormStore } from '../../stores/form/useFormStore.js' import { RouteExecutionStatus } from '../../stores/routes/types.js' import { WidgetEvent } from '../../types/events.js' import type { ExchangeRateBottomSheetBase } from './ExchangeRateBottomSheet.js' @@ -24,6 +25,7 @@ export const TransactionPage = () => { const stateRouteId = search?.routeId const [routeId, setRouteId] = useState(stateRouteId) const [routeRefreshing, setRouteRefreshing] = useState(false) + const setFieldValue = useFormStore((store) => store.setFieldValue) const exchangeRateBottomSheetRef = useRef(null) @@ -67,6 +69,11 @@ export const TransactionPage = () => { [stateRouteId, status] ) + const cleanFields = () => { + setFieldValue('fromAmount', '') + setFieldValue('toAmount', '') + } + useHeader(getHeaderTitle(), headerAction) // biome-ignore lint/correctness/useExhaustiveDependencies: We want to emit event only when the page is mounted @@ -74,6 +81,11 @@ export const TransactionPage = () => { if (status === RouteExecutionStatus.Idle) { emitter.emit(WidgetEvent.ReviewTransactionPageEntered, route) } + + // Clean form fields when leaving the page + return () => { + cleanFields() + } }, []) if (!route || !status) { From 147a93fe4ff7878b95403dfbd7f56d24d732147d Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Fri, 13 Mar 2026 12:42:54 +0000 Subject: [PATCH 14/22] feat: store done transactions locally and deduplicate once available from api --- .../widget/src/hooks/useContactSupport.ts | 16 +++ .../widget/src/hooks/useTransactionHistory.ts | 28 ++++- .../ContactSupportButton.tsx | 18 +--- .../TransactionDetailsPage.tsx | 22 +--- .../ActiveTransactionCard.tsx | 5 +- .../TransactionHistoryItem.tsx | 63 +++++------ .../TransactionHistoryPage.tsx | 102 ++++++++++++------ .../useDeduplicateRoutes.ts | 31 ++++++ .../TransactionPage/ExecutionDoneCard.tsx | 2 +- .../ExecutionProgressCards.tsx | 57 +++++++++- .../TransactionFailedButtons.tsx | 38 ++++--- .../routes/createRouteExecutionStore.ts | 40 +++---- .../stores/routes/useCompletedRoutesIds.ts | 31 ++++++ 13 files changed, 310 insertions(+), 143 deletions(-) create mode 100644 packages/widget/src/hooks/useContactSupport.ts create mode 100644 packages/widget/src/pages/TransactionHistoryPage/useDeduplicateRoutes.ts create mode 100644 packages/widget/src/stores/routes/useCompletedRoutesIds.ts diff --git a/packages/widget/src/hooks/useContactSupport.ts b/packages/widget/src/hooks/useContactSupport.ts new file mode 100644 index 000000000..8b6f685ee --- /dev/null +++ b/packages/widget/src/hooks/useContactSupport.ts @@ -0,0 +1,16 @@ +import { WidgetEvent } from '../types/events.js' +import { useWidgetEvents } from './useWidgetEvents.js' + +export const useContactSupport = (supportId?: string) => { + const widgetEvents = useWidgetEvents() + + const handleContactSupport = () => { + if (!widgetEvents.all.has(WidgetEvent.ContactSupport)) { + window.open('https://help.li.fi', '_blank', 'nofollow noreferrer') + } else { + widgetEvents.emit(WidgetEvent.ContactSupport, { supportId }) + } + } + + return handleContactSupport +} diff --git a/packages/widget/src/hooks/useTransactionHistory.ts b/packages/widget/src/hooks/useTransactionHistory.ts index c517b59ce..c34eb9800 100644 --- a/packages/widget/src/hooks/useTransactionHistory.ts +++ b/packages/widget/src/hooks/useTransactionHistory.ts @@ -3,16 +3,21 @@ import { type ExtendedTransactionInfo, getTransactionHistory } from '@lifi/sdk' import { useAccount } from '@lifi/wallet-management' import type { QueryFunction } from '@tanstack/react-query' import { useQueries } from '@tanstack/react-query' +import { useMemo } from 'react' import { useSDKClient } from '../providers/SDKClientProvider.js' import { useWidgetConfig } from '../providers/WidgetProvider/WidgetProvider.js' +import type { RouteExecution } from '../stores/routes/types.js' +import { buildRouteFromTxHistory } from '../utils/converters.js' import { getQueryKey } from '../utils/queries.js' +import { useTools } from './useTools.js' export const useTransactionHistory = () => { const { accounts } = useAccount() const { keyPrefix } = useWidgetConfig() const sdkClient = useSDKClient() + const { tools } = useTools() - const { data, isLoading } = useQueries({ + const { data: transactions, isLoading } = useQueries({ queries: accounts.map((account) => ({ queryKey: [ getQueryKey('transaction-history', keyPrefix), @@ -41,7 +46,7 @@ export const useTransactionHistory = () => { enabled: Boolean(account.address), })), combine: (results) => { - const uniqueTransactions = new Map() + const uniqueTransactions = new Map() results.forEach((result) => { if (result.data) { result.data.forEach((transaction) => { @@ -63,16 +68,29 @@ export const useTransactionHistory = () => { ((b?.sending as ExtendedTransactionInfo)?.timestamp ?? 0) - ((a?.sending as ExtendedTransactionInfo)?.timestamp ?? 0) ) - }) as StatusResponse[] + }) return { - data: data, + data, isLoading: results.some((result) => result.isLoading), } }, }) + const routeExecutions = useMemo( + () => + (transactions ?? []).flatMap((transaction) => { + const routeExecution = buildRouteFromTxHistory( + transaction as FullStatusData, + tools + ) + return routeExecution ? [routeExecution] : [] + }), + [tools, transactions] + ) + return { - data, + data: routeExecutions, + rawData: transactions, isLoading, } } diff --git a/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx b/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx index ad2cbb21d..465ff5fdd 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx @@ -1,7 +1,6 @@ import { useTranslation } from 'react-i18next' import { CardIconButton } from '../../components/Card/CardIconButton.js' -import { useWidgetEvents } from '../../hooks/useWidgetEvents.js' -import { WidgetEvent } from '../../types/events.js' +import { useContactSupport } from '../../hooks/useContactSupport.js' interface ContactSupportButtonProps { supportId?: string @@ -11,23 +10,12 @@ export const ContactSupportButton = ({ supportId, }: ContactSupportButtonProps) => { const { t } = useTranslation() - const widgetEvents = useWidgetEvents() - - const handleClick = () => { - if (!widgetEvents.all.has(WidgetEvent.ContactSupport)) { - const url = 'https://help.li.fi' - const target = '_blank' - const rel = 'nofollow noreferrer' - window.open(url, target, rel) - } else { - widgetEvents.emit(WidgetEvent.ContactSupport, { supportId }) - } - } + const handleContactSupport = useContactSupport(supportId) return ( {t('button.contactSupport')} diff --git a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx index 2f1edc8bf..1ce175f6f 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx @@ -15,7 +15,6 @@ import { useHeader } from '../../hooks/useHeader.js' import { useTools } from '../../hooks/useTools.js' import { useTransactionDetails } from '../../hooks/useTransactionDetails.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' -import { useRouteExecutionStore } from '../../stores/routes/RouteExecutionStore.js' import { getSourceTxHash } from '../../stores/routes/utils.js' import { buildRouteFromTxHistory } from '../../utils/converters.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' @@ -35,11 +34,8 @@ export const TransactionDetailsPage: React.FC = () => { const { search }: any = useLocation() const { tools } = useTools() const { getTransactionLink } = useExplorer() - const storedRouteExecution = useRouteExecutionStore( - (store) => store.routes[search?.routeId] - ) const { transaction, isLoading } = useTransactionDetails( - !storedRouteExecution && search?.transactionHash + search?.transactionHash ) const title = @@ -49,20 +45,13 @@ export const TransactionDetailsPage: React.FC = () => { useHeader(title) const routeExecution = useMemo(() => { - if (storedRouteExecution) { - return storedRouteExecution - } if (isLoading) { return } if (transaction) { - const routeExecution = buildRouteFromTxHistory( - transaction as FullStatusData, - tools - ) - return routeExecution + return buildRouteFromTxHistory(transaction as FullStatusData, tools) } - }, [isLoading, storedRouteExecution, tools, transaction]) + }, [isLoading, tools, transaction]) useEffect(() => { if (!isLoading && !routeExecution) { @@ -93,11 +82,10 @@ export const TransactionDetailsPage: React.FC = () => { } const startedAt = new Date( - (routeExecution?.route.steps[0].execution?.startedAt ?? 0) * - (storedRouteExecution ? 1 : 1000) // local and BE routes have different ms handling + (routeExecution?.route.steps[0].execution?.startedAt ?? 0) * 1000 ) - if (isLoading && !storedRouteExecution) { + if (isLoading) { return } diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx index da27aef1f..bef50f7f0 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx @@ -2,6 +2,7 @@ import DeleteOutline from '@mui/icons-material/DeleteOutline' import { Box } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import type { MouseEvent } from 'react' +import { memo } from 'react' import { useTranslation } from 'react-i18next' import { ActionRow } from '../../components/ActionRow/ActionRow.js' import { Card } from '../../components/Card/Card.js' @@ -19,7 +20,7 @@ import { export const ActiveTransactionCard: React.FC<{ routeId: string -}> = ({ routeId }) => { +}> = memo(({ routeId }) => { const { t } = useTranslation() const navigate = useNavigate() const { route, status, restartRoute, deleteRoute } = useRouteExecution({ @@ -91,4 +92,4 @@ export const ActiveTransactionCard: React.FC<{ ) -} +}) diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx index a3e9f8490..de2b2ba83 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx @@ -1,48 +1,37 @@ -import type { FullStatusData, StatusResponse } from '@lifi/sdk' +import type { RouteExtended } from '@lifi/sdk' import { Box } from '@mui/material' import { useNavigate } from '@tanstack/react-router' -import { useMemo } from 'react' +import { memo } from 'react' import { Card } from '../../components/Card/Card.js' import { DateLabel } from '../../components/DateLabel/DateLabel.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' -import { useTools } from '../../hooks/useTools.js' -import { buildRouteFromTxHistory } from '../../utils/converters.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' -export const TransactionHistoryItem: React.FC<{ - transaction: StatusResponse -}> = ({ transaction }) => { - const navigate = useNavigate() - const { tools } = useTools() +interface TransactionHistoryItemProps { + route: RouteExtended + transactionHash: string + // startedAt in ms + startedAt: number +} - const routeExecution = useMemo( - () => buildRouteFromTxHistory(transaction as FullStatusData, tools), - [transaction, tools] - ) +export const TransactionHistoryItem = memo( + ({ route, transactionHash, startedAt }: TransactionHistoryItemProps) => { + const navigate = useNavigate() - if (!routeExecution?.route) { - return null - } + const handleClick = () => { + navigate({ + to: navigationRoutes.transactionDetails, + search: { transactionHash }, + }) + } - const handleClick = () => { - navigate({ - to: navigationRoutes.transactionDetails, - search: { - transactionHash: (transaction as FullStatusData).sending.txHash, - }, - }) + return ( + + + + + + + ) } - - const startedAt = new Date( - (routeExecution.route.steps[0].execution?.startedAt ?? 0) * 1000 - ) - - return ( - - - - - - - ) -} +) diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx index 50efcde44..97cb973bd 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx @@ -1,8 +1,3 @@ -import type { - ExtendedTransactionInfo, - FullStatusData, - StatusResponse, -} from '@lifi/sdk' import { Box, List } from '@mui/material' import { useVirtualizer } from '@tanstack/react-virtual' import { useCallback, useMemo, useRef } from 'react' @@ -12,36 +7,53 @@ import { useHeader } from '../../hooks/useHeader.js' import { useListHeight } from '../../hooks/useListHeight.js' import { useTransactionHistory } from '../../hooks/useTransactionHistory.js' import { useRouteExecutionStore } from '../../stores/routes/RouteExecutionStore.js' -import type { RouteExecutionState } from '../../stores/routes/types.js' +import type { + RouteExecution, + RouteExecutionState, +} from '../../stores/routes/types.js' +import { useCompletedRoutesIds } from '../../stores/routes/useCompletedRoutesIds.js' import { useExecutingRoutesIds } from '../../stores/routes/useExecutingRoutesIds.js' +import { getSourceTxHash } from '../../stores/routes/utils.js' import { ActiveTransactionCard } from './ActiveTransactionCard.js' import { minTransactionListHeight } from './constants.js' import { TransactionHistoryEmpty } from './TransactionHistoryEmpty.js' import { TransactionHistoryItem } from './TransactionHistoryItem.js' import { TransactionHistoryItemSkeleton } from './TransactionHistorySkeleton.js' +import { useDeduplicateRoutes } from './useDeduplicateRoutes.js' type ActiveItem = { type: 'active'; routeId: string; startedAt: number } +type LocalItem = { + type: 'local' + routeExecution: RouteExecution + txHash: string + // startedAt in ms + startedAt: number +} type HistoryItem = { type: 'history' - transaction: StatusResponse + routeExecution: RouteExecution + txHash: string + // startedAt in ms startedAt: number } -type TransactionListItem = ActiveItem | HistoryItem +type TransactionListItem = ActiveItem | LocalItem | HistoryItem -const routeStartedAtSelector = +const routeDataSelector = (routeIds: string[]) => (state: RouteExecutionState) => - Object.fromEntries( - routeIds.map((id) => [ - id, - state.routes[id]?.route.steps[0]?.execution?.startedAt ?? 0, - ]) - ) + Object.fromEntries(routeIds.map((id) => [id, state.routes[id]])) export const TransactionHistoryPage = () => { // Parent ref and useVirtualizer should be in one file to avoid blank page (0 virtual items) issue const parentRef = useRef(null) - const { data: transactions, isLoading } = useTransactionHistory() + const { + data: apiRouteExecutions, + rawData: rawTransactions, + isLoading, + } = useTransactionHistory() const executingRouteIds = useExecutingRoutesIds() + const completedRouteIds = useCompletedRoutesIds() + + useDeduplicateRoutes(rawTransactions ?? []) const { t } = useTranslation() useHeader(t('header.transactionHistory')) @@ -50,34 +62,60 @@ export const TransactionHistoryPage = () => { listParentRef: parentRef, }) - const startedAtByRouteId = useRouteExecutionStore( - routeStartedAtSelector(executingRouteIds) + const executingRouteData = useRouteExecutionStore( + routeDataSelector(executingRouteIds) + ) + const completedRouteData = useRouteExecutionStore( + routeDataSelector(completedRouteIds) ) const allItems = useMemo(() => { const activeItems: ActiveItem[] = executingRouteIds.map((routeId) => ({ type: 'active', routeId, - startedAt: startedAtByRouteId[routeId] ?? 0, - })) - const historyItems: HistoryItem[] = transactions.map((transaction) => ({ - type: 'history', - transaction, startedAt: - ((transaction as FullStatusData).sending as ExtendedTransactionInfo) - ?.timestamp ?? 0, + executingRouteData[routeId]?.route.steps[0]?.execution?.startedAt ?? 0, })) - return [...activeItems, ...historyItems].sort( + const localItems: LocalItem[] = completedRouteIds + .filter((id) => completedRouteData[id]) + .map((routeId) => { + const routeExecution = completedRouteData[routeId]! + return { + type: 'local', + routeExecution, + txHash: getSourceTxHash(routeExecution.route) ?? '', + // store startedAt is already in ms + startedAt: routeExecution.route.steps[0]?.execution?.startedAt ?? 0, + } + }) + const historyItems: HistoryItem[] = apiRouteExecutions.map( + (routeExecution) => ({ + type: 'history', + routeExecution, + txHash: getSourceTxHash(routeExecution.route) ?? '', + // API startedAt is in seconds; multiply by 1000 to normalize to ms + startedAt: + (routeExecution.route.steps[0]?.execution?.startedAt ?? 0) * 1000, + }) + ) + return [...activeItems, ...localItems, ...historyItems].sort( (a, b) => b.startedAt - a.startedAt ) - }, [executingRouteIds, startedAtByRouteId, transactions]) + }, [ + apiRouteExecutions, + completedRouteData, + completedRouteIds, + executingRouteData, + executingRouteIds, + ]) const getItemKey = useCallback( (index: number) => { const item = allItems[index] - return item.type === 'active' - ? `active-${item.routeId}` - : `history-${(item.transaction as FullStatusData).transactionId}-${index}` + if (item.type === 'active') { + return `active-${item.routeId}` + } + return item.txHash || item.routeExecution.route.id }, [allItems] ) @@ -145,7 +183,9 @@ export const TransactionHistoryPage = () => { ) : ( )} diff --git a/packages/widget/src/pages/TransactionHistoryPage/useDeduplicateRoutes.ts b/packages/widget/src/pages/TransactionHistoryPage/useDeduplicateRoutes.ts new file mode 100644 index 000000000..632330143 --- /dev/null +++ b/packages/widget/src/pages/TransactionHistoryPage/useDeduplicateRoutes.ts @@ -0,0 +1,31 @@ +import type { + ExtendedTransactionInfo, + FullStatusData, + StatusResponse, +} from '@lifi/sdk' +import { useEffect } from 'react' +import { useRouteExecutionStoreContext } from '../../stores/routes/RouteExecutionStore.js' +import { getSourceTxHash } from '../../stores/routes/utils.js' + +export const useDeduplicateRoutes = (transactions: StatusResponse[]) => { + const store = useRouteExecutionStoreContext() + + useEffect(() => { + if (!transactions.length) { + return + } + // Match by sending txHash — the only reliable link between store routes and API transfers + const apiTxHashes = new Set( + transactions.map( + (t) => ((t as FullStatusData).sending as ExtendedTransactionInfo).txHash + ) + ) + const { routes, deleteRoute } = store.getState() + for (const [id, routeExecution] of Object.entries(routes)) { + const txHash = getSourceTxHash(routeExecution?.route) + if (txHash && apiTxHashes.has(txHash)) { + deleteRoute(id) + } + } + }, [store, transactions]) +} diff --git a/packages/widget/src/pages/TransactionPage/ExecutionDoneCard.tsx b/packages/widget/src/pages/TransactionPage/ExecutionDoneCard.tsx index 88094915e..dba4f14b6 100644 --- a/packages/widget/src/pages/TransactionPage/ExecutionDoneCard.tsx +++ b/packages/widget/src/pages/TransactionPage/ExecutionDoneCard.tsx @@ -36,7 +36,7 @@ export const ExecutionDoneCard = ({ alignItems: 'center', }} > - + {hasEnumFlag(status, RouteExecutionStatus.Refunded) ? t('header.refunded') : t('header.received')} diff --git a/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx index f20b8e6a4..886b5539c 100644 --- a/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx +++ b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx @@ -1,15 +1,22 @@ import type { RouteExtended } from '@lifi/sdk' -import { Box } from '@mui/material' +import ContentCopyRounded from '@mui/icons-material/ContentCopyRounded' +import OpenInNew from '@mui/icons-material/OpenInNew' +import { Box, IconButton } from '@mui/material' +import type { MouseEvent } from 'react' +import { useTranslation } from 'react-i18next' +import { ActionRow } from '../../components/ActionRow/ActionRow.js' import { Card } from '../../components/Card/Card.js' import { ExecutionProgress } from '../../components/Step/ExecutionProgress.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' import { StepActionRow } from '../../components/Step/StepActionRow.js' +import { useExplorer } from '../../hooks/useExplorer.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { RouteExecutionStatus } from '../../stores/routes/types.js' import { hasEnumFlag } from '../../utils/enum.js' import { prepareActions } from '../../utils/prepareActions.js' +import { shortenAddress } from '../../utils/wallet.js' import { ExecutionDoneCard } from './ExecutionDoneCard.js' -import { TransactionList } from './ReceiptsCard.style.js' +import { ExternalLink, TransactionList } from './ReceiptsCard.style.js' interface ExecutionProgressCardsProps { route: RouteExtended @@ -20,9 +27,25 @@ export const ExecutionProgressCards: React.FC = ({ route, status, }) => { + const { t } = useTranslation() const { feeConfig } = useWidgetConfig() + const { getAddressLink } = useExplorer() + const isDone = hasEnumFlag(status, RouteExecutionStatus.Done) + const toAddress = isDone ? route.toAddress : undefined const VcComponent = status === RouteExecutionStatus.Done ? feeConfig?._vcComponent : undefined + + const handleCopy = (e: MouseEvent) => { + e.stopPropagation() + if (toAddress) { + navigator.clipboard.writeText(toAddress) + } + } + + const addressLink = toAddress + ? getAddressLink(toAddress, route.toChainId) + : undefined + return ( @@ -42,10 +65,38 @@ export const ExecutionProgressCards: React.FC = ({ )} ))} + {toAddress ? ( + + + + + {addressLink ? ( + + + + ) : undefined} + + } + /> + ) : undefined} - {hasEnumFlag(status, RouteExecutionStatus.Done) ? ( + {isDone ? ( ) : ( diff --git a/packages/widget/src/pages/TransactionPage/TransactionFailedButtons.tsx b/packages/widget/src/pages/TransactionPage/TransactionFailedButtons.tsx index 8e9c86157..5c09d4dc5 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionFailedButtons.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionFailedButtons.tsx @@ -1,7 +1,11 @@ import type { RouteExtended } from '@lifi/sdk' import { Box, Button } from '@mui/material' import { useTranslation } from 'react-i18next' +import { useContactSupport } from '../../hooks/useContactSupport.js' import { useNavigateBack } from '../../hooks/useNavigateBack.js' +import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { getSourceTxHash } from '../../stores/routes/utils.js' +import { HiddenUI } from '../../types/widget.js' import { StartTransactionButton } from './StartTransactionButton.js' interface TransactionFailedButtonsProps { @@ -14,7 +18,10 @@ export const TransactionFailedButtons: React.FC< TransactionFailedButtonsProps > = ({ route, restartRoute, deleteRoute }) => { const { t } = useTranslation() + const { hiddenUI } = useWidgetConfig() const navigateBack = useNavigateBack() + const supportId = getSourceTxHash(route) ?? route.id + const handleContactSupport = useContactSupport(supportId) const handleRemoveRoute = () => { navigateBack() @@ -22,19 +29,26 @@ export const TransactionFailedButtons: React.FC< } return ( - - - - - - + + + + + + + + + {!hiddenUI?.includes(HiddenUI.ContactSupport) ? ( + + ) : null} ) } diff --git a/packages/widget/src/stores/routes/createRouteExecutionStore.ts b/packages/widget/src/stores/routes/createRouteExecutionStore.ts index 01978020a..081674c5c 100644 --- a/packages/widget/src/stores/routes/createRouteExecutionStore.ts +++ b/packages/widget/src/stores/routes/createRouteExecutionStore.ts @@ -3,7 +3,7 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' import { hasEnumFlag } from '../../utils/enum.js' import type { PersistStoreProps } from '../types.js' -import type { RouteExecutionState } from './types.js' +import type { RouteExecution, RouteExecutionState } from './types.js' import { RouteExecutionStatus } from './types.js' import { isRouteDone, @@ -21,18 +21,14 @@ export const createRouteExecutionStore = ({ namePrefix }: PersistStoreProps) => if (!get().routes[route.id]) { set((state: RouteExecutionState) => { const routes = { ...state.routes } - // clean previous idle and done routes + // clean previous idle routes Object.keys(routes) .filter( (routeId) => - (!observableRouteIds?.includes(routeId) && - hasEnumFlag( - routes[routeId]!.status, - RouteExecutionStatus.Idle - )) || + !observableRouteIds?.includes(routeId) && hasEnumFlag( routes[routeId]!.status, - RouteExecutionStatus.Done + RouteExecutionStatus.Idle ) ) .forEach((routeId) => { @@ -107,19 +103,23 @@ export const createRouteExecutionStore = ({ namePrefix }: PersistStoreProps) => ...persistedState, } as RouteExecutionState try { - // Remove failed transactions from history after 1 day - const currentTime = Date.now() - const oneDay = 1000 * 60 * 60 * 24 - Object.values(state.routes).forEach((routeExecution) => { - const startedAt = - routeExecution?.route.steps?.find( - (step) => step.execution?.status === 'FAILED' - )?.execution?.startedAt ?? 0 - const outdated = startedAt > 0 && currentTime - startedAt > oneDay - if (routeExecution?.route && outdated) { - delete state.routes[routeExecution.route.id] + // Keep only the most recent 100 routes, evicting the oldest when the + // limit is exceeded. + const maxStoredRoutes = 100 + const allRoutes = Object.values(state.routes) as RouteExecution[] + const storedRoutes = allRoutes + .sort( + (a, b) => + (b.route.steps[0]?.execution?.startedAt ?? 0) - + (a.route.steps[0]?.execution?.startedAt ?? 0) + ) + .slice(0, maxStoredRoutes) + const keepIds = new Set(storedRoutes.map((r) => r.route.id)) + for (const id of Object.keys(state.routes)) { + if (!keepIds.has(id)) { + delete state.routes[id] } - }) + } } catch (error) { console.error(error) } diff --git a/packages/widget/src/stores/routes/useCompletedRoutesIds.ts b/packages/widget/src/stores/routes/useCompletedRoutesIds.ts new file mode 100644 index 000000000..5d9bc1d41 --- /dev/null +++ b/packages/widget/src/stores/routes/useCompletedRoutesIds.ts @@ -0,0 +1,31 @@ +import { useAccount } from '@lifi/wallet-management' +import { useCallback, useMemo } from 'react' +import { hasEnumFlag } from '../../utils/enum.js' +import { useRouteExecutionStore } from './RouteExecutionStore.js' +import type { RouteExecution, RouteExecutionState } from './types.js' +import { RouteExecutionStatus } from './types.js' + +export const useCompletedRoutesIds = () => { + const { accounts } = useAccount() + const accountAddresses = useMemo( + () => accounts.map((account) => account.address), + [accounts] + ) + const selector = useCallback( + (state: RouteExecutionState) => + (Object.values(state.routes) as RouteExecution[]) + .filter( + (item) => + accountAddresses.includes(item.route.fromAddress) && + hasEnumFlag(item.status, RouteExecutionStatus.Done) + ) + .sort( + (a, b) => + (b.route.steps[0]?.execution?.startedAt ?? 0) - + (a.route.steps[0]?.execution?.startedAt ?? 0) + ) + .map(({ route }) => route.id), + [accountAddresses] + ) + return useRouteExecutionStore(selector) +} From 2f3688873a4f97ff59987fe0229474957b9530b0 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Fri, 13 Mar 2026 16:31:16 +0000 Subject: [PATCH 15/22] refactor: timer --- .../components/ActionRow/ActionRow.style.tsx | 2 +- .../src/components/ActionRow/ActionRow.tsx | 30 +------- .../AmountInputAdornment.style.tsx | 28 ------- .../AmountInputEndAdornment.style.tsx | 12 +++ .../AmountInput/AmountInputEndAdornment.tsx | 2 +- .../AmountInput/AmountInputHeaderBadge.tsx | 8 +- .../src/components/ButtonChip/ButtonChip.tsx | 10 +++ .../src/components/Card/CardIconButton.tsx | 3 +- .../IconCircle/IconCircle.style.tsx | 12 +-- .../src/components/RouteCard/RouteToken.tsx | 6 +- .../src/components/Step/StepActionRow.tsx | 7 +- .../components/Step/StepStatusIndicator.tsx | 5 +- .../components/StepActions/StepActions.tsx | 6 +- .../Timer/StepStatusTimer.style.tsx | 25 +------ .../src/components/Timer/StepStatusTimer.tsx | 75 +++++++++++-------- .../widget/src/components/Token/Token.tsx | 2 +- .../components/TokenList/TokenListItem.tsx | 1 - .../widget/src/hooks/timer/useLoopProgress.ts | 36 +++++++++ .../ContactSupportButton.tsx | 10 +-- .../TransactionDetailsPage.tsx | 5 -- .../ActiveTransactionCard.style.tsx | 25 ++++--- .../ActiveTransactionCard.tsx | 20 ++--- .../ExecutionProgressCards.tsx | 8 +- .../pages/TransactionPage/ReceiptsCard.tsx | 8 +- 24 files changed, 175 insertions(+), 171 deletions(-) delete mode 100644 packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx create mode 100644 packages/widget/src/components/AmountInput/AmountInputEndAdornment.style.tsx create mode 100644 packages/widget/src/components/ButtonChip/ButtonChip.tsx create mode 100644 packages/widget/src/hooks/timer/useLoopProgress.ts diff --git a/packages/widget/src/components/ActionRow/ActionRow.style.tsx b/packages/widget/src/components/ActionRow/ActionRow.style.tsx index 54c669222..ed4b5575c 100644 --- a/packages/widget/src/components/ActionRow/ActionRow.style.tsx +++ b/packages/widget/src/components/ActionRow/ActionRow.style.tsx @@ -16,7 +16,7 @@ export const ActionIconCircle = styled(Box)(({ theme }) => ({ width: 24, height: 24, borderRadius: '50%', - backgroundColor: `rgba(${theme.vars.palette.success.mainChannel} / 0.12)`, + backgroundColor: `color-mix(in srgb, rgb(${theme.vars.palette.success.mainChannel}) 12%, ${theme.vars.palette.background.paper})`, })) export const ActionRowLabel = styled(Typography)(({ theme }) => ({ diff --git a/packages/widget/src/components/ActionRow/ActionRow.tsx b/packages/widget/src/components/ActionRow/ActionRow.tsx index 98502377c..abdd10645 100644 --- a/packages/widget/src/components/ActionRow/ActionRow.tsx +++ b/packages/widget/src/components/ActionRow/ActionRow.tsx @@ -1,42 +1,20 @@ -import Wallet from '@mui/icons-material/Wallet' -import { CircularProgress } from '@mui/material' import type { FC, ReactNode } from 'react' -import { IconCircle } from '../IconCircle/IconCircle.js' -import { - ActionIconCircle, - ActionRowContainer, - ActionRowLabel, -} from './ActionRow.style.js' - -export type ActionRowVariant = 'success' | 'error' | 'wallet' | 'pending' +import { ActionRowContainer, ActionRowLabel } from './ActionRow.style.js' interface ActionRowProps { - variant: ActionRowVariant message: string + startAdornment: ReactNode endAdornment?: ReactNode } -const startIcons: Record = { - success: () => , - error: () => , - wallet: () => ( - - - - ), - pending: () => , -} - export const ActionRow: FC = ({ - variant, message, + startAdornment, endAdornment, }) => { - const StartIcon = startIcons[variant] - return ( - + {startAdornment} {message} {endAdornment} diff --git a/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx b/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx deleted file mode 100644 index b41bacb91..000000000 --- a/packages/widget/src/components/AmountInput/AmountInputAdornment.style.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Box, styled } from '@mui/material' -import { ButtonTertiary } from '../ButtonTertiary.js' - -export const ButtonContainer = styled(Box)(({ theme }) => ({ - display: 'flex', - gap: theme.spacing(1), - padding: theme.spacing(0, 2, 2, 2), -})) - -export const AmountInputButton = styled(ButtonTertiary)(({ theme }) => ({ - padding: theme.spacing(0.75, 1.5), - lineHeight: 1, - fontSize: 12, - fontWeight: 700, - minWidth: 'unset', - height: 'auto', - flex: 1, - backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, - '&:hover, &:active': { - backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.08)`, - }, - ...theme.applyStyles('dark', { - backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, - '&:hover, &:active': { - backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.08)`, - }, - }), -})) diff --git a/packages/widget/src/components/AmountInput/AmountInputEndAdornment.style.tsx b/packages/widget/src/components/AmountInput/AmountInputEndAdornment.style.tsx new file mode 100644 index 000000000..1842f493c --- /dev/null +++ b/packages/widget/src/components/AmountInput/AmountInputEndAdornment.style.tsx @@ -0,0 +1,12 @@ +import { Box, styled } from '@mui/material' +import { ButtonChip } from '../ButtonChip/ButtonChip.js' + +export const ButtonContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1), + padding: theme.spacing(0, 2, 2, 2), +})) + +export const AmountInputButton = styled(ButtonChip)(() => ({ + flex: 1, +})) diff --git a/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx b/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx index 8cdc48881..182eb4a7c 100644 --- a/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputEndAdornment.tsx @@ -11,7 +11,7 @@ import { useFieldValues } from '../../stores/form/useFieldValues.js' import { AmountInputButton, ButtonContainer, -} from './AmountInputAdornment.style.js' +} from './AmountInputEndAdornment.style.js' export const AmountInputEndAdornment = memo(({ formType }: FormTypeProps) => { const { t } = useTranslation() diff --git a/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx index 6f8b88ed1..dec3da0a9 100644 --- a/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx @@ -8,7 +8,7 @@ import { useFieldValues } from '../../stores/form/useFieldValues.js' import { DisabledUI, HiddenUI } from '../../types/widget.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { shortenAddress } from '../../utils/wallet.js' -import { AmountInputButton } from './AmountInputAdornment.style.js' +import { ButtonChip } from '../ButtonChip/ButtonChip.js' export const AmountInputHeaderBadge: React.FC = () => { const { t } = useTranslation() @@ -44,14 +44,14 @@ export const AmountInputHeaderBadge: React.FC = () => { }) return ( - {toAddress ? shortenAddress(toAddress) : label} - + ) } diff --git a/packages/widget/src/components/ButtonChip/ButtonChip.tsx b/packages/widget/src/components/ButtonChip/ButtonChip.tsx new file mode 100644 index 000000000..725e765db --- /dev/null +++ b/packages/widget/src/components/ButtonChip/ButtonChip.tsx @@ -0,0 +1,10 @@ +import { styled } from '@mui/material' +import { ButtonTertiary } from '../ButtonTertiary.js' + +export const ButtonChip = styled(ButtonTertiary)(({ theme }) => ({ + padding: theme.spacing(0.5, 1.5), + fontSize: 12, + fontWeight: 700, + lineHeight: 1.3334, + height: 'auto', +})) diff --git a/packages/widget/src/components/Card/CardIconButton.tsx b/packages/widget/src/components/Card/CardIconButton.tsx index c22e0f2a6..edb3735e8 100644 --- a/packages/widget/src/components/Card/CardIconButton.tsx +++ b/packages/widget/src/components/Card/CardIconButton.tsx @@ -5,12 +5,11 @@ export const CardIconButton = styled(MuiIconButton)< IconButtonProps & Pick >(({ theme }) => { return { - padding: theme.spacing(1), + padding: theme.spacing(0.5), backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, '&:hover': { backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.08)`, }, fontSize: '1rem', - borderRadius: theme.vars.shape.borderRadiusSecondary, } }) diff --git a/packages/widget/src/components/IconCircle/IconCircle.style.tsx b/packages/widget/src/components/IconCircle/IconCircle.style.tsx index c13e2dd2d..0482b9cd9 100644 --- a/packages/widget/src/components/IconCircle/IconCircle.style.tsx +++ b/packages/widget/src/components/IconCircle/IconCircle.style.tsx @@ -5,7 +5,7 @@ export const iconCircleSize = 90 interface StatusColorConfig { color: string - alpha: number + mixAmount: number lightDarken: number darkDarken: number } @@ -20,28 +20,28 @@ export const getStatusColor = ( case 'success': return { color: theme.vars.palette.success.mainChannel, - alpha: 0.12, + mixAmount: 12, lightDarken: 0, darkDarken: 0, } case 'error': return { color: theme.vars.palette.error.mainChannel, - alpha: 0.12, + mixAmount: 12, lightDarken: 0, darkDarken: 0, } case 'warning': return { color: theme.vars.palette.warning.mainChannel, - alpha: 0.48, + mixAmount: 48, lightDarken: 0.32, darkDarken: 0, } case 'info': return { color: theme.vars.palette.info.mainChannel, - alpha: 0.12, + mixAmount: 12, lightDarken: 0, darkDarken: 0, } @@ -56,7 +56,7 @@ export const IconCircleRoot = styled(Box, { ({ theme, colorConfig, circleSize }) => { const svgSize = Math.round(circleSize * iconSizeRatio) return { - backgroundColor: `rgba(${colorConfig.color} / ${colorConfig.alpha})`, + backgroundColor: `color-mix(in srgb, rgb(${colorConfig.color}) ${colorConfig.mixAmount}%, ${theme.vars.palette.background.paper})`, borderRadius: '50%', width: circleSize, height: circleSize, diff --git a/packages/widget/src/components/RouteCard/RouteToken.tsx b/packages/widget/src/components/RouteCard/RouteToken.tsx index f7b847882..aa8c1cf46 100644 --- a/packages/widget/src/components/RouteCard/RouteToken.tsx +++ b/packages/widget/src/components/RouteCard/RouteToken.tsx @@ -45,11 +45,7 @@ export const RouteToken = ({ )} /> {!defaultExpanded ? ( - + {cardExpanded ? ( ) : ( diff --git a/packages/widget/src/components/Step/StepActionRow.tsx b/packages/widget/src/components/Step/StepActionRow.tsx index 29b604a7e..ebe6c71b7 100644 --- a/packages/widget/src/components/Step/StepActionRow.tsx +++ b/packages/widget/src/components/Step/StepActionRow.tsx @@ -5,6 +5,7 @@ import { useActionMessage } from '../../hooks/useActionMessage.js' import { useExplorer } from '../../hooks/useExplorer.js' import { ExternalLink } from '../../pages/TransactionPage/ReceiptsCard.style.js' import { ActionRow } from '../ActionRow/ActionRow.js' +import { IconCircle } from '../IconCircle/IconCircle.js' export const StepActionRow: React.FC<{ step: LiFiStepExtended @@ -23,13 +24,15 @@ export const StepActionRow: React.FC<{ ? getTransactionLink({ txLink: action.txLink, chain: action.chainId }) : undefined - if (!isDone && !isFailed && !transactionLink) { + if ((!isDone && !isFailed) || !transactionLink) { return null } return ( + } message={title ?? ''} endAdornment={ = ({ switch (status) { case 'PENDING': { - if (!step.execution?.signedAt) { - return - } return } case 'ACTION_REQUIRED': diff --git a/packages/widget/src/components/StepActions/StepActions.tsx b/packages/widget/src/components/StepActions/StepActions.tsx index c1bc8fa0f..2de2ed850 100644 --- a/packages/widget/src/components/StepActions/StepActions.tsx +++ b/packages/widget/src/components/StepActions/StepActions.tsx @@ -63,7 +63,9 @@ export const StepActions: React.FC<{ ({ + borderRadius: theme.vars.shape.borderRadiusSecondary, + })} > {cardExpanded ? ( @@ -77,7 +79,7 @@ export const StepActions: React.FC<{ {includedStep.toolDetails.name[0]} diff --git a/packages/widget/src/components/Timer/StepStatusTimer.style.tsx b/packages/widget/src/components/Timer/StepStatusTimer.style.tsx index afe5d1bf2..87ac4c9f0 100644 --- a/packages/widget/src/components/Timer/StepStatusTimer.style.tsx +++ b/packages/widget/src/components/Timer/StepStatusTimer.style.tsx @@ -7,12 +7,11 @@ import { import { iconCircleSize } from '../IconCircle/IconCircle.style.js' export const StatusCircle = styled(Box)({ + position: 'absolute', + inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', - width: iconCircleSize, - height: iconCircleSize, - borderRadius: '50%', }) export const RingContainer = styled(Box)({ @@ -30,24 +29,6 @@ export const ProgressTrack = styled(MuiCircularProgress)(({ theme }) => ({ })) export const TimerLabel = styled(Typography)({ - fontSize: 22, + fontSize: 18, fontWeight: 700, - fontVariantNumeric: 'tabular-nums', }) - -export const IndeterminateRing: React.FC = () => ( - - - - -) diff --git a/packages/widget/src/components/Timer/StepStatusTimer.tsx b/packages/widget/src/components/Timer/StepStatusTimer.tsx index eb4b017c9..03bf94965 100644 --- a/packages/widget/src/components/Timer/StepStatusTimer.tsx +++ b/packages/widget/src/components/Timer/StepStatusTimer.tsx @@ -2,79 +2,92 @@ import type { LiFiStepExtended } from '@lifi/sdk' import { CircularProgress as MuiCircularProgress } from '@mui/material' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { useLoopProgress } from '../../hooks/timer/useLoopProgress.js' import { useTimer } from '../../hooks/timer/useTimer.js' import { formatTimer } from '../../utils/timer.js' import { iconCircleSize } from '../IconCircle/IconCircle.style.js' import { - IndeterminateRing, ProgressTrack, RingContainer, StatusCircle, TimerLabel, } from './StepStatusTimer.style.js' -export { IndeterminateRing } - -const getExpiryTimestamp = (step: LiFiStepExtended) => { - const execution = step?.execution - if (!execution) { +function getExpiryTimestamp(step: LiFiStepExtended): Date { + const { signedAt } = step.execution ?? {} + if (!signedAt) { return new Date() } - return new Date( - (execution.signedAt ?? Date.now()) + step.estimate.executionDuration * 1000 - ) + return new Date(signedAt + step.estimate.executionDuration * 1000) +} + +interface TimerRingProps { + step: LiFiStepExtended + size?: number + showLabel?: boolean } -export const TimerRing: React.FC<{ step: LiFiStepExtended }> = ({ step }) => { +export const TimerRing: React.FC = ({ + step, + size = iconCircleSize, + showLabel = true, +}) => { const { i18n } = useTranslation() const [isExpired, setExpired] = useState(false) + const signedAt = step.execution?.signedAt const totalDuration = step.estimate.executionDuration * 1000 const expiryTimestamp = getExpiryTimestamp(step) const { days, hours, minutes, seconds } = useTimer({ - autoStart: true, + autoStart: Boolean(signedAt), expiryTimestamp, onExpire: () => setExpired(true), }) - const isTimerExpired = isExpired || (!minutes && !seconds) + const hasActiveCountdown = + Boolean(signedAt) && !isExpired && (Boolean(minutes) || Boolean(seconds)) + const remaining = Math.max(expiryTimestamp.getTime() - Date.now(), 0) - const progress = + const countdownProgress = totalDuration > 0 ? Math.min(((totalDuration - remaining) / totalDuration) * 100, 100) : 0 - if (isTimerExpired) { - return - } + const loopProgress = useLoopProgress({ + active: !hasActiveCountdown, + durationMs: 60_000, + tickMs: 100, + }) + const progress = hasActiveCountdown ? countdownProgress : loopProgress return ( - + - - - {formatTimer({ - locale: i18n.language, - days, - hours, - minutes, - seconds, - })} - - + {showLabel && hasActiveCountdown ? ( + + + {formatTimer({ + locale: i18n.language, + days, + hours, + minutes, + seconds, + })} + + + ) : null} ) } diff --git a/packages/widget/src/components/Token/Token.tsx b/packages/widget/src/components/Token/Token.tsx index 579698270..82a825195 100644 --- a/packages/widget/src/components/Token/Token.tsx +++ b/packages/widget/src/components/Token/Token.tsx @@ -157,7 +157,7 @@ const TokenBase: FC = ({ ) : null} {impactToken ? ( enableImpactTokenTooltip ? ( - + {t('format.percent', { value: priceImpact, diff --git a/packages/widget/src/components/TokenList/TokenListItem.tsx b/packages/widget/src/components/TokenList/TokenListItem.tsx index 3e1aa71a8..4d279e1f4 100644 --- a/packages/widget/src/components/TokenList/TokenListItem.tsx +++ b/packages/widget/src/components/TokenList/TokenListItem.tsx @@ -206,7 +206,6 @@ const TokenListItemButton: React.FC = memo( display: 'flex', fontSize: 16, color: 'warning.main', - cursor: 'help', }} /> diff --git a/packages/widget/src/hooks/timer/useLoopProgress.ts b/packages/widget/src/hooks/timer/useLoopProgress.ts new file mode 100644 index 000000000..7f8243d75 --- /dev/null +++ b/packages/widget/src/hooks/timer/useLoopProgress.ts @@ -0,0 +1,36 @@ +import { useEffect, useRef, useState } from 'react' + +interface UseLoopProgressOptions { + active: boolean + durationMs: number + tickMs: number +} + +/** + * Returns a 0–100 progress value that continuously loops over `durationMs`, + * updating every `tickMs`. Useful for animating indeterminate progress rings + * when the actual duration is unknown. The loop resets whenever `active` flips + * from false to true. + */ +export function useLoopProgress({ + active, + durationMs, + tickMs, +}: UseLoopProgressOptions): number { + const [progress, setProgress] = useState(0) + const startRef = useRef(0) + + useEffect(() => { + if (!active) { + return + } + startRef.current = Date.now() + const id = setInterval(() => { + const elapsed = (Date.now() - startRef.current) % durationMs + setProgress((elapsed / durationMs) * 100) + }, tickMs) + return () => clearInterval(id) + }, [active, durationMs, tickMs]) + + return progress +} diff --git a/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx b/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx index 465ff5fdd..17d932eb0 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/ContactSupportButton.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next' -import { CardIconButton } from '../../components/Card/CardIconButton.js' +import { ButtonChip } from '../../components/ButtonChip/ButtonChip.js' import { useContactSupport } from '../../hooks/useContactSupport.js' interface ContactSupportButtonProps { @@ -13,12 +13,8 @@ export const ContactSupportButton = ({ const handleContactSupport = useContactSupport(supportId) return ( - + {t('button.contactSupport')} - + ) } diff --git a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx index 1ce175f6f..d0f4c120d 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx @@ -7,7 +7,6 @@ import { Card } from '../../components/Card/Card.js' import { ContractComponent } from '../../components/ContractComponent/ContractComponent.js' import { DateLabel } from '../../components/DateLabel/DateLabel.js' import { PageContainer } from '../../components/PageContainer.js' -import { RouteCardEssentials } from '../../components/RouteCard/RouteCardEssentials.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' import { internalExplorerUrl } from '../../config/constants.js' import { useExplorer } from '../../hooks/useExplorer.js' @@ -102,10 +101,6 @@ export const TransactionDetailsPage: React.FC = () => { - diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx index 077fda473..2243a867a 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx @@ -1,16 +1,13 @@ -import { Button, IconButton, styled, Typography } from '@mui/material' - -export const TimerText = styled(Typography)({ - fontSize: 14, - fontWeight: 700, - fontVariantNumeric: 'tabular-nums', -}) +import { Button, buttonClasses, IconButton, styled } from '@mui/material' export const DeleteButton = styled(IconButton)(({ theme }) => ({ padding: theme.spacing(0.5), backgroundColor: theme.vars.palette.background.paper, width: 24, height: 24, + ...theme.applyStyles('dark', { + backgroundColor: theme.vars.palette.background.paper, + }), })) export const RetryButton = styled(Button)(({ theme }) => ({ @@ -18,10 +15,20 @@ export const RetryButton = styled(Button)(({ theme }) => ({ fontSize: 12, height: 24, minWidth: 'auto', - padding: theme.spacing(0.5, 1), + padding: theme.spacing(0.5, 1.5), color: theme.vars.palette.text.primary, backgroundColor: theme.vars.palette.background.paper, '&:hover': { - backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, + backgroundColor: theme.vars.palette.background.paper, }, + ...theme.applyStyles('dark', { + color: theme.vars.palette.text.primary, + backgroundColor: theme.vars.palette.background.paper, + '&:hover': { + backgroundColor: theme.vars.palette.background.paper, + }, + [`&.${buttonClasses.focusVisible}`]: { + backgroundColor: theme.vars.palette.background.paper, + }, + }), })) diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx index bef50f7f0..1e94cf39e 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx @@ -6,17 +6,15 @@ import { memo } from 'react' import { useTranslation } from 'react-i18next' import { ActionRow } from '../../components/ActionRow/ActionRow.js' import { Card } from '../../components/Card/Card.js' +import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { TimerRing } from '../../components/Timer/StepStatusTimer.js' import { StepTimer } from '../../components/Timer/StepTimer.js' import { useActionMessage } from '../../hooks/useActionMessage.js' import { useRouteExecution } from '../../hooks/useRouteExecution.js' import { RouteExecutionStatus } from '../../stores/routes/types.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' -import { - DeleteButton, - RetryButton, - TimerText, -} from './ActiveTransactionCard.style.js' +import { DeleteButton, RetryButton } from './ActiveTransactionCard.style.js' export const ActiveTransactionCard: React.FC<{ routeId: string @@ -61,7 +59,7 @@ export const ActiveTransactionCard: React.FC<{ {isFailed ? ( } message={t('error.title.transactionFailed')} endAdornment={ <> @@ -77,15 +75,13 @@ export const ActiveTransactionCard: React.FC<{ ) : undefined} {!isFailed && title ? ( - - + ) : undefined } + message={title} + endAdornment={lastStep ? : undefined} /> ) : undefined} diff --git a/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx index 886b5539c..a1f35a4ef 100644 --- a/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx +++ b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx @@ -1,10 +1,12 @@ import type { RouteExtended } from '@lifi/sdk' import ContentCopyRounded from '@mui/icons-material/ContentCopyRounded' import OpenInNew from '@mui/icons-material/OpenInNew' +import Wallet from '@mui/icons-material/Wallet' import { Box, IconButton } from '@mui/material' import type { MouseEvent } from 'react' import { useTranslation } from 'react-i18next' import { ActionRow } from '../../components/ActionRow/ActionRow.js' +import { ActionIconCircle } from '../../components/ActionRow/ActionRow.style.js' import { Card } from '../../components/Card/Card.js' import { ExecutionProgress } from '../../components/Step/ExecutionProgress.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' @@ -67,7 +69,11 @@ export const ExecutionProgressCards: React.FC = ({ ))} {toAddress ? ( + + + } message={t('main.sentToWallet', { address: shortenAddress(toAddress), })} diff --git a/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx b/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx index 77dd8e205..a76b62c3a 100644 --- a/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx +++ b/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx @@ -1,10 +1,12 @@ import type { RouteExtended } from '@lifi/sdk' import ContentCopyRounded from '@mui/icons-material/ContentCopyRounded' import OpenInNew from '@mui/icons-material/OpenInNew' +import Wallet from '@mui/icons-material/Wallet' import { IconButton } from '@mui/material' import type { MouseEvent } from 'react' import { useTranslation } from 'react-i18next' import { ActionRow } from '../../components/ActionRow/ActionRow.js' +import { ActionIconCircle } from '../../components/ActionRow/ActionRow.style.js' import { Card } from '../../components/Card/Card.js' import { CardTitle } from '../../components/Card/CardTitle.js' import { StepActionRow } from '../../components/Step/StepActionRow.js' @@ -52,7 +54,11 @@ export const ReceiptsCard = ({ route }: ReceiptsCardProps) => { ))} {toAddress ? ( + + + } message={t('main.sentToWallet', { address: shortenAddress(toAddress), })} From 75691795a06e652858917ee00cd06be46ffc4b1d Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Mon, 16 Mar 2026 12:14:32 +0000 Subject: [PATCH 16/22] refactor: styling --- .../Header/TransactionHistoryButton.style.tsx | 37 +++--- .../Header/TransactionHistoryButton.tsx | 29 +++-- .../src/components/Step/ExecutionProgress.tsx | 2 +- .../Timer/StepStatusTimer.style.tsx | 8 ++ .../src/components/Timer/StepStatusTimer.tsx | 10 +- .../widget/src/components/Timer/StepTimer.tsx | 13 +- .../src/components/Timer/TimerContent.tsx | 33 ----- .../useDeduplicateRoutes.ts | 4 +- .../ActiveTransactionCard.tsx | 91 ------------- ...le.tsx => ActiveTransactionItem.style.tsx} | 15 +-- .../ActiveTransactionItem.tsx | 115 +++++++++++++++++ .../TransactionHistoryItem.tsx | 40 +++--- .../TransactionHistoryPage.tsx | 121 ++---------------- .../src/pages/TransactionHistoryPage/types.ts | 18 +++ .../useTransactionList.ts | 88 +++++++++++++ ....tsx => ExchangeRateBottomSheet.style.tsx} | 0 .../ExchangeRateBottomSheet.tsx | 2 +- .../TokenValueBottomSheet.style.tsx | 6 + .../TransactionPage/TokenValueBottomSheet.tsx | 2 +- .../stores/routes/useCompletedRoutesIds.ts | 31 ----- .../stores/routes/useExecutingRoutesIds.ts | 31 ----- .../routes/useRouteExecutionIndicators.ts | 27 ++-- 22 files changed, 334 insertions(+), 389 deletions(-) delete mode 100644 packages/widget/src/components/Timer/TimerContent.tsx rename packages/widget/src/{pages/TransactionHistoryPage => hooks}/useDeduplicateRoutes.ts (84%) delete mode 100644 packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx rename packages/widget/src/pages/TransactionHistoryPage/{ActiveTransactionCard.style.tsx => ActiveTransactionItem.style.tsx} (56%) create mode 100644 packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.tsx create mode 100644 packages/widget/src/pages/TransactionHistoryPage/types.ts create mode 100644 packages/widget/src/pages/TransactionHistoryPage/useTransactionList.ts rename packages/widget/src/pages/TransactionPage/{StatusBottomSheet.style.tsx => ExchangeRateBottomSheet.style.tsx} (100%) delete mode 100644 packages/widget/src/stores/routes/useCompletedRoutesIds.ts delete mode 100644 packages/widget/src/stores/routes/useExecutingRoutesIds.ts diff --git a/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx b/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx index dca73b121..e6ce1eb75 100644 --- a/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx +++ b/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx @@ -1,4 +1,18 @@ -import { Badge, Box, CircularProgress, styled } from '@mui/material' +import { Badge, Box, IconButton, styled } from '@mui/material' +import type { RouteExecutionIndicator } from '../../stores/routes/useRouteExecutionIndicators.js' + +export const HistoryIconButton = styled(IconButton, { + shouldForwardProp: (prop) => prop !== 'indicator', +})<{ indicator: RouteExecutionIndicator }>(({ theme, indicator }) => + indicator !== 'idle' + ? { + backgroundColor: `color-mix(in srgb, rgb(${theme.vars.palette.info.mainChannel}) 8%, ${theme.vars.palette.background.paper})`, + '&:hover': { + backgroundColor: `color-mix(in srgb, rgb(${theme.vars.palette.info.mainChannel}) 12%, ${theme.vars.palette.background.paper})`, + }, + } + : {} +) export const ErrorBadge = styled(Badge)(({ theme }) => ({ '& .MuiBadge-badge': { @@ -8,8 +22,9 @@ export const ErrorBadge = styled(Badge)(({ theme }) => ({ height: 16, borderRadius: '50%', backgroundColor: theme.vars.palette.background.paper, - top: 0, - left: 8, + boxShadow: `0 0 0 2px ${theme.vars.palette.background.paper}`, + top: -2, + left: 10, }, })) @@ -19,19 +34,3 @@ export const ProgressContainer = styled(Box)({ alignItems: 'center', justifyContent: 'center', }) - -export const ProgressTrack = styled(CircularProgress)(({ theme }) => ({ - position: 'absolute', - color: theme.vars.palette.grey[300], - ...theme.applyStyles('dark', { - color: theme.vars.palette.grey[800], - }), -})) - -export const ProgressFill = styled(CircularProgress)(({ theme }) => ({ - position: 'absolute', - color: theme.vars.palette.primary.main, - ...theme.applyStyles('dark', { - color: theme.vars.palette.primary.light, - }), -})) diff --git a/packages/widget/src/components/Header/TransactionHistoryButton.tsx b/packages/widget/src/components/Header/TransactionHistoryButton.tsx index f6bc70154..c6c634b54 100644 --- a/packages/widget/src/components/Header/TransactionHistoryButton.tsx +++ b/packages/widget/src/components/Header/TransactionHistoryButton.tsx @@ -1,57 +1,58 @@ import ErrorRounded from '@mui/icons-material/ErrorRounded' import ReceiptLong from '@mui/icons-material/ReceiptLong' -import { IconButton, Tooltip } from '@mui/material' +import { Tooltip } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' -import { useRouteExecutionIndicators } from '../../stores/routes/useRouteExecutionIndicators.js' +import { useRouteExecutionIndicator } from '../../stores/routes/useRouteExecutionIndicators.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' +import { ProgressFill, ProgressTrack } from '../Timer/StepStatusTimer.style.js' import { ErrorBadge, + HistoryIconButton, ProgressContainer, - ProgressFill, - ProgressTrack, } from './TransactionHistoryButton.style.js' export const TransactionHistoryButton = () => { const { t } = useTranslation() const navigate = useNavigate() - const { hasActiveRoutes, hasFailedRoutes } = useRouteExecutionIndicators() + const indicator = useRouteExecutionIndicator() return ( - navigate({ to: navigationRoutes.transactionHistory })} > + } overlap="circular" anchorOrigin={{ vertical: 'top', horizontal: 'right' }} > - {hasActiveRoutes ? ( + {indicator !== 'idle' ? ( <> ) : null} - + ) } diff --git a/packages/widget/src/components/Step/ExecutionProgress.tsx b/packages/widget/src/components/Step/ExecutionProgress.tsx index d2351238d..e32864a56 100644 --- a/packages/widget/src/components/Step/ExecutionProgress.tsx +++ b/packages/widget/src/components/Step/ExecutionProgress.tsx @@ -32,7 +32,7 @@ export const ExecutionProgress: React.FC<{ status === RouteExecutionStatus.Done ? feeConfig?._vcComponent : undefined return ( - + {!showContractComponent ? ( diff --git a/packages/widget/src/components/Timer/StepStatusTimer.style.tsx b/packages/widget/src/components/Timer/StepStatusTimer.style.tsx index 87ac4c9f0..a57e78d2e 100644 --- a/packages/widget/src/components/Timer/StepStatusTimer.style.tsx +++ b/packages/widget/src/components/Timer/StepStatusTimer.style.tsx @@ -28,6 +28,14 @@ export const ProgressTrack = styled(MuiCircularProgress)(({ theme }) => ({ }), })) +export const ProgressFill = styled(MuiCircularProgress)(({ theme }) => ({ + position: 'absolute', + color: theme.vars.palette.primary.main, + ...theme.applyStyles('dark', { + color: theme.vars.palette.primary.light, + }), +})) + export const TimerLabel = styled(Typography)({ fontSize: 18, fontWeight: 700, diff --git a/packages/widget/src/components/Timer/StepStatusTimer.tsx b/packages/widget/src/components/Timer/StepStatusTimer.tsx index 03bf94965..5765eede0 100644 --- a/packages/widget/src/components/Timer/StepStatusTimer.tsx +++ b/packages/widget/src/components/Timer/StepStatusTimer.tsx @@ -1,5 +1,4 @@ import type { LiFiStepExtended } from '@lifi/sdk' -import { CircularProgress as MuiCircularProgress } from '@mui/material' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLoopProgress } from '../../hooks/timer/useLoopProgress.js' @@ -7,6 +6,7 @@ import { useTimer } from '../../hooks/timer/useTimer.js' import { formatTimer } from '../../utils/timer.js' import { iconCircleSize } from '../IconCircle/IconCircle.style.js' import { + ProgressFill, ProgressTrack, RingContainer, StatusCircle, @@ -24,12 +24,14 @@ function getExpiryTimestamp(step: LiFiStepExtended): Date { interface TimerRingProps { step: LiFiStepExtended size?: number + thickness?: number showLabel?: boolean } export const TimerRing: React.FC = ({ step, size = iconCircleSize, + thickness = 2, showLabel = true, }) => { const { i18n } = useTranslation() @@ -67,13 +69,13 @@ export const TimerRing: React.FC = ({ variant="determinate" value={100} size={size} - thickness={2} + thickness={thickness} /> - {showLabel && hasActiveCountdown ? ( diff --git a/packages/widget/src/components/Timer/StepTimer.tsx b/packages/widget/src/components/Timer/StepTimer.tsx index d48a807b0..2c79b29d3 100644 --- a/packages/widget/src/components/Timer/StepTimer.tsx +++ b/packages/widget/src/components/Timer/StepTimer.tsx @@ -1,9 +1,9 @@ import type { LiFiStepExtended } from '@lifi/sdk' +import { Typography } from '@mui/material' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useTimer } from '../../hooks/timer/useTimer.js' import { formatTimer } from '../../utils/timer.js' -import { TimerContent } from './TimerContent.js' const getExpiryTimestamp = (step: LiFiStepExtended) => { const execution = step?.execution @@ -21,15 +21,12 @@ export const StepTimer: React.FC<{ }> = ({ step }) => { if ( step.execution?.status === 'DONE' || - step.execution?.status === 'FAILED' + step.execution?.status === 'FAILED' || + !step.execution?.signedAt ) { return null } - if (!step.execution?.signedAt) { - return null - } - return } @@ -51,7 +48,7 @@ const ExecutionTimer = ({ expiryTimestamp }: { expiryTimestamp: Date }) => { } return ( - + {formatTimer({ locale: i18n.language, days, @@ -59,6 +56,6 @@ const ExecutionTimer = ({ expiryTimestamp }: { expiryTimestamp: Date }) => { minutes, seconds, })} - + ) } diff --git a/packages/widget/src/components/Timer/TimerContent.tsx b/packages/widget/src/components/Timer/TimerContent.tsx deleted file mode 100644 index 31f258c3b..000000000 --- a/packages/widget/src/components/Timer/TimerContent.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import AccessTimeFilled from '@mui/icons-material/AccessTimeFilled' -import { Box, Tooltip } from '@mui/material' -import type { FC, PropsWithChildren } from 'react' -import { useTranslation } from 'react-i18next' -import { IconTypography } from '../IconTypography.js' - -export const TimerContent: FC = ({ children }) => { - const { t } = useTranslation() - return ( - - - - - - - {children} - - - - ) -} diff --git a/packages/widget/src/pages/TransactionHistoryPage/useDeduplicateRoutes.ts b/packages/widget/src/hooks/useDeduplicateRoutes.ts similarity index 84% rename from packages/widget/src/pages/TransactionHistoryPage/useDeduplicateRoutes.ts rename to packages/widget/src/hooks/useDeduplicateRoutes.ts index 632330143..d91387f87 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/useDeduplicateRoutes.ts +++ b/packages/widget/src/hooks/useDeduplicateRoutes.ts @@ -4,8 +4,8 @@ import type { StatusResponse, } from '@lifi/sdk' import { useEffect } from 'react' -import { useRouteExecutionStoreContext } from '../../stores/routes/RouteExecutionStore.js' -import { getSourceTxHash } from '../../stores/routes/utils.js' +import { useRouteExecutionStoreContext } from '../stores/routes/RouteExecutionStore.js' +import { getSourceTxHash } from '../stores/routes/utils.js' export const useDeduplicateRoutes = (transactions: StatusResponse[]) => { const store = useRouteExecutionStoreContext() diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx deleted file mode 100644 index 1e94cf39e..000000000 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import DeleteOutline from '@mui/icons-material/DeleteOutline' -import { Box } from '@mui/material' -import { useNavigate } from '@tanstack/react-router' -import type { MouseEvent } from 'react' -import { memo } from 'react' -import { useTranslation } from 'react-i18next' -import { ActionRow } from '../../components/ActionRow/ActionRow.js' -import { Card } from '../../components/Card/Card.js' -import { IconCircle } from '../../components/IconCircle/IconCircle.js' -import { RouteTokens } from '../../components/Step/RouteTokens.js' -import { TimerRing } from '../../components/Timer/StepStatusTimer.js' -import { StepTimer } from '../../components/Timer/StepTimer.js' -import { useActionMessage } from '../../hooks/useActionMessage.js' -import { useRouteExecution } from '../../hooks/useRouteExecution.js' -import { RouteExecutionStatus } from '../../stores/routes/types.js' -import { navigationRoutes } from '../../utils/navigationRoutes.js' -import { DeleteButton, RetryButton } from './ActiveTransactionCard.style.js' - -export const ActiveTransactionCard: React.FC<{ - routeId: string -}> = memo(({ routeId }) => { - const { t } = useTranslation() - const navigate = useNavigate() - const { route, status, restartRoute, deleteRoute } = useRouteExecution({ - routeId, - executeInBackground: true, - }) - - const lastStep = route?.steps.findLast((step) => step.execution) - const lastAction = lastStep?.execution?.actions?.at(-1) - const { title } = useActionMessage(lastStep, lastAction) - - if (!route) { - return null - } - - const isFailed = status === RouteExecutionStatus.Failed - - const handleClick = () => { - navigate({ - to: navigationRoutes.transactionExecution, - search: { routeId }, - }) - } - - const handleDelete = (e: MouseEvent) => { - e.stopPropagation() - deleteRoute() - } - - const handleRetry = () => { - // NB: Do not stop propagation here: - // open the transaction execution page and retry the transaction simultaneously - restartRoute() - } - - return ( - - - {isFailed ? ( - } - message={t('error.title.transactionFailed')} - endAdornment={ - <> - - {t('button.retry')} - - - - - - } - /> - ) : undefined} - {!isFailed && title ? ( - - ) : undefined - } - message={title} - endAdornment={lastStep ? : undefined} - /> - ) : undefined} - - - - ) -}) diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.style.tsx similarity index 56% rename from packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx rename to packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.style.tsx index 2243a867a..15c7ece1a 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionCard.style.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.style.tsx @@ -1,4 +1,4 @@ -import { Button, buttonClasses, IconButton, styled } from '@mui/material' +import { IconButton, styled } from '@mui/material' export const DeleteButton = styled(IconButton)(({ theme }) => ({ padding: theme.spacing(0.5), @@ -10,25 +10,16 @@ export const DeleteButton = styled(IconButton)(({ theme }) => ({ }), })) -export const RetryButton = styled(Button)(({ theme }) => ({ +export const RetryButton = styled(IconButton)(({ theme }) => ({ fontWeight: 700, fontSize: 12, height: 24, minWidth: 'auto', + borderRadius: theme.vars.shape.borderRadius, padding: theme.spacing(0.5, 1.5), color: theme.vars.palette.text.primary, backgroundColor: theme.vars.palette.background.paper, - '&:hover': { - backgroundColor: theme.vars.palette.background.paper, - }, ...theme.applyStyles('dark', { - color: theme.vars.palette.text.primary, backgroundColor: theme.vars.palette.background.paper, - '&:hover': { - backgroundColor: theme.vars.palette.background.paper, - }, - [`&.${buttonClasses.focusVisible}`]: { - backgroundColor: theme.vars.palette.background.paper, - }, }), })) diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.tsx new file mode 100644 index 000000000..6733cf3f9 --- /dev/null +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.tsx @@ -0,0 +1,115 @@ +import type { LiFiStepExtended } from '@lifi/sdk' +import DeleteOutline from '@mui/icons-material/DeleteOutline' +import { Box } from '@mui/material' +import { useNavigate } from '@tanstack/react-router' +import type { MouseEvent } from 'react' +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import { ActionRow } from '../../components/ActionRow/ActionRow.js' +import { Card } from '../../components/Card/Card.js' +import { IconCircle } from '../../components/IconCircle/IconCircle.js' +import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { TimerRing } from '../../components/Timer/StepStatusTimer.js' +import { StepTimer } from '../../components/Timer/StepTimer.js' +import { useActionMessage } from '../../hooks/useActionMessage.js' +import { useRouteExecution } from '../../hooks/useRouteExecution.js' +import { RouteExecutionStatus } from '../../stores/routes/types.js' +import { navigationRoutes } from '../../utils/navigationRoutes.js' +import { DeleteButton, RetryButton } from './ActiveTransactionItem.style.js' + +export const ActiveTransactionItem: React.FC<{ routeId: string }> = memo( + ({ routeId }) => { + const navigate = useNavigate() + const { route, status, restartRoute, deleteRoute } = useRouteExecution({ + routeId, + executeInBackground: true, + }) + + const lastStep = route?.steps.findLast((step) => step.execution) + const lastAction = lastStep?.execution?.actions?.at(-1) + const { title } = useActionMessage(lastStep, lastAction) + + if (!route) { + return null + } + + const isFailed = status === RouteExecutionStatus.Failed + + const handleClick = () => { + navigate({ + to: navigationRoutes.transactionExecution, + search: { routeId }, + }) + } + + const handleDelete = (e: MouseEvent) => { + e.stopPropagation() + deleteRoute() + } + + const handleRetry = () => { + // NB: Do not stop propagation here: + // open the transaction execution page and retry the transaction simultaneously + restartRoute() + } + + return ( + + + {isFailed ? ( + + ) : ( + + )} + + + + ) + } +) + +const FailedTransactionRow: React.FC<{ + onRetry: () => void + onDelete: (e: MouseEvent) => void +}> = ({ onRetry, onDelete }) => { + const { t } = useTranslation() + return ( + } + message={t('error.title.transactionFailed')} + endAdornment={ + <> + + {t('button.retry')} + + + + + + } + /> + ) +} + +const PendingTransactionRow: React.FC<{ + step: LiFiStepExtended | undefined + title: string | undefined +}> = ({ step, title }) => { + if (!title) { + return null + } + return ( + + ) : undefined + } + message={title} + endAdornment={step ? : undefined} + /> + ) +} diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx index de2b2ba83..10cf0892a 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx @@ -7,31 +7,27 @@ import { DateLabel } from '../../components/DateLabel/DateLabel.js' import { RouteTokens } from '../../components/Step/RouteTokens.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' -interface TransactionHistoryItemProps { +export const TransactionHistoryItem: React.FC<{ route: RouteExtended transactionHash: string // startedAt in ms startedAt: number -} +}> = memo(({ route, transactionHash, startedAt }) => { + const navigate = useNavigate() -export const TransactionHistoryItem = memo( - ({ route, transactionHash, startedAt }: TransactionHistoryItemProps) => { - const navigate = useNavigate() - - const handleClick = () => { - navigate({ - to: navigationRoutes.transactionDetails, - search: { transactionHash }, - }) - } - - return ( - - - - - - - ) + const handleClick = () => { + navigate({ + to: navigationRoutes.transactionDetails, + search: { transactionHash }, + }) } -) + + return ( + + + + + + + ) +}) diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx index 97cb973bd..ceca040d8 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx @@ -1,59 +1,21 @@ import { Box, List } from '@mui/material' import { useVirtualizer } from '@tanstack/react-virtual' -import { useCallback, useMemo, useRef } from 'react' +import { useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' import { PageContainer } from '../../components/PageContainer.js' import { useHeader } from '../../hooks/useHeader.js' import { useListHeight } from '../../hooks/useListHeight.js' -import { useTransactionHistory } from '../../hooks/useTransactionHistory.js' -import { useRouteExecutionStore } from '../../stores/routes/RouteExecutionStore.js' -import type { - RouteExecution, - RouteExecutionState, -} from '../../stores/routes/types.js' -import { useCompletedRoutesIds } from '../../stores/routes/useCompletedRoutesIds.js' -import { useExecutingRoutesIds } from '../../stores/routes/useExecutingRoutesIds.js' -import { getSourceTxHash } from '../../stores/routes/utils.js' -import { ActiveTransactionCard } from './ActiveTransactionCard.js' +import { ActiveTransactionItem } from './ActiveTransactionItem.js' import { minTransactionListHeight } from './constants.js' import { TransactionHistoryEmpty } from './TransactionHistoryEmpty.js' import { TransactionHistoryItem } from './TransactionHistoryItem.js' import { TransactionHistoryItemSkeleton } from './TransactionHistorySkeleton.js' -import { useDeduplicateRoutes } from './useDeduplicateRoutes.js' - -type ActiveItem = { type: 'active'; routeId: string; startedAt: number } -type LocalItem = { - type: 'local' - routeExecution: RouteExecution - txHash: string - // startedAt in ms - startedAt: number -} -type HistoryItem = { - type: 'history' - routeExecution: RouteExecution - txHash: string - // startedAt in ms - startedAt: number -} -type TransactionListItem = ActiveItem | LocalItem | HistoryItem - -const routeDataSelector = - (routeIds: string[]) => (state: RouteExecutionState) => - Object.fromEntries(routeIds.map((id) => [id, state.routes[id]])) +import { useTransactionList } from './useTransactionList.js' export const TransactionHistoryPage = () => { // Parent ref and useVirtualizer should be in one file to avoid blank page (0 virtual items) issue const parentRef = useRef(null) - const { - data: apiRouteExecutions, - rawData: rawTransactions, - isLoading, - } = useTransactionHistory() - const executingRouteIds = useExecutingRoutesIds() - const completedRouteIds = useCompletedRoutesIds() - - useDeduplicateRoutes(rawTransactions ?? []) + const { items, isLoading } = useTransactionList() const { t } = useTranslation() useHeader(t('header.transactionHistory')) @@ -62,66 +24,19 @@ export const TransactionHistoryPage = () => { listParentRef: parentRef, }) - const executingRouteData = useRouteExecutionStore( - routeDataSelector(executingRouteIds) - ) - const completedRouteData = useRouteExecutionStore( - routeDataSelector(completedRouteIds) - ) - - const allItems = useMemo(() => { - const activeItems: ActiveItem[] = executingRouteIds.map((routeId) => ({ - type: 'active', - routeId, - startedAt: - executingRouteData[routeId]?.route.steps[0]?.execution?.startedAt ?? 0, - })) - const localItems: LocalItem[] = completedRouteIds - .filter((id) => completedRouteData[id]) - .map((routeId) => { - const routeExecution = completedRouteData[routeId]! - return { - type: 'local', - routeExecution, - txHash: getSourceTxHash(routeExecution.route) ?? '', - // store startedAt is already in ms - startedAt: routeExecution.route.steps[0]?.execution?.startedAt ?? 0, - } - }) - const historyItems: HistoryItem[] = apiRouteExecutions.map( - (routeExecution) => ({ - type: 'history', - routeExecution, - txHash: getSourceTxHash(routeExecution.route) ?? '', - // API startedAt is in seconds; multiply by 1000 to normalize to ms - startedAt: - (routeExecution.route.steps[0]?.execution?.startedAt ?? 0) * 1000, - }) - ) - return [...activeItems, ...localItems, ...historyItems].sort( - (a, b) => b.startedAt - a.startedAt - ) - }, [ - apiRouteExecutions, - completedRouteData, - completedRouteIds, - executingRouteData, - executingRouteIds, - ]) - const getItemKey = useCallback( (index: number) => { - const item = allItems[index] + const item = items[index] if (item.type === 'active') { return `active-${item.routeId}` } return item.txHash || item.routeExecution.route.id }, - [allItems] + [items] ) const { getVirtualItems, getTotalSize, measureElement } = useVirtualizer({ - count: allItems.length, + count: items.length, overscan: 3, paddingEnd: 12, getScrollElement: () => parentRef.current, @@ -129,7 +44,7 @@ export const TransactionHistoryPage = () => { getItemKey, }) - if (!allItems.length && !isLoading) { + if (!items.length && !isLoading) { return } @@ -140,13 +55,8 @@ export const TransactionHistoryPage = () => { > {isLoading ? ( @@ -157,14 +67,11 @@ export const TransactionHistoryPage = () => { ) : ( {getVirtualItems().map((item) => { - const listItem = allItems[item.index] + const listItem = items[item.index] return (
  • { top: 0, left: 0, width: '100%', - paddingBottom: 12, + paddingBottom: 16, transform: `translateY(${item.start}px)`, }} > {listItem.type === 'active' ? ( - + ) : ( state.routes + +export const useTransactionList = () => { + const { accounts } = useAccount() + const accountAddresses = useMemo( + () => accounts.map((account) => account.address), + [accounts] + ) + + const { + data: apiRouteExecutions, + rawData: rawTransactions, + isLoading, + } = useTransactionHistory() + + useDeduplicateRoutes(rawTransactions ?? []) + + const routes = useRouteExecutionStore(routesSelector) + + const items = useMemo(() => { + const owned = (Object.values(routes) as RouteExecution[]).filter((r) => + accountAddresses.includes(r.route.fromAddress) + ) + const byStartedAt = (a: RouteExecution, b: RouteExecution) => + (b.route.steps[0]?.execution?.startedAt ?? 0) - + (a.route.steps[0]?.execution?.startedAt ?? 0) + + const activeItems: ActiveItem[] = owned + .filter( + (r) => + r.status === RouteExecutionStatus.Pending || + r.status === RouteExecutionStatus.Failed + ) + .sort(byStartedAt) + .map((r) => ({ + type: 'active', + routeId: r.route.id, + startedAt: r.route.steps[0]?.execution?.startedAt ?? 0, + })) + + const localItems: LocalItem[] = owned + .filter((r) => hasEnumFlag(r.status, RouteExecutionStatus.Done)) + .sort(byStartedAt) + .map((r) => ({ + type: 'local', + routeExecution: r, + txHash: getSourceTxHash(r.route) ?? '', + // store startedAt is already in ms + startedAt: r.route.steps[0]?.execution?.startedAt ?? 0, + })) + + const historyItems: HistoryItem[] = apiRouteExecutions.map( + (routeExecution) => ({ + type: 'history', + routeExecution, + txHash: getSourceTxHash(routeExecution.route) ?? '', + // API startedAt is in seconds; multiply by 1000 to normalize to ms + startedAt: + (routeExecution.route.steps[0]?.execution?.startedAt ?? 0) * 1000, + }) + ) + + return [...activeItems, ...localItems, ...historyItems].sort( + (a, b) => b.startedAt - a.startedAt + ) + }, [accountAddresses, apiRouteExecutions, routes]) + + return { items, isLoading } +} diff --git a/packages/widget/src/pages/TransactionPage/StatusBottomSheet.style.tsx b/packages/widget/src/pages/TransactionPage/ExchangeRateBottomSheet.style.tsx similarity index 100% rename from packages/widget/src/pages/TransactionPage/StatusBottomSheet.style.tsx rename to packages/widget/src/pages/TransactionPage/ExchangeRateBottomSheet.style.tsx diff --git a/packages/widget/src/pages/TransactionPage/ExchangeRateBottomSheet.tsx b/packages/widget/src/pages/TransactionPage/ExchangeRateBottomSheet.tsx index 855766fd9..9ec7f6f3b 100644 --- a/packages/widget/src/pages/TransactionPage/ExchangeRateBottomSheet.tsx +++ b/packages/widget/src/pages/TransactionPage/ExchangeRateBottomSheet.tsx @@ -14,7 +14,7 @@ import type { BottomSheetBase } from '../../components/BottomSheet/types.js' import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { useSetContentHeight } from '../../hooks/useSetContentHeight.js' import { formatTokenAmount } from '../../utils/format.js' -import { CenterContainer } from './StatusBottomSheet.style.js' +import { CenterContainer } from './ExchangeRateBottomSheet.style.js' export interface ExchangeRateBottomSheetBase { isOpen(): void diff --git a/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.style.tsx b/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.style.tsx index 5d9db0b84..5d107ece0 100644 --- a/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.style.tsx +++ b/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.style.tsx @@ -1,5 +1,11 @@ import { Box, styled, Typography } from '@mui/material' +export const CenterContainer = styled(Box)(() => ({ + display: 'grid', + placeItems: 'center', + position: 'relative', +})) + export const ContentContainer = styled(Box)(({ theme }) => ({ padding: theme.spacing(3), })) diff --git a/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx b/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx index 552c2cbe1..05863de3f 100644 --- a/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx +++ b/packages/widget/src/pages/TransactionPage/TokenValueBottomSheet.tsx @@ -9,9 +9,9 @@ import { FeeBreakdownTooltip } from '../../components/FeeBreakdownTooltip.js' import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { useSetContentHeight } from '../../hooks/useSetContentHeight.js' import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js' -import { CenterContainer } from './StatusBottomSheet.style.js' import { ButtonRow, + CenterContainer, ContentContainer, DetailLabel, DetailRow, diff --git a/packages/widget/src/stores/routes/useCompletedRoutesIds.ts b/packages/widget/src/stores/routes/useCompletedRoutesIds.ts deleted file mode 100644 index 5d9bc1d41..000000000 --- a/packages/widget/src/stores/routes/useCompletedRoutesIds.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useAccount } from '@lifi/wallet-management' -import { useCallback, useMemo } from 'react' -import { hasEnumFlag } from '../../utils/enum.js' -import { useRouteExecutionStore } from './RouteExecutionStore.js' -import type { RouteExecution, RouteExecutionState } from './types.js' -import { RouteExecutionStatus } from './types.js' - -export const useCompletedRoutesIds = () => { - const { accounts } = useAccount() - const accountAddresses = useMemo( - () => accounts.map((account) => account.address), - [accounts] - ) - const selector = useCallback( - (state: RouteExecutionState) => - (Object.values(state.routes) as RouteExecution[]) - .filter( - (item) => - accountAddresses.includes(item.route.fromAddress) && - hasEnumFlag(item.status, RouteExecutionStatus.Done) - ) - .sort( - (a, b) => - (b.route.steps[0]?.execution?.startedAt ?? 0) - - (a.route.steps[0]?.execution?.startedAt ?? 0) - ) - .map(({ route }) => route.id), - [accountAddresses] - ) - return useRouteExecutionStore(selector) -} diff --git a/packages/widget/src/stores/routes/useExecutingRoutesIds.ts b/packages/widget/src/stores/routes/useExecutingRoutesIds.ts deleted file mode 100644 index e63fa244e..000000000 --- a/packages/widget/src/stores/routes/useExecutingRoutesIds.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useAccount } from '@lifi/wallet-management' -import { useCallback, useMemo } from 'react' -import { useRouteExecutionStore } from './RouteExecutionStore.js' -import type { RouteExecution, RouteExecutionState } from './types.js' -import { RouteExecutionStatus } from './types.js' - -export const useExecutingRoutesIds = () => { - const { accounts } = useAccount() - const accountAddresses = useMemo( - () => accounts.map((account) => account.address), - [accounts] - ) - const selector = useCallback( - (state: RouteExecutionState) => - (Object.values(state.routes) as RouteExecution[]) - .filter( - (item) => - accountAddresses.includes(item.route.fromAddress) && - (item.status === RouteExecutionStatus.Pending || - item.status === RouteExecutionStatus.Failed) - ) - .sort( - (a, b) => - (b?.route.steps[0].execution?.startedAt ?? 0) - - (a?.route.steps[0].execution?.startedAt ?? 0) - ) - .map(({ route }) => route.id), - [accountAddresses] - ) - return useRouteExecutionStore(selector) -} diff --git a/packages/widget/src/stores/routes/useRouteExecutionIndicators.ts b/packages/widget/src/stores/routes/useRouteExecutionIndicators.ts index 4a23af1d0..10f684029 100644 --- a/packages/widget/src/stores/routes/useRouteExecutionIndicators.ts +++ b/packages/widget/src/stores/routes/useRouteExecutionIndicators.ts @@ -4,35 +4,38 @@ import { useRouteExecutionStore } from './RouteExecutionStore.js' import type { RouteExecution, RouteExecutionState } from './types.js' import { RouteExecutionStatus } from './types.js' +export type RouteExecutionIndicator = 'idle' | 'active' | 'failed' + const isRecentTransaction = (route: RouteExecution): boolean => { const startedAt = route.route.steps[0]?.execution?.startedAt ?? 0 return startedAt > 0 && Date.now() - startedAt < 1000 * 60 * 60 * 24 // 1 day } -export const useRouteExecutionIndicators = () => { +export const useRouteExecutionIndicator = (): RouteExecutionIndicator => { const { accounts } = useAccount() const accountAddresses = useMemo( () => accounts.map((account) => account.address), [accounts] ) const selector = useCallback( - (state: RouteExecutionState) => { + (state: RouteExecutionState): RouteExecutionIndicator => { const routes = Object.values(state.routes) as RouteExecution[] - const ownedRoutes = routes.filter( + const recentOwnedRoutes = routes.filter( (route) => accountAddresses.includes(route.route.fromAddress) && isRecentTransaction(route) ) - return { - hasActiveRoutes: ownedRoutes.some((r) => - [RouteExecutionStatus.Pending, RouteExecutionStatus.Failed].includes( - r.status - ) - ), - hasFailedRoutes: ownedRoutes.some( - (r) => r.status === RouteExecutionStatus.Failed - ), + if ( + recentOwnedRoutes.some((r) => r.status === RouteExecutionStatus.Failed) + ) { + return 'failed' + } + if ( + recentOwnedRoutes.some((r) => r.status === RouteExecutionStatus.Pending) + ) { + return 'active' } + return 'idle' }, [accountAddresses] ) From 78c77ecbf931541753ec1a0a9e6df7d2146d9d35 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Mon, 16 Mar 2026 16:36:51 +0000 Subject: [PATCH 17/22] refactor: update component structure and styles for transaction details and execution progress --- .../components/ActionRow/ActionRow.style.tsx | 2 +- .../AmountInput/AmountInputHeaderBadge.tsx | 6 +- .../ExecutionProgress.tsx | 11 +- .../ExecutionStatusIndicator.tsx | 36 ++++++ .../RouteDetails.style.tsx | 0 .../{Step => RouteCard}/RouteDetails.tsx | 0 .../src/components/RouteCard/RouteToken.tsx | 2 +- .../{Step => RouteCard}/RouteTokens.tsx | 2 +- .../components/Step/StepStatusIndicator.tsx | 31 ----- .../StepActions/StepActions.style.tsx | 3 + .../src/components/Timer/StepStatusTimer.tsx | 12 +- .../TransactionDetailsPage.tsx | 2 +- .../ActiveTransactionItem.tsx | 108 +++++++++++------- .../TransactionHistoryItem.tsx | 2 +- .../ExecutionProgressCards.tsx | 67 ++--------- .../pages/TransactionPage/ReceiptsCard.tsx | 53 +-------- .../pages/TransactionPage/SentToWalletRow.tsx | 59 ++++++++++ .../TransactionPage}/StepActionRow.tsx | 6 +- .../TransactionPage/TransactionReview.tsx | 18 ++- 19 files changed, 208 insertions(+), 212 deletions(-) rename packages/widget/src/components/{Step => ExecutionProgress}/ExecutionProgress.tsx (90%) create mode 100644 packages/widget/src/components/ExecutionProgress/ExecutionStatusIndicator.tsx rename packages/widget/src/components/{Step => RouteCard}/RouteDetails.style.tsx (100%) rename packages/widget/src/components/{Step => RouteCard}/RouteDetails.tsx (100%) rename packages/widget/src/components/{Step => RouteCard}/RouteTokens.tsx (96%) delete mode 100644 packages/widget/src/components/Step/StepStatusIndicator.tsx create mode 100644 packages/widget/src/pages/TransactionPage/SentToWalletRow.tsx rename packages/widget/src/{components/Step => pages/TransactionPage}/StepActionRow.tsx (87%) diff --git a/packages/widget/src/components/ActionRow/ActionRow.style.tsx b/packages/widget/src/components/ActionRow/ActionRow.style.tsx index ed4b5575c..292462fd4 100644 --- a/packages/widget/src/components/ActionRow/ActionRow.style.tsx +++ b/packages/widget/src/components/ActionRow/ActionRow.style.tsx @@ -5,7 +5,7 @@ export const ActionRowContainer = styled(Box)(({ theme }) => ({ alignItems: 'center', gap: theme.spacing(1), padding: theme.spacing(1), - borderRadius: theme.vars.shape.borderRadiusTertiary, + borderRadius: theme.vars.shape.borderRadiusSecondary, backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, })) diff --git a/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx index dec3da0a9..f2e1ae335 100644 --- a/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx @@ -3,6 +3,7 @@ import Wallet from '@mui/icons-material/Wallet' import { Typography } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' +import { useChain } from '../../hooks/useChain.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' import { DisabledUI, HiddenUI } from '../../types/widget.js' @@ -15,8 +16,9 @@ export const AmountInputHeaderBadge: React.FC = () => { const navigate = useNavigate() const { subvariant, subvariantOptions, toAddresses, disabledUI, hiddenUI } = useWidgetConfig() - const { account } = useAccount() - const [toAddress] = useFieldValues('toAddress') + const [toChainId, toAddress] = useFieldValues('toChain', 'toAddress') + const { chain: toChain } = useChain(toChainId) + const { account } = useAccount({ chainType: toChain?.chainType }) const hiddenToAddress = hiddenUI?.includes(HiddenUI.ToAddress) const disabledToAddress = disabledUI?.includes(DisabledUI.ToAddress) diff --git a/packages/widget/src/components/Step/ExecutionProgress.tsx b/packages/widget/src/components/ExecutionProgress/ExecutionProgress.tsx similarity index 90% rename from packages/widget/src/components/Step/ExecutionProgress.tsx rename to packages/widget/src/components/ExecutionProgress/ExecutionProgress.tsx index e32864a56..00bb5cf74 100644 --- a/packages/widget/src/components/Step/ExecutionProgress.tsx +++ b/packages/widget/src/components/ExecutionProgress/ExecutionProgress.tsx @@ -4,7 +4,7 @@ import { useRouteExecutionMessage } from '../../hooks/useRouteExecutionMessage.j import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { RouteExecutionStatus } from '../../stores/routes/types.js' import { hasEnumFlag } from '../../utils/enum.js' -import { StepStatusIndicator } from './StepStatusIndicator.js' +import { ExecutionStatusIndicator } from './ExecutionStatusIndicator.js' export const ExecutionProgress: React.FC<{ route: RouteExtended @@ -16,13 +16,8 @@ export const ExecutionProgress: React.FC<{ contractSecondaryComponent, contractCompactComponent, } = useWidgetConfig() - const lastStep = route.steps.at(-1) const { title, message } = useRouteExecutionMessage(route, status) - if (!lastStep) { - return null - } - const showContractComponent = subvariant === 'custom' && hasEnumFlag(status, RouteExecutionStatus.Done) && @@ -32,10 +27,10 @@ export const ExecutionProgress: React.FC<{ status === RouteExecutionStatus.Done ? feeConfig?._vcComponent : undefined return ( - + {!showContractComponent ? ( - + ) : ( contractCompactComponent || contractSecondaryComponent diff --git a/packages/widget/src/components/ExecutionProgress/ExecutionStatusIndicator.tsx b/packages/widget/src/components/ExecutionProgress/ExecutionStatusIndicator.tsx new file mode 100644 index 000000000..d4ae2cc33 --- /dev/null +++ b/packages/widget/src/components/ExecutionProgress/ExecutionStatusIndicator.tsx @@ -0,0 +1,36 @@ +import type { RouteExtended } from '@lifi/sdk' +import { RouteExecutionStatus } from '../../stores/routes/types.js' +import { hasEnumFlag } from '../../utils/enum.js' +import { IconCircle } from '../IconCircle/IconCircle.js' +import { TimerRing } from '../Timer/StepStatusTimer.js' + +interface ExecutionStatusIndicatorProps { + route: RouteExtended + status: RouteExecutionStatus +} + +export const ExecutionStatusIndicator: React.FC< + ExecutionStatusIndicatorProps +> = ({ route, status }) => { + const step = route.steps.at(-1) + + if (hasEnumFlag(status, RouteExecutionStatus.Done)) { + if ( + hasEnumFlag(status, RouteExecutionStatus.Partial) || + hasEnumFlag(status, RouteExecutionStatus.Refunded) + ) { + return + } + return + } + + if (hasEnumFlag(status, RouteExecutionStatus.Failed)) { + return + } + + if (step?.execution?.status === 'ACTION_REQUIRED') { + return + } + + return +} diff --git a/packages/widget/src/components/Step/RouteDetails.style.tsx b/packages/widget/src/components/RouteCard/RouteDetails.style.tsx similarity index 100% rename from packages/widget/src/components/Step/RouteDetails.style.tsx rename to packages/widget/src/components/RouteCard/RouteDetails.style.tsx diff --git a/packages/widget/src/components/Step/RouteDetails.tsx b/packages/widget/src/components/RouteCard/RouteDetails.tsx similarity index 100% rename from packages/widget/src/components/Step/RouteDetails.tsx rename to packages/widget/src/components/RouteCard/RouteDetails.tsx diff --git a/packages/widget/src/components/RouteCard/RouteToken.tsx b/packages/widget/src/components/RouteCard/RouteToken.tsx index aa8c1cf46..803049089 100644 --- a/packages/widget/src/components/RouteCard/RouteToken.tsx +++ b/packages/widget/src/components/RouteCard/RouteToken.tsx @@ -6,8 +6,8 @@ import { type MouseEventHandler, useState } from 'react' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { HiddenUI } from '../../types/widget.js' import { CardIconButton } from '../Card/CardIconButton.js' -import { RouteDetails } from '../Step/RouteDetails.js' import { Token } from '../Token/Token.js' +import { RouteDetails } from './RouteDetails.js' import { TokenContainer } from './RouteToken.style.js' interface RouteTokenProps { diff --git a/packages/widget/src/components/Step/RouteTokens.tsx b/packages/widget/src/components/RouteCard/RouteTokens.tsx similarity index 96% rename from packages/widget/src/components/Step/RouteTokens.tsx rename to packages/widget/src/components/RouteCard/RouteTokens.tsx index ee2fb5d5a..f994e02b3 100644 --- a/packages/widget/src/components/Step/RouteTokens.tsx +++ b/packages/widget/src/components/RouteCard/RouteTokens.tsx @@ -2,8 +2,8 @@ import type { RouteExtended } from '@lifi/sdk' import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward' import { Box } from '@mui/material' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' -import { RouteToken } from '../RouteCard/RouteToken.js' import { Token } from '../Token/Token.js' +import { RouteToken } from './RouteToken.js' export const RouteTokens: React.FC<{ route: RouteExtended diff --git a/packages/widget/src/components/Step/StepStatusIndicator.tsx b/packages/widget/src/components/Step/StepStatusIndicator.tsx deleted file mode 100644 index ca05fd3f5..000000000 --- a/packages/widget/src/components/Step/StepStatusIndicator.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { LiFiStepExtended } from '@lifi/sdk' -import { IconCircle } from '../IconCircle/IconCircle.js' -import { TimerRing } from '../Timer/StepStatusTimer.js' - -interface StepStatusIndicatorProps { - step: LiFiStepExtended -} - -export const StepStatusIndicator: React.FC = ({ - step, -}) => { - const lastAction = step.execution?.actions?.at(-1) - - const status = step.execution?.status || 'PENDING' - const substatus = lastAction?.substatus - - switch (status) { - case 'PENDING': { - return - } - case 'ACTION_REQUIRED': - return - case 'DONE': - if (substatus === 'PARTIAL' || substatus === 'REFUNDED') { - return - } - return - case 'FAILED': - return - } -} diff --git a/packages/widget/src/components/StepActions/StepActions.style.tsx b/packages/widget/src/components/StepActions/StepActions.style.tsx index 4b6eaf941..b709af653 100644 --- a/packages/widget/src/components/StepActions/StepActions.style.tsx +++ b/packages/widget/src/components/StepActions/StepActions.style.tsx @@ -67,6 +67,9 @@ export const StepContent = styled(Box, { style: { borderLeft: 'none', paddingLeft: theme.spacing(4.625), + ...theme.applyStyles('dark', { + borderLeft: 'none', + }), }, }, ], diff --git a/packages/widget/src/components/Timer/StepStatusTimer.tsx b/packages/widget/src/components/Timer/StepStatusTimer.tsx index 5765eede0..850f761e2 100644 --- a/packages/widget/src/components/Timer/StepStatusTimer.tsx +++ b/packages/widget/src/components/Timer/StepStatusTimer.tsx @@ -13,16 +13,16 @@ import { TimerLabel, } from './StepStatusTimer.style.js' -function getExpiryTimestamp(step: LiFiStepExtended): Date { - const { signedAt } = step.execution ?? {} - if (!signedAt) { +function getExpiryTimestamp(step?: LiFiStepExtended): Date { + const { signedAt } = step?.execution ?? {} + if (!step || !signedAt) { return new Date() } return new Date(signedAt + step.estimate.executionDuration * 1000) } interface TimerRingProps { - step: LiFiStepExtended + step?: LiFiStepExtended size?: number thickness?: number showLabel?: boolean @@ -37,8 +37,8 @@ export const TimerRing: React.FC = ({ const { i18n } = useTranslation() const [isExpired, setExpired] = useState(false) - const signedAt = step.execution?.signedAt - const totalDuration = step.estimate.executionDuration * 1000 + const signedAt = step?.execution?.signedAt + const totalDuration = (step?.estimate.executionDuration ?? 0) * 1000 const expiryTimestamp = getExpiryTimestamp(step) const { days, hours, minutes, seconds } = useTimer({ diff --git a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx index d0f4c120d..95a0ee800 100644 --- a/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx +++ b/packages/widget/src/pages/TransactionDetailsPage/TransactionDetailsPage.tsx @@ -7,7 +7,7 @@ import { Card } from '../../components/Card/Card.js' import { ContractComponent } from '../../components/ContractComponent/ContractComponent.js' import { DateLabel } from '../../components/DateLabel/DateLabel.js' import { PageContainer } from '../../components/PageContainer.js' -import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { RouteTokens } from '../../components/RouteCard/RouteTokens.js' import { internalExplorerUrl } from '../../config/constants.js' import { useExplorer } from '../../hooks/useExplorer.js' import { useHeader } from '../../hooks/useHeader.js' diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.tsx b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.tsx index 6733cf3f9..59afaf9e8 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.tsx +++ b/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.tsx @@ -1,4 +1,4 @@ -import type { LiFiStepExtended } from '@lifi/sdk' +import type { LiFiStepExtended, RouteExtended } from '@lifi/sdk' import DeleteOutline from '@mui/icons-material/DeleteOutline' import { Box } from '@mui/material' import { useNavigate } from '@tanstack/react-router' @@ -8,68 +8,83 @@ import { useTranslation } from 'react-i18next' import { ActionRow } from '../../components/ActionRow/ActionRow.js' import { Card } from '../../components/Card/Card.js' import { IconCircle } from '../../components/IconCircle/IconCircle.js' -import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { RouteTokens } from '../../components/RouteCard/RouteTokens.js' import { TimerRing } from '../../components/Timer/StepStatusTimer.js' import { StepTimer } from '../../components/Timer/StepTimer.js' -import { useActionMessage } from '../../hooks/useActionMessage.js' import { useRouteExecution } from '../../hooks/useRouteExecution.js' +import { useRouteExecutionMessage } from '../../hooks/useRouteExecutionMessage.js' import { RouteExecutionStatus } from '../../stores/routes/types.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { DeleteButton, RetryButton } from './ActiveTransactionItem.style.js' export const ActiveTransactionItem: React.FC<{ routeId: string }> = memo( ({ routeId }) => { - const navigate = useNavigate() const { route, status, restartRoute, deleteRoute } = useRouteExecution({ routeId, executeInBackground: true, }) - const lastStep = route?.steps.findLast((step) => step.execution) - const lastAction = lastStep?.execution?.actions?.at(-1) - const { title } = useActionMessage(lastStep, lastAction) - if (!route) { return null } - const isFailed = status === RouteExecutionStatus.Failed + return ( + + ) + } +) - const handleClick = () => { - navigate({ - to: navigationRoutes.transactionExecution, - search: { routeId }, - }) - } +const ActiveTransactionItemContent: React.FC<{ + route: RouteExtended + status: RouteExecutionStatus | undefined + restartRoute: () => void + deleteRoute: () => void +}> = ({ route, status, restartRoute, deleteRoute }) => { + const navigate = useNavigate() + const lastStep = route.steps.at(-1) + const { title } = useRouteExecutionMessage( + route, + status ?? RouteExecutionStatus.Pending + ) - const handleDelete = (e: MouseEvent) => { - e.stopPropagation() - deleteRoute() - } + const handleClick = () => { + navigate({ + to: navigationRoutes.transactionExecution, + search: { routeId: route.id }, + }) + } - const handleRetry = () => { - // NB: Do not stop propagation here: - // open the transaction execution page and retry the transaction simultaneously - restartRoute() - } + const handleDelete = (e: MouseEvent) => { + e.stopPropagation() + deleteRoute() + } - return ( - - - {isFailed ? ( - - ) : ( - - )} - - - - ) + const handleRetry = () => { + // NB: Do not stop propagation here: + // open the transaction execution page and retry the transaction simultaneously + restartRoute() } -) + + const isFailed = status === RouteExecutionStatus.Failed + + return ( + + + {isFailed ? ( + + ) : ( + + )} + + + + ) +} const FailedTransactionRow: React.FC<{ onRetry: () => void @@ -98,9 +113,20 @@ const PendingTransactionRow: React.FC<{ step: LiFiStepExtended | undefined title: string | undefined }> = ({ step, title }) => { - if (!title) { + if (!title || step?.execution?.status === 'DONE') { return null } + + const isActionRequired = step?.execution?.status === 'ACTION_REQUIRED' + if (isActionRequired) { + return ( + } + message={title} + /> + ) + } + return ( = ({ route, status, }) => { - const { t } = useTranslation() const { feeConfig } = useWidgetConfig() - const { getAddressLink } = useExplorer() const isDone = hasEnumFlag(status, RouteExecutionStatus.Done) const toAddress = isDone ? route.toAddress : undefined const VcComponent = status === RouteExecutionStatus.Done ? feeConfig?._vcComponent : undefined - const handleCopy = (e: MouseEvent) => { - e.stopPropagation() - if (toAddress) { - navigator.clipboard.writeText(toAddress) - } - } - - const addressLink = toAddress - ? getAddressLink(toAddress, route.toChainId) - : undefined - return ( - + {route.steps.map((step) => ( @@ -68,35 +47,9 @@ export const ExecutionProgressCards: React.FC = ({ ))} {toAddress ? ( - - - - } - message={t('main.sentToWallet', { - address: shortenAddress(toAddress), - })} - endAdornment={ - <> - - - - {addressLink ? ( - - - - ) : undefined} - - } + ) : undefined} diff --git a/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx b/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx index a76b62c3a..56eb9d54d 100644 --- a/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx +++ b/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx @@ -1,19 +1,11 @@ import type { RouteExtended } from '@lifi/sdk' -import ContentCopyRounded from '@mui/icons-material/ContentCopyRounded' -import OpenInNew from '@mui/icons-material/OpenInNew' -import Wallet from '@mui/icons-material/Wallet' -import { IconButton } from '@mui/material' -import type { MouseEvent } from 'react' import { useTranslation } from 'react-i18next' -import { ActionRow } from '../../components/ActionRow/ActionRow.js' -import { ActionIconCircle } from '../../components/ActionRow/ActionRow.style.js' import { Card } from '../../components/Card/Card.js' import { CardTitle } from '../../components/Card/CardTitle.js' -import { StepActionRow } from '../../components/Step/StepActionRow.js' -import { useExplorer } from '../../hooks/useExplorer.js' import { prepareActions } from '../../utils/prepareActions.js' -import { shortenAddress } from '../../utils/wallet.js' -import { ExternalLink, TransactionList } from './ReceiptsCard.style.js' +import { TransactionList } from './ReceiptsCard.style.js' +import { SentToWalletRow } from './SentToWalletRow.js' +import { StepActionRow } from './StepActionRow.js' interface ReceiptsCardProps { route: RouteExtended @@ -21,20 +13,8 @@ interface ReceiptsCardProps { export const ReceiptsCard = ({ route }: ReceiptsCardProps) => { const { t } = useTranslation() - const { getAddressLink } = useExplorer() const toAddress = route.toAddress - const handleCopy = (e: MouseEvent) => { - e.stopPropagation() - if (toAddress) { - navigator.clipboard.writeText(toAddress) - } - } - - const addressLink = toAddress - ? getAddressLink(toAddress, route.toChainId) - : undefined - return ( {t('main.receipts')} @@ -53,32 +33,7 @@ export const ReceiptsCard = ({ route }: ReceiptsCardProps) => { ))} {toAddress ? ( - - - - } - message={t('main.sentToWallet', { - address: shortenAddress(toAddress), - })} - endAdornment={ - <> - - - - {addressLink ? ( - - - - ) : undefined} - - } - /> + ) : undefined} diff --git a/packages/widget/src/pages/TransactionPage/SentToWalletRow.tsx b/packages/widget/src/pages/TransactionPage/SentToWalletRow.tsx new file mode 100644 index 000000000..d28ac4d6b --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/SentToWalletRow.tsx @@ -0,0 +1,59 @@ +import ContentCopyRounded from '@mui/icons-material/ContentCopyRounded' +import OpenInNew from '@mui/icons-material/OpenInNew' +import Wallet from '@mui/icons-material/Wallet' +import { Box, IconButton } from '@mui/material' +import type { MouseEvent } from 'react' +import { useTranslation } from 'react-i18next' +import { ActionRow } from '../../components/ActionRow/ActionRow.js' +import { ActionIconCircle } from '../../components/ActionRow/ActionRow.style.js' +import { useExplorer } from '../../hooks/useExplorer.js' +import { shortenAddress } from '../../utils/wallet.js' +import { ExternalLink } from './ReceiptsCard.style.js' + +interface SentToWalletRowProps { + toAddress: string + toChainId: number +} + +export const SentToWalletRow: React.FC = ({ + toAddress, + toChainId, +}) => { + const { t } = useTranslation() + const { getAddressLink } = useExplorer() + const addressLink = getAddressLink(toAddress, toChainId) + + const handleCopy = (e: MouseEvent) => { + e.stopPropagation() + navigator.clipboard.writeText(toAddress) + } + + return ( + + + + } + message={t('main.sentToWallet', { + address: shortenAddress(toAddress), + })} + endAdornment={ + + + + + {addressLink ? ( + + + + ) : undefined} + + } + /> + ) +} diff --git a/packages/widget/src/components/Step/StepActionRow.tsx b/packages/widget/src/pages/TransactionPage/StepActionRow.tsx similarity index 87% rename from packages/widget/src/components/Step/StepActionRow.tsx rename to packages/widget/src/pages/TransactionPage/StepActionRow.tsx index ebe6c71b7..fc328abe8 100644 --- a/packages/widget/src/components/Step/StepActionRow.tsx +++ b/packages/widget/src/pages/TransactionPage/StepActionRow.tsx @@ -1,11 +1,11 @@ import type { ExecutionAction, LiFiStepExtended } from '@lifi/sdk' import OpenInNew from '@mui/icons-material/OpenInNew' import type React from 'react' +import { ActionRow } from '../../components/ActionRow/ActionRow.js' +import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { useActionMessage } from '../../hooks/useActionMessage.js' import { useExplorer } from '../../hooks/useExplorer.js' -import { ExternalLink } from '../../pages/TransactionPage/ReceiptsCard.style.js' -import { ActionRow } from '../ActionRow/ActionRow.js' -import { IconCircle } from '../IconCircle/IconCircle.js' +import { ExternalLink } from './ReceiptsCard.style.js' export const StepActionRow: React.FC<{ step: LiFiStepExtended diff --git a/packages/widget/src/pages/TransactionPage/TransactionReview.tsx b/packages/widget/src/pages/TransactionPage/TransactionReview.tsx index f77f728a7..e4fee279b 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionReview.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionReview.tsx @@ -7,7 +7,7 @@ import type { BottomSheetBase } from '../../components/BottomSheet/types.js' import { Card } from '../../components/Card/Card.js' import { WarningMessages } from '../../components/Messages/WarningMessages.js' import { RouteCardEssentials } from '../../components/RouteCard/RouteCardEssentials.js' -import { RouteTokens } from '../../components/Step/RouteTokens.js' +import { RouteTokens } from '../../components/RouteCard/RouteTokens.js' import { useAddressActivity } from '../../hooks/useAddressActivity.js' import { useWidgetEvents } from '../../hooks/useWidgetEvents.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' @@ -134,15 +134,13 @@ export const TransactionReview: React.FC = ({ - - - - + + {subvariant !== 'custom' ? ( Date: Wed, 18 Mar 2026 11:27:29 +0000 Subject: [PATCH 18/22] refactor: AmountInputHeaderBadge logic and add StepActionsList --- .../AmountInput/AmountInputHeaderBadge.tsx | 11 ++---- .../src/components/Timer/StepStatusTimer.tsx | 10 +----- .../widget/src/components/Timer/StepTimer.tsx | 13 +------ .../ExecutionProgressCards.tsx | 33 +++++------------- .../pages/TransactionPage/ReceiptsCard.tsx | 24 ++----------- .../pages/TransactionPage/StepActionsList.tsx | 34 +++++++++++++++++++ packages/widget/src/utils/timer.ts | 10 ++++++ 7 files changed, 58 insertions(+), 77 deletions(-) create mode 100644 packages/widget/src/pages/TransactionPage/StepActionsList.tsx diff --git a/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx index f2e1ae335..56e7e7209 100644 --- a/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx @@ -1,9 +1,7 @@ -import { useAccount } from '@lifi/wallet-management' import Wallet from '@mui/icons-material/Wallet' import { Typography } from '@mui/material' import { useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' -import { useChain } from '../../hooks/useChain.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' import { DisabledUI, HiddenUI } from '../../types/widget.js' @@ -16,16 +14,11 @@ export const AmountInputHeaderBadge: React.FC = () => { const navigate = useNavigate() const { subvariant, subvariantOptions, toAddresses, disabledUI, hiddenUI } = useWidgetConfig() - const [toChainId, toAddress] = useFieldValues('toChain', 'toAddress') - const { chain: toChain } = useChain(toChainId) - const { account } = useAccount({ chainType: toChain?.chainType }) + const [toAddress] = useFieldValues('toAddress') const hiddenToAddress = hiddenUI?.includes(HiddenUI.ToAddress) const disabledToAddress = disabledUI?.includes(DisabledUI.ToAddress) - const showWalletBadge = - !hiddenToAddress && - !(disabledToAddress && !toAddress) && - account.isConnected + const showWalletBadge = !hiddenToAddress && !(disabledToAddress && !toAddress) if (!showWalletBadge) { return null diff --git a/packages/widget/src/components/Timer/StepStatusTimer.tsx b/packages/widget/src/components/Timer/StepStatusTimer.tsx index 850f761e2..44c34118c 100644 --- a/packages/widget/src/components/Timer/StepStatusTimer.tsx +++ b/packages/widget/src/components/Timer/StepStatusTimer.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLoopProgress } from '../../hooks/timer/useLoopProgress.js' import { useTimer } from '../../hooks/timer/useTimer.js' -import { formatTimer } from '../../utils/timer.js' +import { formatTimer, getExpiryTimestamp } from '../../utils/timer.js' import { iconCircleSize } from '../IconCircle/IconCircle.style.js' import { ProgressFill, @@ -13,14 +13,6 @@ import { TimerLabel, } from './StepStatusTimer.style.js' -function getExpiryTimestamp(step?: LiFiStepExtended): Date { - const { signedAt } = step?.execution ?? {} - if (!step || !signedAt) { - return new Date() - } - return new Date(signedAt + step.estimate.executionDuration * 1000) -} - interface TimerRingProps { step?: LiFiStepExtended size?: number diff --git a/packages/widget/src/components/Timer/StepTimer.tsx b/packages/widget/src/components/Timer/StepTimer.tsx index 2c79b29d3..9d9e9eb97 100644 --- a/packages/widget/src/components/Timer/StepTimer.tsx +++ b/packages/widget/src/components/Timer/StepTimer.tsx @@ -3,18 +3,7 @@ import { Typography } from '@mui/material' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useTimer } from '../../hooks/timer/useTimer.js' -import { formatTimer } from '../../utils/timer.js' - -const getExpiryTimestamp = (step: LiFiStepExtended) => { - const execution = step?.execution - if (!execution) { - return new Date() - } - const expiry = new Date( - (execution.signedAt ?? Date.now()) + step.estimate.executionDuration * 1000 - ) - return expiry -} +import { formatTimer, getExpiryTimestamp } from '../../utils/timer.js' export const StepTimer: React.FC<{ step: LiFiStepExtended diff --git a/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx index 75a7bc465..f4ce8c996 100644 --- a/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx +++ b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx @@ -6,11 +6,8 @@ import { RouteTokens } from '../../components/RouteCard/RouteTokens.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { RouteExecutionStatus } from '../../stores/routes/types.js' import { hasEnumFlag } from '../../utils/enum.js' -import { prepareActions } from '../../utils/prepareActions.js' import { ExecutionDoneCard } from './ExecutionDoneCard.js' -import { TransactionList } from './ReceiptsCard.style.js' -import { SentToWalletRow } from './SentToWalletRow.js' -import { StepActionRow } from './StepActionRow.js' +import { StepActionsList } from './StepActionsList.js' interface ExecutionProgressCardsProps { route: RouteExtended @@ -24,6 +21,10 @@ export const ExecutionProgressCards: React.FC = ({ const { feeConfig } = useWidgetConfig() const isDone = hasEnumFlag(status, RouteExecutionStatus.Done) const toAddress = isDone ? route.toAddress : undefined + const hasActions = route.steps.some( + (step) => (step.execution?.actions?.length ?? 0) > 0 + ) + const hasListItems = hasActions || !!toAddress const VcComponent = status === RouteExecutionStatus.Done ? feeConfig?._vcComponent : undefined @@ -32,27 +33,9 @@ export const ExecutionProgressCards: React.FC = ({ - - {route.steps.map((step) => ( - - {prepareActions(step.execution?.actions ?? []).map( - (actionsGroup, index) => ( - - ) - )} - - ))} - {toAddress ? ( - - ) : undefined} - + {hasListItems ? ( + + ) : null} {isDone ? ( diff --git a/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx b/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx index 56eb9d54d..9d1670888 100644 --- a/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx +++ b/packages/widget/src/pages/TransactionPage/ReceiptsCard.tsx @@ -2,10 +2,7 @@ import type { RouteExtended } from '@lifi/sdk' import { useTranslation } from 'react-i18next' import { Card } from '../../components/Card/Card.js' import { CardTitle } from '../../components/Card/CardTitle.js' -import { prepareActions } from '../../utils/prepareActions.js' -import { TransactionList } from './ReceiptsCard.style.js' -import { SentToWalletRow } from './SentToWalletRow.js' -import { StepActionRow } from './StepActionRow.js' +import { StepActionsList } from './StepActionsList.js' interface ReceiptsCardProps { route: RouteExtended @@ -18,24 +15,7 @@ export const ReceiptsCard = ({ route }: ReceiptsCardProps) => { return ( {t('main.receipts')} - - {route.steps.map((step) => ( - - {prepareActions(step.execution?.actions ?? []).map( - (actionsGroup, index) => ( - - ) - )} - - ))} - {toAddress ? ( - - ) : undefined} - + ) } diff --git a/packages/widget/src/pages/TransactionPage/StepActionsList.tsx b/packages/widget/src/pages/TransactionPage/StepActionsList.tsx new file mode 100644 index 000000000..f5173db44 --- /dev/null +++ b/packages/widget/src/pages/TransactionPage/StepActionsList.tsx @@ -0,0 +1,34 @@ +import type { RouteExtended } from '@lifi/sdk' +import { prepareActions } from '../../utils/prepareActions.js' +import { TransactionList } from './ReceiptsCard.style.js' +import { SentToWalletRow } from './SentToWalletRow.js' +import { StepActionRow } from './StepActionRow.js' + +interface StepActionsListProps { + route: RouteExtended + toAddress?: string +} + +export const StepActionsList: React.FC = ({ + route, + toAddress, +}) => ( + + {route.steps.map((step) => ( + + {prepareActions(step.execution?.actions ?? []).map( + (actionsGroup, index) => ( + + ) + )} + + ))} + {toAddress ? ( + + ) : null} + +) diff --git a/packages/widget/src/utils/timer.ts b/packages/widget/src/utils/timer.ts index b70386333..61b849e8a 100644 --- a/packages/widget/src/utils/timer.ts +++ b/packages/widget/src/utils/timer.ts @@ -1,3 +1,13 @@ +import type { LiFiStepExtended } from '@lifi/sdk' + +export function getExpiryTimestamp(step?: LiFiStepExtended): Date { + const { signedAt } = step?.execution ?? {} + if (!step || !signedAt) { + return new Date() + } + return new Date(signedAt + step.estimate.executionDuration * 1000) +} + export const formatTimer = ({ days = 0, hours = 0, From a1b8badd74ed37dff0a4906ce7d697d9cfd5cb63 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Wed, 18 Mar 2026 15:23:00 +0000 Subject: [PATCH 19/22] feat: include route essentials into expansion --- .../src/components/RouteCard/RouteCard.tsx | 3 +- .../RouteCard/RouteCardEssentials.style.tsx | 36 +++++++++ .../RouteCard/RouteCardEssentials.tsx | 73 ++++++------------- .../RouteCard/RouteDetails.style.tsx | 15 ++-- .../src/components/RouteCard/RouteDetails.tsx | 56 +++++++++++--- .../src/components/RouteCard/RouteToken.tsx | 8 ++ .../src/components/RouteCard/RouteTokens.tsx | 4 +- .../StepActions/StepActions.style.tsx | 15 +++- .../components/StepActions/StepActions.tsx | 27 ++----- .../components/TokenRate/TokenRate.style.ts | 17 ----- .../useTokenRateText.ts} | 28 +++---- packages/widget/src/i18n/en.json | 3 + .../TransactionPage/TransactionReview.tsx | 4 +- 13 files changed, 161 insertions(+), 128 deletions(-) create mode 100644 packages/widget/src/components/RouteCard/RouteCardEssentials.style.tsx delete mode 100644 packages/widget/src/components/TokenRate/TokenRate.style.ts rename packages/widget/src/{components/TokenRate/TokenRate.tsx => hooks/useTokenRateText.ts} (71%) diff --git a/packages/widget/src/components/RouteCard/RouteCard.tsx b/packages/widget/src/components/RouteCard/RouteCard.tsx index c96851f1f..ed810a8eb 100644 --- a/packages/widget/src/components/RouteCard/RouteCard.tsx +++ b/packages/widget/src/components/RouteCard/RouteCard.tsx @@ -9,7 +9,6 @@ import type { CardProps } from '../Card/Card.js' import { Card } from '../Card/Card.js' import { CardLabel, CardLabelTypography } from '../Card/CardLabel.js' import { getMatchingLabels } from './getMatchingLabels.js' -import { RouteCardEssentials } from './RouteCardEssentials.js' import { RouteToken } from './RouteToken.js' import type { RouteCardProps } from './types.js' @@ -111,8 +110,8 @@ export const RouteCard: React.FC< token={token} impactToken={impactToken} defaultExpanded={defaultExpanded} + showEssentials /> - ) diff --git a/packages/widget/src/components/RouteCard/RouteCardEssentials.style.tsx b/packages/widget/src/components/RouteCard/RouteCardEssentials.style.tsx new file mode 100644 index 000000000..e5ad43534 --- /dev/null +++ b/packages/widget/src/components/RouteCard/RouteCardEssentials.style.tsx @@ -0,0 +1,36 @@ +import { Box, styled, Typography } from '@mui/material' + +export const EssentialsContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + flex: 1, + marginTop: theme.spacing(2), + gap: theme.spacing(1.5), +})) + +export const EssentialsRateTypography = styled(Typography)(({ theme }) => ({ + fontSize: 14, + fontWeight: 500, + color: theme.vars.palette.text.secondary, + cursor: 'pointer', + '&:hover': { + color: theme.vars.palette.text.primary, + }, + transition: theme.transitions.create(['color'], { + duration: theme.transitions.duration.enteringScreen, + easing: theme.transitions.easing.easeOut, + }), +})) + +export const EssentialsValueTypography = styled(Typography)(({ theme }) => ({ + fontSize: 14, + color: theme.vars.palette.text.primary, + fontWeight: 600, +})) + +export const EssentialsIconValueContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.75), +})) diff --git a/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx b/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx index ef193841e..78880ede4 100644 --- a/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx +++ b/packages/widget/src/components/RouteCard/RouteCardEssentials.tsx @@ -1,12 +1,18 @@ import AccessTimeFilled from '@mui/icons-material/AccessTimeFilled' import LocalGasStationRounded from '@mui/icons-material/LocalGasStationRounded' -import { Box, Tooltip, Typography } from '@mui/material' +import { Box, Tooltip } from '@mui/material' import { useTranslation } from 'react-i18next' +import { useTokenRateText } from '../../hooks/useTokenRateText.js' import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js' import { formatDuration } from '../../utils/format.js' import { FeeBreakdownTooltip } from '../FeeBreakdownTooltip.js' import { IconTypography } from '../IconTypography.js' -import { TokenRate } from '../TokenRate/TokenRate.js' +import { + EssentialsContainer, + EssentialsIconValueContainer, + EssentialsRateTypography, + EssentialsValueTypography, +} from './RouteCardEssentials.style.js' import type { RouteCardEssentialsProps } from './types.js' export const RouteCardEssentials: React.FC = ({ @@ -14,6 +20,7 @@ export const RouteCardEssentials: React.FC = ({ showDuration = true, }) => { const { t, i18n } = useTranslation() + const { rateText, toggleRate } = useTokenRateText(route) const executionTimeSeconds = Math.floor( route.steps.reduce( (duration, step) => duration + step.estimate.executionDuration, @@ -24,76 +31,44 @@ export const RouteCardEssentials: React.FC = ({ const { gasCosts, feeCosts, combinedFeesUSD } = getAccumulatedFeeCostsBreakdown(route) return ( - - + + + + {rateText} + + - + - + {!combinedFeesUSD ? t('main.fees.free') : t('format.currency', { value: combinedFeesUSD, })} - - + + {showDuration && ( - + - + {formatDuration(executionTimeSeconds, i18n.language)} - - + + )} - + ) } diff --git a/packages/widget/src/components/RouteCard/RouteDetails.style.tsx b/packages/widget/src/components/RouteCard/RouteDetails.style.tsx index e2c570d45..4db63665d 100644 --- a/packages/widget/src/components/RouteCard/RouteDetails.style.tsx +++ b/packages/widget/src/components/RouteCard/RouteDetails.style.tsx @@ -1,3 +1,4 @@ +import InfoOutlined from '@mui/icons-material/InfoOutlined' import { Box, styled, Typography } from '@mui/material' export const DetailRow = styled(Box)(({ theme }) => ({ @@ -14,20 +15,20 @@ export const DetailLabelContainer = styled(Box)(({ theme }) => ({ })) export const DetailLabel = styled(Typography)(({ theme }) => ({ - fontSize: 12, + fontSize: 14, fontWeight: 500, - lineHeight: '16px', + lineHeight: 1.3334, color: theme.vars.palette.text.secondary, })) export const DetailValue = styled(Typography)(() => ({ - fontSize: 12, + fontSize: 14, fontWeight: 700, - lineHeight: '16px', + lineHeight: 1.3334, textAlign: 'right', })) -export const DetailInfoIcon = { +export const DetailInfoIcon = styled(InfoOutlined)(({ theme }) => ({ fontSize: 16, - color: 'text.secondary', -} as const + color: theme.vars.palette.text.secondary, +})) diff --git a/packages/widget/src/components/RouteCard/RouteDetails.tsx b/packages/widget/src/components/RouteCard/RouteDetails.tsx index bd2da963b..d2c64a447 100644 --- a/packages/widget/src/components/RouteCard/RouteDetails.tsx +++ b/packages/widget/src/components/RouteCard/RouteDetails.tsx @@ -1,12 +1,16 @@ import type { RouteExtended } from '@lifi/sdk' import { useEthereumContext } from '@lifi/widget-provider' -import InfoOutlined from '@mui/icons-material/InfoOutlined' import { Box, Tooltip } from '@mui/material' import { useTranslation } from 'react-i18next' +import { useTokenRateText } from '../../hooks/useTokenRateText.js' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' import { isRouteDone } from '../../stores/routes/utils.js' import { getAccumulatedFeeCostsBreakdown } from '../../utils/fees.js' -import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js' +import { + formatDuration, + formatTokenAmount, + formatTokenPrice, +} from '../../utils/format.js' import { getPriceImpact } from '../../utils/getPriceImpact.js' import { FeeBreakdownTooltip } from '../FeeBreakdownTooltip.js' import { StepActions } from '../StepActions/StepActions.js' @@ -23,7 +27,8 @@ interface RouteDetailsProps { } export const RouteDetails = ({ route }: RouteDetailsProps) => { - const { t } = useTranslation() + const { t, i18n } = useTranslation() + const { rateText, toggleRate } = useTokenRateText(route) const { feeConfig } = useWidgetConfig() @@ -65,6 +70,13 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { ) ?? 0 } + const executionTimeSeconds = Math.floor( + route.steps.reduce( + (duration, step) => duration + step.estimate.executionDuration, + 0 + ) + ) + const hasGaslessSupport = route.steps.every((step) => isGaslessStep?.(step)) const showIntegratorFeeCollectionDetails = @@ -79,7 +91,7 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { {t('main.fees.network')} - + @@ -95,7 +107,7 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { {t('main.fees.provider')} - + @@ -122,7 +134,7 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { t('tooltip.feeCollection', { tool: feeConfig.name }) } > - + ) : null} @@ -137,7 +149,7 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { {t('main.priceImpact')} - + @@ -153,7 +165,7 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { {t('main.maxSlippage')} - + @@ -168,7 +180,7 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { {t('main.minReceived')} - + @@ -183,6 +195,32 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { ) : null} + + + {t('main.exchangeRate')} + + + + + + {rateText} + + + + + {t('main.estimatedTime')} + + + + + + {formatDuration(executionTimeSeconds, i18n.language)} + + ) } diff --git a/packages/widget/src/components/RouteCard/RouteToken.tsx b/packages/widget/src/components/RouteCard/RouteToken.tsx index 803049089..df2652804 100644 --- a/packages/widget/src/components/RouteCard/RouteToken.tsx +++ b/packages/widget/src/components/RouteCard/RouteToken.tsx @@ -7,6 +7,7 @@ import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.j import { HiddenUI } from '../../types/widget.js' import { CardIconButton } from '../Card/CardIconButton.js' import { Token } from '../Token/Token.js' +import { RouteCardEssentials } from './RouteCardEssentials.js' import { RouteDetails } from './RouteDetails.js' import { TokenContainer } from './RouteToken.style.js' @@ -15,6 +16,7 @@ interface RouteTokenProps { token: TokenAmount impactToken?: TokenAmount defaultExpanded?: boolean + showEssentials?: boolean } export const RouteToken = ({ @@ -22,6 +24,7 @@ export const RouteToken = ({ token, impactToken, defaultExpanded, + showEssentials, }: RouteTokenProps) => { const { hiddenUI } = useWidgetConfig() @@ -57,6 +60,11 @@ export const RouteToken = ({ + {showEssentials ? ( + + + + ) : null} ) } diff --git a/packages/widget/src/components/RouteCard/RouteTokens.tsx b/packages/widget/src/components/RouteCard/RouteTokens.tsx index f994e02b3..67835b250 100644 --- a/packages/widget/src/components/RouteCard/RouteTokens.tsx +++ b/packages/widget/src/components/RouteCard/RouteTokens.tsx @@ -7,7 +7,8 @@ import { RouteToken } from './RouteToken.js' export const RouteTokens: React.FC<{ route: RouteExtended -}> = ({ route }) => { + showEssentials?: boolean +}> = ({ route, showEssentials }) => { const { subvariant, defaultUI } = useWidgetConfig() const fromToken = { @@ -46,6 +47,7 @@ export const RouteTokens: React.FC<{ token={toToken} impactToken={fromToken} defaultExpanded={defaultUI?.transactionDetailsExpanded} + showEssentials={showEssentials} /> ) : null} diff --git a/packages/widget/src/components/StepActions/StepActions.style.tsx b/packages/widget/src/components/StepActions/StepActions.style.tsx index b709af653..a6fe04dfc 100644 --- a/packages/widget/src/components/StepActions/StepActions.style.tsx +++ b/packages/widget/src/components/StepActions/StepActions.style.tsx @@ -31,8 +31,8 @@ export const StepLabel = styled(MuiStepLabel, { padding: 0, alignItems: 'center', [`.${stepLabelClasses.iconContainer}`]: { - paddingLeft: theme.spacing(1.25), - paddingRight: theme.spacing(3.25), + paddingLeft: theme.spacing(0.5), + paddingRight: theme.spacing(2.5), }, [`.${stepLabelClasses.labelContainer}`]: { minHeight: 24, @@ -79,3 +79,14 @@ export const StepAvatar = styled(AvatarMasked)(({ theme }) => ({ color: theme.vars.palette.text.primary, backgroundColor: 'transparent', })) + +export const StepActionsHeader = styled(Box)(() => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', +})) + +export const StepActionsTitle = styled(Typography)(() => ({ + fontSize: 14, + fontWeight: 700, +})) diff --git a/packages/widget/src/components/StepActions/StepActions.tsx b/packages/widget/src/components/StepActions/StepActions.tsx index 2de2ed850..07ae3bd14 100644 --- a/packages/widget/src/components/StepActions/StepActions.tsx +++ b/packages/widget/src/components/StepActions/StepActions.tsx @@ -22,6 +22,8 @@ import { formatTokenAmount, formatTokenPrice } from '../../utils/format.js' import { SmallAvatar } from '../Avatar/SmallAvatar.js' import { CardIconButton } from '../Card/CardIconButton.js' import { + StepActionsHeader, + StepActionsTitle, StepConnector, StepContent, StepLabel, @@ -43,22 +45,9 @@ export const StepActions: React.FC<{ const includedSteps = route.steps.flatMap((step) => step.includedSteps) return ( - - - - {t('main.route')} - + + + {t('main.route')} )} - + {route.steps.map((step) => ( @@ -136,7 +125,7 @@ const IncludedSteps: React.FC = ({ step }) => { {toolName?.[0]} @@ -148,7 +137,7 @@ const IncludedSteps: React.FC = ({ step }) => { return ( ({ - fontSize: 14, - lineHeight: 1.429, - fontWeight: 500, - color: theme.vars.palette.text.primary, - cursor: 'pointer', - '&:hover': { - opacity: 1, - }, - opacity: 0.56, - transition: theme.transitions.create(['opacity'], { - duration: theme.transitions.duration.enteringScreen, - easing: theme.transitions.easing.easeOut, - }), -})) diff --git a/packages/widget/src/components/TokenRate/TokenRate.tsx b/packages/widget/src/hooks/useTokenRateText.ts similarity index 71% rename from packages/widget/src/components/TokenRate/TokenRate.tsx rename to packages/widget/src/hooks/useTokenRateText.ts index 4586df71d..0746157bf 100644 --- a/packages/widget/src/components/TokenRate/TokenRate.tsx +++ b/packages/widget/src/hooks/useTokenRateText.ts @@ -1,32 +1,21 @@ import { formatUnits, type RouteExtended } from '@lifi/sdk' -import type { TypographyProps } from '@mui/material' import type { MouseEventHandler } from 'react' import { useTranslation } from 'react-i18next' import { create } from 'zustand' -import { TokenRateTypography } from './TokenRate.style.js' - -interface TokenRateProps extends TypographyProps { - route: RouteExtended -} interface TokenRateState { isForward: boolean toggleIsForward(): void } -const useTokenRate = create((set) => ({ +const useTokenRateStore = create((set) => ({ isForward: true, toggleIsForward: () => set((state) => ({ isForward: !state.isForward })), })) -export const TokenRate: React.FC = ({ route }) => { +export function useTokenRateText(route: RouteExtended) { const { t } = useTranslation() - const { isForward, toggleIsForward } = useTokenRate() - - const toggleRate: MouseEventHandler = (e) => { - e.stopPropagation() - toggleIsForward() - } + const { isForward, toggleIsForward } = useTokenRateStore() const lastStep = route.steps.at(-1) @@ -54,9 +43,10 @@ export const TokenRate: React.FC = ({ route }) => { ? `1 ${fromToken.symbol} ≈ ${t('format.tokenAmount', { value: fromToRate })} ${toToken.symbol}` : `1 ${toToken.symbol} ≈ ${t('format.tokenAmount', { value: toFromRate })} ${fromToken.symbol}` - return ( - - {rateText} - - ) + const toggleRate: MouseEventHandler = (e) => { + e.stopPropagation() + toggleIsForward() + } + + return { rateText, toggleRate } } diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json index 0bee03d23..fe2493000 100644 --- a/packages/widget/src/i18n/en.json +++ b/packages/widget/src/i18n/en.json @@ -198,6 +198,7 @@ "tooltip": { "deselectAll": "Deselect all", "estimatedTime": "Time to complete the swap or bridge transaction, excluding chain switching and token approval.", + "exchangeRate": "The estimated conversion rate between source and destination tokens. Click to toggle the direction.", "feeCollection": "The fee is applied to selected token pairs and ensures we can provide the best experience.", "minReceived": "The estimated minimum amount may change until the swapping/bridging transaction is signed. For 2-step transfers, this applies until the second step transaction is signed.", "notFound": { @@ -219,6 +220,8 @@ "checkoutStepDetails": "Purchase via {{tool}}", "currentAmount": "Current amount", "depositStepDetails": "Deposit via {{tool}}", + "estimatedTime": "Estimated time", + "exchangeRate": "Exchange rate", "featuredTokens": "Featured tokens", "fees": { "defaultIntegrator": "Integrator fee", diff --git a/packages/widget/src/pages/TransactionPage/TransactionReview.tsx b/packages/widget/src/pages/TransactionPage/TransactionReview.tsx index e4fee279b..893585f8d 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionReview.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionReview.tsx @@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next' import type { BottomSheetBase } from '../../components/BottomSheet/types.js' import { Card } from '../../components/Card/Card.js' import { WarningMessages } from '../../components/Messages/WarningMessages.js' -import { RouteCardEssentials } from '../../components/RouteCard/RouteCardEssentials.js' import { RouteTokens } from '../../components/RouteCard/RouteTokens.js' import { useAddressActivity } from '../../hooks/useAddressActivity.js' import { useWidgetEvents } from '../../hooks/useWidgetEvents.js' @@ -130,8 +129,7 @@ export const TransactionReview: React.FC = ({ return ( <> - - + From 270f808f2ad1cc725bc47c24bc8512abb20e17a4 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Wed, 18 Mar 2026 16:38:14 +0000 Subject: [PATCH 20/22] feat: add account and remove func to AmountInputHeaderBadge --- .../AmountInput/AmountInputHeaderBadge.tsx | 94 +++++++++++++++++-- .../src/components/Avatar/AccountAvatar.tsx | 27 +++++- .../src/components/Avatar/Avatar.style.tsx | 46 ++++----- .../widget/src/components/Avatar/utils.ts | 9 +- 4 files changed, 137 insertions(+), 39 deletions(-) diff --git a/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx index 56e7e7209..2c50ebf69 100644 --- a/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx +++ b/packages/widget/src/components/AmountInput/AmountInputHeaderBadge.tsx @@ -1,24 +1,49 @@ +import { useAccount } from '@lifi/wallet-management' +import { useChainTypeFromAddress } from '@lifi/widget-provider' +import Close from '@mui/icons-material/Close' import Wallet from '@mui/icons-material/Wallet' -import { Typography } from '@mui/material' +import { Box, IconButton, Typography } from '@mui/material' import { useNavigate } from '@tanstack/react-router' +import type { MouseEvent } from 'react' import { useTranslation } from 'react-i18next' import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js' +import { useBookmarkActions } from '../../stores/bookmarks/useBookmarkActions.js' +import { useBookmarks } from '../../stores/bookmarks/useBookmarks.js' +import { useFieldActions } from '../../stores/form/useFieldActions.js' import { useFieldValues } from '../../stores/form/useFieldValues.js' import { DisabledUI, HiddenUI } from '../../types/widget.js' +import { defaultChainIdsByType } from '../../utils/chainType.js' import { navigationRoutes } from '../../utils/navigationRoutes.js' import { shortenAddress } from '../../utils/wallet.js' +import { AccountAvatar } from '../Avatar/AccountAvatar.js' import { ButtonChip } from '../ButtonChip/ButtonChip.js' export const AmountInputHeaderBadge: React.FC = () => { const { t } = useTranslation() const navigate = useNavigate() - const { subvariant, subvariantOptions, toAddresses, disabledUI, hiddenUI } = - useWidgetConfig() - const [toAddress] = useFieldValues('toAddress') + const { + subvariant, + subvariantOptions, + toAddress, + toAddresses, + disabledUI, + hiddenUI, + } = useWidgetConfig() + const { setFieldValue } = useFieldActions() + const [toAddressValue, toChainId, toTokenAddress] = useFieldValues( + 'toAddress', + 'toChain', + 'toToken' + ) + const { selectedBookmark } = useBookmarks() + const { setSelectedBookmark } = useBookmarkActions() + const { accounts } = useAccount() + const { getChainTypeFromAddress } = useChainTypeFromAddress() const hiddenToAddress = hiddenUI?.includes(HiddenUI.ToAddress) const disabledToAddress = disabledUI?.includes(DisabledUI.ToAddress) - const showWalletBadge = !hiddenToAddress && !(disabledToAddress && !toAddress) + const showWalletBadge = + !hiddenToAddress && !(disabledToAddress && !toAddressValue) if (!showWalletBadge) { return null @@ -38,15 +63,64 @@ export const AmountInputHeaderBadge: React.FC = () => { : navigationRoutes.sendToWallet, }) + const handleRemove = (e: MouseEvent) => { + e.stopPropagation() + setFieldValue('toAddress', '', { isTouched: true }) + setSelectedBookmark() + } + + const matchingConnectedAccount = accounts.find( + (account) => account.address === toAddressValue + ) + + const chainType = !matchingConnectedAccount + ? selectedBookmark?.chainType || + (toAddressValue ? getChainTypeFromAddress(toAddressValue) : undefined) + : undefined + + const chainId = + toChainId && toTokenAddress + ? toChainId + : matchingConnectedAccount + ? matchingConnectedAccount.chainId + : chainType + ? defaultChainIdsByType[chainType] + : undefined + return ( ({ + display: 'flex', + alignItems: 'center', + gap: 0.75, + borderRadius: theme.shape.borderRadius, + height: 32, + })} > - - - {toAddress ? shortenAddress(toAddress) : label} - + + {toAddressValue ? ( + + ) : null} + + {toAddressValue ? shortenAddress(toAddressValue) : label} + + + {toAddressValue && !disabledToAddress ? ( + + + + ) : ( + + )} ) } diff --git a/packages/widget/src/components/Avatar/AccountAvatar.tsx b/packages/widget/src/components/Avatar/AccountAvatar.tsx index e8b1575f4..da0ccffe7 100644 --- a/packages/widget/src/components/Avatar/AccountAvatar.tsx +++ b/packages/widget/src/components/Avatar/AccountAvatar.tsx @@ -12,6 +12,9 @@ interface AccountAvatarProps { account?: Account toAddress?: ToAddress empty?: boolean + size?: number + badgeSize?: number + badgeBorderWidthPx?: number } export const AccountAvatar = ({ @@ -19,21 +22,37 @@ export const AccountAvatar = ({ account, empty, toAddress, + size, + badgeSize, + badgeBorderWidthPx, }: AccountAvatarProps) => { const { chain } = useChain(chainId) + const walletIconSize = size ? Math.floor(size * 0.5) : 20 + const avatar = empty ? ( - + ) : account?.connector || toAddress?.logoURI ? ( {(toAddress?.name || account?.connector?.name)?.[0]} ) : ( - - + + ) @@ -41,7 +60,7 @@ export const AccountAvatar = ({ } + badgeContent={} > {avatar} diff --git a/packages/widget/src/components/Avatar/Avatar.style.tsx b/packages/widget/src/components/Avatar/Avatar.style.tsx index 18d0b3db1..0e88e55bf 100644 --- a/packages/widget/src/components/Avatar/Avatar.style.tsx +++ b/packages/widget/src/components/Avatar/Avatar.style.tsx @@ -10,12 +10,13 @@ import { import { getAvatarMask } from './utils.js' export const AvatarMasked = styled(MuiAvatar, { - shouldForwardProp: (prop) => prop !== 'avatarSize' && prop !== 'badgeSize', -})<{ avatarSize?: number; badgeSize?: number }>( - ({ avatarSize = 40, badgeSize = 16 }) => ({ + shouldForwardProp: (prop: string) => + !['avatarSize', 'badgeSize', 'badgeBorderWidthPx'].includes(prop), +})<{ avatarSize?: number; badgeSize?: number; badgeBorderWidthPx?: number }>( + ({ avatarSize = 40, badgeSize = 16, badgeBorderWidthPx = 2.5 }) => ({ width: avatarSize, height: avatarSize, - mask: getAvatarMask(badgeSize), + mask: getAvatarMask(badgeSize, badgeBorderWidthPx), }) ) @@ -37,24 +38,27 @@ export const TokenAvatarGroup = styled(AvatarGroup)(({ theme }) => ({ })) export const AvatarDefault = styled(Box, { - shouldForwardProp: (prop) => prop !== 'badgeSize', -})<{ badgeSize?: number }>(({ theme, badgeSize = 16 }) => { - const root = theme.components?.MuiAvatar?.styleOverrides?.root as CSSObject - return { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - borderRadius: '50%', - height: root?.height, - width: root?.width, - color: theme.vars.palette.text.secondary, - mask: getAvatarMask(badgeSize), - background: theme.vars.palette.grey[300], - ...theme.applyStyles('dark', { - background: theme.vars.palette.grey[800], - }), + shouldForwardProp: (prop) => + prop !== 'badgeSize' && prop !== 'badgeBorderWidthPx', +})<{ badgeSize?: number; badgeBorderWidthPx?: number }>( + ({ theme, badgeSize = 16, badgeBorderWidthPx = 2.5 }) => { + const root = theme.components?.MuiAvatar?.styleOverrides?.root as CSSObject + return { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + borderRadius: '50%', + height: root?.height, + width: root?.width, + color: theme.vars.palette.text.secondary, + mask: getAvatarMask(badgeSize, badgeBorderWidthPx), + background: theme.vars.palette.grey[300], + ...theme.applyStyles('dark', { + background: theme.vars.palette.grey[800], + }), + } } -}) +) export const AvatarDefaultBadge = styled(Box, { shouldForwardProp: (prop) => prop !== 'size', diff --git a/packages/widget/src/components/Avatar/utils.ts b/packages/widget/src/components/Avatar/utils.ts index 9c7de02d4..f0f19c225 100644 --- a/packages/widget/src/components/Avatar/utils.ts +++ b/packages/widget/src/components/Avatar/utils.ts @@ -1,6 +1,7 @@ -const borderWidthPx = 2.5 - // 14% is the right bottom offset of the MUI's badge (MuiBadge-badge class) -export const getAvatarMask = (badgeSize: number) => { - return `radial-gradient(circle ${badgeSize / 2 + borderWidthPx}px at calc(100% - 14%) calc(100% - 14%), #fff0 96%, #fff) 100% 100% / 100% 100% no-repeat` +export const getAvatarMask = ( + badgeSize: number, + badgeBorderWidthPx: number = 2.5 +) => { + return `radial-gradient(circle ${badgeSize / 2 + badgeBorderWidthPx}px at calc(100% - 14%) calc(100% - 14%), #fff0 96%, #fff) 100% 100% / 100% 100% no-repeat` } From cf3016c819e3adfd80226a493e0426a85bc24cd2 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Wed, 18 Mar 2026 16:59:32 +0000 Subject: [PATCH 21/22] feat: activities --- packages/widget/src/AppDefault.tsx | 28 +++++++++---------- .../components/ActionRow/ActionRow.style.tsx | 2 +- ...n.style.tsx => ActivitiesButton.style.tsx} | 0 ...HistoryButton.tsx => ActivitiesButton.tsx} | 8 +++--- .../components/Header/NavigationHeader.tsx | 4 +-- .../src/components/RouteCard/RouteDetails.tsx | 4 +-- .../useTransactionList.ts | 22 +++++++-------- packages/widget/src/i18n/en.json | 2 +- .../ActiveTransactionItem.style.tsx | 0 .../ActiveTransactionItem.tsx | 0 .../ActivitiesPage.tsx} | 6 ++-- .../TransactionHistoryEmpty.tsx | 0 .../TransactionHistoryItem.tsx | 0 .../TransactionHistorySkeleton.tsx | 0 .../constants.ts | 0 .../types.ts | 0 .../pages/TransactionPage/SentToWalletRow.tsx | 2 +- packages/widget/src/utils/navigationRoutes.ts | 6 ++-- 18 files changed, 41 insertions(+), 43 deletions(-) rename packages/widget/src/components/Header/{TransactionHistoryButton.style.tsx => ActivitiesButton.style.tsx} (100%) rename packages/widget/src/components/Header/{TransactionHistoryButton.tsx => ActivitiesButton.tsx} (88%) rename packages/widget/src/{pages/TransactionHistoryPage => hooks}/useTransactionList.ts (83%) rename packages/widget/src/pages/{TransactionHistoryPage => ActivitiesPage}/ActiveTransactionItem.style.tsx (100%) rename packages/widget/src/pages/{TransactionHistoryPage => ActivitiesPage}/ActiveTransactionItem.tsx (100%) rename packages/widget/src/pages/{TransactionHistoryPage/TransactionHistoryPage.tsx => ActivitiesPage/ActivitiesPage.tsx} (95%) rename packages/widget/src/pages/{TransactionHistoryPage => ActivitiesPage}/TransactionHistoryEmpty.tsx (100%) rename packages/widget/src/pages/{TransactionHistoryPage => ActivitiesPage}/TransactionHistoryItem.tsx (100%) rename packages/widget/src/pages/{TransactionHistoryPage => ActivitiesPage}/TransactionHistorySkeleton.tsx (100%) rename packages/widget/src/pages/{TransactionHistoryPage => ActivitiesPage}/constants.ts (100%) rename packages/widget/src/pages/{TransactionHistoryPage => ActivitiesPage}/types.ts (100%) diff --git a/packages/widget/src/AppDefault.tsx b/packages/widget/src/AppDefault.tsx index fb66397be..32f35785f 100644 --- a/packages/widget/src/AppDefault.tsx +++ b/packages/widget/src/AppDefault.tsx @@ -8,6 +8,7 @@ import { } from '@tanstack/react-router' import { AppLayout } from './AppLayout.js' import { NotFound } from './components/NotFound.js' +import { ActivitiesPage } from './pages/ActivitiesPage/ActivitiesPage.js' import { LanguagesPage } from './pages/LanguagesPage.js' import { MainPage } from './pages/MainPage/MainPage.js' import { RoutesPage } from './pages/RoutesPage/RoutesPage.js' @@ -21,7 +22,6 @@ import { SendToConfiguredWalletPage } from './pages/SendToWallet/SendToConfigure import { SendToWalletPage } from './pages/SendToWallet/SendToWalletPage.js' import { SettingsPage } from './pages/SettingsPage/SettingsPage.js' import { TransactionDetailsPage } from './pages/TransactionDetailsPage/TransactionDetailsPage.js' -import { TransactionHistoryPage } from './pages/TransactionHistoryPage/TransactionHistoryPage.js' import { TransactionPage } from './pages/TransactionPage/TransactionPage.js' import { navigationRoutes } from './utils/navigationRoutes.js' @@ -168,25 +168,25 @@ const configuredWalletsRoute = createRoute({ component: SendToConfiguredWalletPage, }) -const transactionHistoryLayoutRoute = createRoute({ +const activitiesLayoutRoute = createRoute({ getParentRoute: () => rootRoute, - path: navigationRoutes.transactionHistory, + path: navigationRoutes.activities, }) -const transactionHistoryIndexRoute = createRoute({ - getParentRoute: () => transactionHistoryLayoutRoute, +const activitiesIndexRoute = createRoute({ + getParentRoute: () => activitiesLayoutRoute, path: '/', - component: TransactionHistoryPage, + component: ActivitiesPage, }) -const transactionHistoryDetailsRoute = createRoute({ - getParentRoute: () => transactionHistoryLayoutRoute, +const activitiesDetailsRoute = createRoute({ + getParentRoute: () => activitiesLayoutRoute, path: navigationRoutes.transactionDetails, component: TransactionDetailsPage, }) -const transactionHistoryExecutionRoute = createRoute({ - getParentRoute: () => transactionHistoryLayoutRoute, +const activitiesExecutionRoute = createRoute({ + getParentRoute: () => activitiesLayoutRoute, path: navigationRoutes.transactionExecution, component: TransactionPage, }) @@ -240,10 +240,10 @@ const routeTree = rootRoute.addChildren([ sendToWalletConnectedWalletsRoute, ]), configuredWalletsRoute, - transactionHistoryLayoutRoute.addChildren([ - transactionHistoryIndexRoute, - transactionHistoryDetailsRoute, - transactionHistoryExecutionRoute, + activitiesLayoutRoute.addChildren([ + activitiesIndexRoute, + activitiesDetailsRoute, + activitiesExecutionRoute, ]), ]) diff --git a/packages/widget/src/components/ActionRow/ActionRow.style.tsx b/packages/widget/src/components/ActionRow/ActionRow.style.tsx index 292462fd4..ed4b5575c 100644 --- a/packages/widget/src/components/ActionRow/ActionRow.style.tsx +++ b/packages/widget/src/components/ActionRow/ActionRow.style.tsx @@ -5,7 +5,7 @@ export const ActionRowContainer = styled(Box)(({ theme }) => ({ alignItems: 'center', gap: theme.spacing(1), padding: theme.spacing(1), - borderRadius: theme.vars.shape.borderRadiusSecondary, + borderRadius: theme.vars.shape.borderRadiusTertiary, backgroundColor: `rgba(${theme.vars.palette.common.onBackgroundChannel} / 0.04)`, })) diff --git a/packages/widget/src/components/Header/TransactionHistoryButton.style.tsx b/packages/widget/src/components/Header/ActivitiesButton.style.tsx similarity index 100% rename from packages/widget/src/components/Header/TransactionHistoryButton.style.tsx rename to packages/widget/src/components/Header/ActivitiesButton.style.tsx diff --git a/packages/widget/src/components/Header/TransactionHistoryButton.tsx b/packages/widget/src/components/Header/ActivitiesButton.tsx similarity index 88% rename from packages/widget/src/components/Header/TransactionHistoryButton.tsx rename to packages/widget/src/components/Header/ActivitiesButton.tsx index c6c634b54..77abbf58f 100644 --- a/packages/widget/src/components/Header/TransactionHistoryButton.tsx +++ b/packages/widget/src/components/Header/ActivitiesButton.tsx @@ -10,19 +10,19 @@ import { ErrorBadge, HistoryIconButton, ProgressContainer, -} from './TransactionHistoryButton.style.js' +} from './ActivitiesButton.style.js' -export const TransactionHistoryButton = () => { +export const ActivitiesButton = () => { const { t } = useTranslation() const navigate = useNavigate() const indicator = useRouteExecutionIndicator() return ( - + navigate({ to: navigationRoutes.transactionHistory })} + onClick={() => navigate({ to: navigationRoutes.activities })} > { const { subvariant, hiddenUI, variant, defaultUI, subvariantOptions } = @@ -61,7 +61,7 @@ export const NavigationHeader: React.FC = () => { {pathname === navigationRoutes.home ? ( {account.isConnected && !hiddenUI?.includes(HiddenUI.History) && ( - + )} {variant === 'drawer' && diff --git a/packages/widget/src/components/RouteCard/RouteDetails.tsx b/packages/widget/src/components/RouteCard/RouteDetails.tsx index d2c64a447..e1e18641d 100644 --- a/packages/widget/src/components/RouteCard/RouteDetails.tsx +++ b/packages/widget/src/components/RouteCard/RouteDetails.tsx @@ -83,9 +83,7 @@ export const RouteDetails = ({ route }: RouteDetailsProps) => { (feeAmountUSD || Number.isFinite(feeConfig?.fee)) && !hasGaslessSupport return ( - + diff --git a/packages/widget/src/pages/TransactionHistoryPage/useTransactionList.ts b/packages/widget/src/hooks/useTransactionList.ts similarity index 83% rename from packages/widget/src/pages/TransactionHistoryPage/useTransactionList.ts rename to packages/widget/src/hooks/useTransactionList.ts index 231bb3bdd..b513fef43 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/useTransactionList.ts +++ b/packages/widget/src/hooks/useTransactionList.ts @@ -1,21 +1,21 @@ import { useAccount } from '@lifi/wallet-management' import { useMemo } from 'react' -import { useDeduplicateRoutes } from '../../hooks/useDeduplicateRoutes.js' -import { useTransactionHistory } from '../../hooks/useTransactionHistory.js' -import { useRouteExecutionStore } from '../../stores/routes/RouteExecutionStore.js' -import type { - RouteExecution, - RouteExecutionState, -} from '../../stores/routes/types.js' -import { RouteExecutionStatus } from '../../stores/routes/types.js' -import { getSourceTxHash } from '../../stores/routes/utils.js' -import { hasEnumFlag } from '../../utils/enum.js' import type { ActiveItem, HistoryItem, LocalItem, TransactionListItem, -} from './types.js' +} from '../pages/ActivitiesPage/types.js' +import { useRouteExecutionStore } from '../stores/routes/RouteExecutionStore.js' +import type { + RouteExecution, + RouteExecutionState, +} from '../stores/routes/types.js' +import { RouteExecutionStatus } from '../stores/routes/types.js' +import { getSourceTxHash } from '../stores/routes/utils.js' +import { hasEnumFlag } from '../utils/enum.js' +import { useDeduplicateRoutes } from './useDeduplicateRoutes.js' +import { useTransactionHistory } from './useTransactionHistory.js' const routesSelector = (state: RouteExecutionState) => state.routes diff --git a/packages/widget/src/i18n/en.json b/packages/widget/src/i18n/en.json index fe2493000..b0ff6467c 100644 --- a/packages/widget/src/i18n/en.json +++ b/packages/widget/src/i18n/en.json @@ -80,7 +80,7 @@ "swap": "Swap", "to": "Exchange to", "transactionDetails": "Transaction details", - "transactionHistory": "Transaction history", + "activities": "Activities", "walletConnected": "Wallet connected", "youGet": "You get", "youPay": "You pay" diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.style.tsx b/packages/widget/src/pages/ActivitiesPage/ActiveTransactionItem.style.tsx similarity index 100% rename from packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.style.tsx rename to packages/widget/src/pages/ActivitiesPage/ActiveTransactionItem.style.tsx diff --git a/packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.tsx b/packages/widget/src/pages/ActivitiesPage/ActiveTransactionItem.tsx similarity index 100% rename from packages/widget/src/pages/TransactionHistoryPage/ActiveTransactionItem.tsx rename to packages/widget/src/pages/ActivitiesPage/ActiveTransactionItem.tsx diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx b/packages/widget/src/pages/ActivitiesPage/ActivitiesPage.tsx similarity index 95% rename from packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx rename to packages/widget/src/pages/ActivitiesPage/ActivitiesPage.tsx index ceca040d8..2e1b020d7 100644 --- a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryPage.tsx +++ b/packages/widget/src/pages/ActivitiesPage/ActivitiesPage.tsx @@ -5,20 +5,20 @@ import { useTranslation } from 'react-i18next' import { PageContainer } from '../../components/PageContainer.js' import { useHeader } from '../../hooks/useHeader.js' import { useListHeight } from '../../hooks/useListHeight.js' +import { useTransactionList } from '../../hooks/useTransactionList.js' import { ActiveTransactionItem } from './ActiveTransactionItem.js' import { minTransactionListHeight } from './constants.js' import { TransactionHistoryEmpty } from './TransactionHistoryEmpty.js' import { TransactionHistoryItem } from './TransactionHistoryItem.js' import { TransactionHistoryItemSkeleton } from './TransactionHistorySkeleton.js' -import { useTransactionList } from './useTransactionList.js' -export const TransactionHistoryPage = () => { +export const ActivitiesPage = () => { // Parent ref and useVirtualizer should be in one file to avoid blank page (0 virtual items) issue const parentRef = useRef(null) const { items, isLoading } = useTransactionList() const { t } = useTranslation() - useHeader(t('header.transactionHistory')) + useHeader(t('header.activities')) const { listHeight } = useListHeight({ listParentRef: parentRef, diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryEmpty.tsx b/packages/widget/src/pages/ActivitiesPage/TransactionHistoryEmpty.tsx similarity index 100% rename from packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryEmpty.tsx rename to packages/widget/src/pages/ActivitiesPage/TransactionHistoryEmpty.tsx diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx b/packages/widget/src/pages/ActivitiesPage/TransactionHistoryItem.tsx similarity index 100% rename from packages/widget/src/pages/TransactionHistoryPage/TransactionHistoryItem.tsx rename to packages/widget/src/pages/ActivitiesPage/TransactionHistoryItem.tsx diff --git a/packages/widget/src/pages/TransactionHistoryPage/TransactionHistorySkeleton.tsx b/packages/widget/src/pages/ActivitiesPage/TransactionHistorySkeleton.tsx similarity index 100% rename from packages/widget/src/pages/TransactionHistoryPage/TransactionHistorySkeleton.tsx rename to packages/widget/src/pages/ActivitiesPage/TransactionHistorySkeleton.tsx diff --git a/packages/widget/src/pages/TransactionHistoryPage/constants.ts b/packages/widget/src/pages/ActivitiesPage/constants.ts similarity index 100% rename from packages/widget/src/pages/TransactionHistoryPage/constants.ts rename to packages/widget/src/pages/ActivitiesPage/constants.ts diff --git a/packages/widget/src/pages/TransactionHistoryPage/types.ts b/packages/widget/src/pages/ActivitiesPage/types.ts similarity index 100% rename from packages/widget/src/pages/TransactionHistoryPage/types.ts rename to packages/widget/src/pages/ActivitiesPage/types.ts diff --git a/packages/widget/src/pages/TransactionPage/SentToWalletRow.tsx b/packages/widget/src/pages/TransactionPage/SentToWalletRow.tsx index d28ac4d6b..da78543c7 100644 --- a/packages/widget/src/pages/TransactionPage/SentToWalletRow.tsx +++ b/packages/widget/src/pages/TransactionPage/SentToWalletRow.tsx @@ -39,7 +39,7 @@ export const SentToWalletRow: React.FC = ({ address: shortenAddress(toAddress), })} endAdornment={ - + diff --git a/packages/widget/src/utils/navigationRoutes.ts b/packages/widget/src/utils/navigationRoutes.ts index 4c6489415..89526f8be 100644 --- a/packages/widget/src/utils/navigationRoutes.ts +++ b/packages/widget/src/utils/navigationRoutes.ts @@ -12,7 +12,7 @@ export const navigationRoutes = { toTokenNative: 'to-token-native', transactionDetails: 'transaction-details', transactionExecution: 'transaction-execution', - transactionHistory: 'transaction-history', + activities: 'activities', sendToWallet: 'send-to-wallet', bookmarks: 'bookmarks', recentWallets: 'recent-wallets', @@ -33,7 +33,7 @@ export const stickyHeaderRoutes = [ navigationRoutes.toTokenNative, navigationRoutes.transactionDetails, navigationRoutes.transactionExecution, - navigationRoutes.transactionHistory, + navigationRoutes.activities, navigationRoutes.sendToWallet, navigationRoutes.bookmarks, navigationRoutes.recentWallets, @@ -54,7 +54,7 @@ export const backButtonRoutes = [ navigationRoutes.toTokenNative, navigationRoutes.transactionDetails, navigationRoutes.transactionExecution, - navigationRoutes.transactionHistory, + navigationRoutes.activities, navigationRoutes.sendToWallet, navigationRoutes.bookmarks, navigationRoutes.recentWallets, From 88a5bc78d8105c9d73a0356a1ec5b7b97512fd98 Mon Sep 17 00:00:00 2001 From: Lizaveta Miasayedava Date: Wed, 18 Mar 2026 18:13:36 +0000 Subject: [PATCH 22/22] fix: amount reset, execution gap --- .../ExecutionProgressCards.tsx | 9 +-- .../pages/TransactionPage/StepActionRow.tsx | 27 ++------ .../pages/TransactionPage/StepActionsList.tsx | 66 ++++++++++++++----- .../pages/TransactionPage/TransactionPage.tsx | 4 +- 4 files changed, 59 insertions(+), 47 deletions(-) diff --git a/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx index f4ce8c996..0bc928df2 100644 --- a/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx +++ b/packages/widget/src/pages/TransactionPage/ExecutionProgressCards.tsx @@ -21,10 +21,7 @@ export const ExecutionProgressCards: React.FC = ({ const { feeConfig } = useWidgetConfig() const isDone = hasEnumFlag(status, RouteExecutionStatus.Done) const toAddress = isDone ? route.toAddress : undefined - const hasActions = route.steps.some( - (step) => (step.execution?.actions?.length ?? 0) > 0 - ) - const hasListItems = hasActions || !!toAddress + const VcComponent = status === RouteExecutionStatus.Done ? feeConfig?._vcComponent : undefined @@ -33,9 +30,7 @@ export const ExecutionProgressCards: React.FC = ({ - {hasListItems ? ( - - ) : null} + {isDone ? ( diff --git a/packages/widget/src/pages/TransactionPage/StepActionRow.tsx b/packages/widget/src/pages/TransactionPage/StepActionRow.tsx index fc328abe8..184945b5c 100644 --- a/packages/widget/src/pages/TransactionPage/StepActionRow.tsx +++ b/packages/widget/src/pages/TransactionPage/StepActionRow.tsx @@ -4,30 +4,15 @@ import type React from 'react' import { ActionRow } from '../../components/ActionRow/ActionRow.js' import { IconCircle } from '../../components/IconCircle/IconCircle.js' import { useActionMessage } from '../../hooks/useActionMessage.js' -import { useExplorer } from '../../hooks/useExplorer.js' import { ExternalLink } from './ReceiptsCard.style.js' export const StepActionRow: React.FC<{ step: LiFiStepExtended - actionsGroup: ExecutionAction[] -}> = ({ step, actionsGroup }) => { - const action = actionsGroup.at(-1) + action: ExecutionAction + href: string +}> = ({ step, action, href }) => { const { title } = useActionMessage(step, action) - const { getTransactionLink } = useExplorer() - - const isDone = action?.status === 'DONE' const isFailed = action?.status === 'FAILED' - - const transactionLink = action?.txHash - ? getTransactionLink({ txHash: action.txHash, chain: action.chainId }) - : action?.txLink - ? getTransactionLink({ txLink: action.txLink, chain: action.chainId }) - : undefined - - if ((!isDone && !isFailed) || !transactionLink) { - return null - } - return ( + } diff --git a/packages/widget/src/pages/TransactionPage/StepActionsList.tsx b/packages/widget/src/pages/TransactionPage/StepActionsList.tsx index f5173db44..8a2aaa06e 100644 --- a/packages/widget/src/pages/TransactionPage/StepActionsList.tsx +++ b/packages/widget/src/pages/TransactionPage/StepActionsList.tsx @@ -1,4 +1,5 @@ import type { RouteExtended } from '@lifi/sdk' +import { useExplorer } from '../../hooks/useExplorer.js' import { prepareActions } from '../../utils/prepareActions.js' import { TransactionList } from './ReceiptsCard.style.js' import { SentToWalletRow } from './SentToWalletRow.js' @@ -12,23 +13,56 @@ interface StepActionsListProps { export const StepActionsList: React.FC = ({ route, toAddress, -}) => ( - - {route.steps.map((step) => ( - - {prepareActions(step.execution?.actions ?? []).map( - (actionsGroup, index) => ( +}) => { + const { getTransactionLink } = useExplorer() + const stepRows = route.steps + .map((step) => { + const rows = prepareActions(step.execution?.actions ?? []) + .map((actionsGroup) => { + const action = actionsGroup.at(-1) + const href = action?.txHash + ? getTransactionLink({ + txHash: action.txHash, + chain: action.chainId, + }) + : action?.txLink + ? getTransactionLink({ + txLink: action.txLink, + chain: action.chainId, + }) + : undefined + return { action, href } + }) + .filter(({ action, href }) => { + const doneOrFailed = + action?.status === 'DONE' || action?.status === 'FAILED' + return Boolean(href && doneOrFailed) + }) + return { step, rows } + }) + .filter(({ rows }) => rows.length > 0) + + if (!stepRows?.length) { + return null + } + + return ( + + {stepRows.map(({ step, rows }) => ( + + {rows.map(({ action, href }, index) => ( - ) - )} - - ))} - {toAddress ? ( - - ) : null} - -) + ))} + + ))} + {toAddress ? ( + + ) : null} + + ) +} diff --git a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx index d3a156b62..7fcd4cfb6 100644 --- a/packages/widget/src/pages/TransactionPage/TransactionPage.tsx +++ b/packages/widget/src/pages/TransactionPage/TransactionPage.tsx @@ -84,7 +84,9 @@ export const TransactionPage = () => { // Clean form fields when leaving the page return () => { - cleanFields() + if (status !== RouteExecutionStatus.Idle) { + cleanFields() + } } }, [])