Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/checkout-flow-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@lifi/widget-checkout": minor
"@lifi/widget": patch
---

Auto-resume a single in-flight checkout deposit on the funding screen, abandon a transfer from the back button (with confirmation), and surface checkout-specific route-not-found copy. Add state-aware status step labels, fail resumed exchange records that lost their session, and hide the redundant token subtext under the cash pay field.
16 changes: 16 additions & 0 deletions .changeset/checkout-flow-polish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@lifi/widget-checkout": patch
"@lifi/widget": patch
---

Polish the checkout flow and align it with the main widget design.

- Cards follow the widget's theme: receive/handoff cards use the shared border radius (so custom shapes like windows95 apply), the from/to token icons and labels line up, and the header back button aligns with page content.
- The refresh-quotes button now refetches, and the checkout route query keeps its previous result so amount/fee values update in place on refresh and on token/amount changes instead of flashing a skeleton or resetting to zero.
- Remove the funding-screen activity cards and stop surfacing the Mesh error on the choose-funding-source page.
- Reuse the widget's send-to-wallet card for the checkout recipient (gated read-only for fixed recipients), and reuse the shared step link styling on the status page.
- A quote with no deposit address is treated as unavailable instead of a silently disabled button, and going back from the amount screen no longer returns to the set-destination screen.
- Cash deposit card: move the disclaimer inside the card, smooth the expand animation, and tighten the amount-to-fees spacing.
- Show the Transak modal close button immediately, and prevent selecting the destination token as the source token.

`@lifi/widget` exposes `SendToWalletButton` (with optional `onEditAddress`/`onClearAddress` overrides and a `requireAddress` flag), `BookmarkStoreProvider`, and the step `ExternalLink` via `@lifi/widget/shared`, and `useRoutes` gains an opt-in `keepPreviousData`.
4 changes: 2 additions & 2 deletions packages/widget-checkout/src/CheckoutRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ const routeTree = rootRoute.addChildren([
])

export const CheckoutRouter: React.FC = () => {
// No deep-resume on mount — pending deposits surface as a tappable activity
// list on the funding screen.
// Always start at home; the funding screen auto-resumes a single in-flight
// deposit and surfaces the rest as a tappable activity list.
const [router] = useState(() =>
createRouter({
routeTree,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useTranslation } from 'react-i18next'
import { ConfirmationBottomSheet } from './ConfirmationBottomSheet.js'

interface AbandonConfirmationDialogProps {
open: boolean
onCancel: () => void
onConfirm: () => void
container?: HTMLElement | null
}

const titleId = 'checkout-abandon-confirmation-title'
const bodyId = 'checkout-abandon-confirmation-body'

export const AbandonConfirmationDialog: React.FC<
AbandonConfirmationDialogProps
> = ({ open, onCancel, onConfirm, container }) => {
const { t } = useTranslation()
return (
<ConfirmationBottomSheet
open={open}
onCancel={onCancel}
onConfirm={onConfirm}
container={container}
titleId={titleId}
bodyId={bodyId}
title={t('checkout.abandonConfirmation.title')}
body={t('checkout.abandonConfirmation.body')}
cancelLabel={t('checkout.abandonConfirmation.cancel')}
confirmLabel={t('checkout.abandonConfirmation.confirm')}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const CashHandoffSheet: React.FC<CashHandoffSheetProps> = ({
sx={{
mt: 2,
p: 2,
borderRadius: 1.5,
borderRadius: 1,
bgcolor: 'background.paper',
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,17 +352,12 @@ const CheckoutTokenFlow: React.FC<FormTypeProps> = ({ formType }) => {
m: 0,
display: 'flex',
alignItems: 'center',
gap: 0.5,
gap: 2,
cursor: isInteractive ? 'pointer' : 'default',
}}
>
{token && chain ? (
<TokenAvatar
token={token}
chain={chain}
tokenAvatarSize={32}
chainAvatarSize={12}
/>
<TokenAvatar token={token} chain={chain} />
) : (
<AvatarBadgedDefault />
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// @vitest-environment happy-dom
import { render, screen } from '@testing-library/react'
import type { ReactNode } from 'react'
import { describe, expect, it, vi } from 'vitest'

const { fundingSourceRef } = vi.hoisted(() => ({
fundingSourceRef: { current: 'cash' as string | null },
}))

vi.mock('@lifi/widget/shared', () => ({
FormKeyHelper: {
getChainKey: (f: string) => `${f}.chain`,
getTokenKey: (f: string) => `${f}.token`,
getAmountKey: (f: string) => `${f}.amount`,
},
formatTokenAmount: (v: unknown) => String(v),
formatTokenPrice: () => '5.00',
InputPriceButton: ({ children }: { children: ReactNode }) => (
<div>{children}</div>
),
useFieldValues: () => ['1', '0xtoken', '5'],
useInputModeStore: () => ({
inputMode: { from: 'price', to: 'price' },
toggleInputMode: () => {},
}),
useToken: () => ({
token: { symbol: 'USDC', decimals: 6, priceUSD: '1' },
isLoading: false,
}),
useTokenAddressBalance: () => ({ token: undefined, isLoading: false }),
}))

vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, opts?: { value?: string }) => opts?.value ?? key,
}),
}))

vi.mock('../hooks/useIsWalletFundedFlow.js', () => ({
useIsWalletFundedFlow: () => false,
}))

vi.mock('../stores/useCheckoutFlowStore.js', () => ({
useCheckoutFlowStore: (
selector: (s: { fundingSource: string | null }) => unknown
) => selector({ fundingSource: fundingSourceRef.current }),
}))

import { CheckoutPriceFormHelperText } from './CheckoutPriceFormHelperText.js'

describe('CheckoutPriceFormHelperText', () => {
it('renders no subtext for the cash "from" field', () => {
fundingSourceRef.current = 'cash'
const { container } = render(
<CheckoutPriceFormHelperText formType="from" />
)
expect(container.firstChild).toBeNull()
expect(screen.queryByText('USDC')).toBeNull()
})

it('renders the token subtext for non-cash funding sources', () => {
fundingSourceRef.current = 'transfer'
render(<CheckoutPriceFormHelperText formType="from" />)
expect(screen.queryByText('USDC')).not.toBeNull()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type React from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { useIsWalletFundedFlow } from '../hooks/useIsWalletFundedFlow.js'
import { useCheckoutFlowStore } from '../stores/useCheckoutFlowStore.js'
import { formatCheckoutBalanceWithToken } from '../utils/formatCheckoutBalance.js'

export const CheckoutPriceFormHelperText: React.NamedExoticComponent<FormTypeProps> =
Expand All @@ -28,6 +29,7 @@ export const CheckoutPriceFormHelperText: React.NamedExoticComponent<FormTypePro
)
const { inputMode, toggleInputMode } = useInputModeStore()
const isWalletFunded = useIsWalletFundedFlow()
const fundingSource = useCheckoutFlowStore((s) => s.fundingSource)

const { token: walletToken, isLoading: walletBalanceLoading } =
useTokenAddressBalance(
Expand Down Expand Up @@ -71,6 +73,11 @@ export const CheckoutPriceFormHelperText: React.NamedExoticComponent<FormTypePro
})
: '0'

// The cash "pay" field already shows the fiat amount; the token subtext under it is noise.
if (formType === 'from' && fundingSource === 'cash') {
return null
}

return (
<FormHelperText
component="div"
Expand Down
Loading