feat(widget-checkout): checkout mode — on-ramp, deposit & wallet funding#817
feat(widget-checkout): checkout mode — on-ramp, deposit & wallet funding#817chybisov wants to merge 17 commits into
Conversation
Co-authored-by: Eugene Chybisov <18644653+chybisov@users.noreply.github.com>
… flow (#784) Co-authored-by: eugene <eugene@li.finance>
Co-authored-by: eugene <eugene@li.finance>
Co-authored-by: eugene <eugene@li.finance>
Co-authored-by: eugene <eugene@li.finance>
…ess (#788) Co-authored-by: eugene <eugene@li.finance>
Co-authored-by: eugene <eugene@li.finance>
Co-authored-by: eugene <eugene@li.finance>
Co-authored-by: eugene <eugene@li.finance>
🦋 Changeset detectedLatest commit: ea4d3cc The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
# Conflicts: # CLAUDE.md # packages/widget-playground/src/components/Widget/WidgetView.tsx # packages/widget-provider/package.json # packages/widget/src/hooks/useRoutes.ts # packages/widget/src/i18n/en.json # pnpm-lock.yaml
✅ E2E Dev Smoke — passing
4 passed · 0 failed · 0 skipped · 34s |
E2E Examples — all passedAll examples passed in the latest run. |
E2E Playground resultsDetails
📥 Download full HTML report (open the run → Artifacts → |
- status polling: expose raw isError so the status page's retry screen can render after a mid-poll failure (stop-on-error + manual retry); add error-path tests - Transak: remove the temporary native-ETH 5xx retry fallback; open the hosted dialog only once a session succeeds (pre-flight errors surface inline on the status page) - Mesh: tear down the Link overlay on close/cancel via closeLink(); close() clears failure/depositTxHash; MMT accounts use the default reconnect TTL instead of the raw token expiry - checkout: restore the integrator exchange list (snapshot-based) when switching to the wallet flow; require explicit pay-from-wallet intent before auto-navigating on wallet connect; drop expired CEX reconnect accounts at selection time - surface routes-level fetch errors through useCheckoutFlowQuote (useRoutes now exposes isError) - harden checkout userId generation for insecure contexts; guard empty route steps; transfer deposit copy; noopener on explorer link; playground env fixes Note: useRoutes.ts diff includes a biome reformat (destructure went multiline); substantive changes are additive only (see diff -w).
Web Crypto's getRandomValues remains the real fallback; the last-resort tier (no Web Crypto at all) now uses a time-based ephemeral id.
# Conflicts: # pnpm-lock.yaml
🔍 QA Review — EMB-464PR: #817 feat(widget-checkout): checkout mode — on-ramp, deposit & wallet funding What this ticket doesEMB-464 replaces the widget's fake Transak quote (a static Acceptance Criteria
Blocking Open Item: Backend Contract —
|
| File | Has Test | Notes |
|---|---|---|
useOnRampQuote.ts |
❌ No | New hook with debounce + enabled-guard logic — no unit test |
useOnRampFiatCurrencies.ts |
❌ No | New hook with normalization logic — no unit test |
useFiatCurrencyStore.tsx |
❌ No | New store with seedCurrency/setCurrency/reset state machine — no unit test |
CheckoutFlowCtaButton.tsx |
❌ No | cashNotReady + cashRouteMatchesQuote logic untested |
SelectCashCurrencyPage.tsx |
❌ No | No component test; fallback to static list untested |
currencyToFlag.ts |
❌ No | New utility — no test for arbitrary/unknown codes |
fiatFormat.ts |
❌ No | New utilities — no test for formatFiat, getCurrencyName, getCurrencySymbol |
usePendingCheckoutWriter.ts |
✅ Yes (250 lines) | Comprehensive; covers writeCashSuccess with fundingSessionId |
usePendingCheckoutStore.ts |
✅ Yes | Covers TTL, sweep, versioning |
useOnRampPreconnect.ts |
✅ Yes | — |
CheckoutModal.tsx |
✅ Yes | Covers close-guard + pending record scenarios |
sessionClient.ts |
✅ Yes | — |
Test gap summary: 7 new implementation files with no test coverage. The most critical missing test is useOnRampQuote.ts — the debounce + isDebouncePending + isReady state machine drives the CTA gate and is high-impact if it regresses.
Note: widget-provider-transak has no vitest setup (acknowledged in PR description). TransakHost.tsx is not independently testable without a vitest environment — this is a known limitation. The OnRampHostedModals.test.tsx provides integration coverage for the modal lifecycle.
Ticket Quality
- Description: Clear, detailed context with backend surface enumeration. Good.
- AC: 7 items, mostly unambiguous. AC-1 ("warnings") is partially ambiguous — the ticket notes uncertainty about whether warnings are blocking vs advisory. If warnings are always advisory, the ticket should say so explicitly to avoid this gap.
- Discoverability: Linear ticket linked in PR attachment (sub-PR [10] feat(checkout): fiat-first cash funding with live on-ramp quotes #803). Integration PR feat(widget-checkout): checkout mode — on-ramp, deposit & wallet funding #817 body does not link EMB-464 directly in the PR description — it references sub-PRs. Discovery is possible but indirect.
Downstream Impact
OnrampSessionRequestfield rename (walletAddress → depositAddress) is a contract break — if the backend hasn't updated, this is a production outage for all Transak users.FiatCurrencywidened from enum tostring— consumers of this type in downstream packages may need updates (none found in PR scope).OnRampSession.fundingSessionIdis a new field —makeSessioninCheckoutModal.test.tsxomits it (as unknown as OnRampSession), which is fine given the cast but confirms the mock will need updating if the interface evolves.
Ticket Coverage: Partial (6/7 AC fully met; AC-1 has an open warnings display gap)
Generated by lifi-qa-agent[bot] on 2026-07-08
There was a problem hiding this comment.
Requesting changes on 4 items — each requires either a code fix or an explicit acceptance comment with justification before this review is considered complete.
| # | Severity | Type | Issue / File |
|---|---|---|---|
| 1 | 🔴 High | Code | warnings[] from onramp quote is typed but never rendered — AC-1 partially unmet |
| 2 | 🔴 High | Security | CheckoutTransferIdCard.tsx — txLink passed to window.open without protocol validation |
| 3 | 🟠 Medium | Code | staleTime: 0 on quote query causes unnecessary refetch on every page re-mount |
| 4 | 🟠 Medium | Code | seedCurrency clears paymentMethod: null — opening/closing currency page may transiently drop payment method |
1. [High] warnings[] from onramp quote is typed but never rendered
OnrampQuoteResponse.warnings?: string[] exists in the type and is returned by the backend. AC-1 explicitly requires: "typing fiat shows a live funding estimate + fees + warnings". CheckoutReceiveCard renders fees.breakdown and fees.total.amount but does not read onRampQuote.data?.warnings anywhere. The field is silently discarded. Add rendering of warnings[] as advisory alerts/chips in CheckoutReceiveCard, or explicitly mark warnings as deferred scope and update AC-1 to remove "warnings" from its text.
Files: packages/widget-checkout/src/components/CheckoutReceiveCard.tsx, packages/widget-provider/src/checkout/api.ts
2. [High] Security — txLink in CheckoutTransferIdCard.tsx passed to window.open without protocol guard
The Aikido bot flagged CheckoutTransferIdCard.tsx. The window.open(txLink, '_blank', 'noopener') call (noted as fixed with noopener) still passes txLink without validating the URL protocol. If txLink is set to a javascript: URI (e.g. from a malicious backend response or a compromised txLink source), window.open will execute it. Add a protocol guard before the call:
const openTransferIdInExplorer = () => {
if (txLink && /^https?:\/\//i.test(txLink)) {
window.open(txLink, '_blank', 'noopener')
}
}File: packages/widget-checkout/src/pages/CheckoutTransactionDetailsPage/CheckoutTransferIdCard.tsx
3. [Medium] staleTime: 0 on quote query causes unnecessary network requests on navigation
useOnRampQuote uses staleTime: 0 with placeholderData: keepPreviousData. On every re-mount of EnterAmountPage (e.g. returning from SelectCashCurrencyPage), a fresh quote is fetched even if the fiat amount and currency are unchanged. Consider staleTime: 15_000 to tolerate back-navigation without unnecessary backend load.
File: packages/widget-checkout/src/hooks/useOnRampQuote.ts
4. [Medium] seedCurrency sets paymentMethod: null — opening currency page transiently drops payment method
useFiatCurrencyStore.seedCurrency resets paymentMethod: null. SelectCashCurrencyPage calls seedCurrency(data.defaultCurrency) on mount. If the user opens and closes the page without selecting a new currency, the payment method is cleared by seedCurrency and only restored when the auto-select useEffect fires on the unchanged selectedCurrency.paymentOptions[0]. This creates a narrow window where the quote is sent without paymentMethod. Fix seedCurrency to preserve paymentMethod when the currency is unchanged:
seedCurrency: (currency) =>
set((state) =>
state.currencyTouched
? state
: state.currency === currency
? state // no-op if currency unchanged — preserve paymentMethod
: { currency, paymentMethod: null }
),Files: packages/widget-checkout/src/stores/useFiatCurrencyStore.tsx, packages/widget-checkout/src/pages/SelectCashCurrencyPage/SelectCashCurrencyPage.tsx
Additionally, the blocking open item in the PR description must be resolved before merge:
☐ Backend contract verification: confirm the backend accepts
depositAddress(notwalletAddress) onPOST /v1/checkout/onramp/session. If the backend still expectswalletAddress, every Transak session creation is broken.
This cannot be verified by frontend tests — it requires a live backend call against the dev environment or a written confirmation from the CORE team.
💡 Once you've addressed the items above, re-apply the "Agent Review Request" label to trigger an automated re-review.
Keeps the on-hold checkout packages out of npm publishing while the changesets pipeline continues to version them; main-widget releases are unaffected. Remove the flags to un-hold. Also removes three dead files left behind by the stack merges (knip-flagged, zero references): CheckoutDepositContractCallsInit, CheckoutRecipientCard, playground CheckoutWalletProvider.
| } | ||
|
|
||
| const openTransferIdInExplorer = () => { | ||
| window.open(txLink, '_blank', 'noopener') |
There was a problem hiding this comment.
Potential Cross Site Scripting (XSS) via window.location.href - high severity
Using window.location.href or similar redirection methods with unvalidated user input can lead to cross-site scripting (XSS). If an attacker controls part of the URL, they may inject malicious scripts that execute in the victim’s browser, potentially allowing session hijacking, data theft, or other unauthorized actions. Proper validation and encoding of user-controlled values are required before using them in redirects or client-side navigation.
Show fix
Remediation: Avoid using window.location.href or similar redirection methods with unvalidated user input. Only allow redirects to trusted, predefined destinations or validate the input against a strict allowlist. When possible, use fixed URLs or server-side redirection logic, and ensure any user-controlled values are properly validated and encoded before use.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
Overview
Integration PR for widget checkout mode — the full on-ramp / deposit / wallet funding feature, assembled from the 11-PR
checkout/*stack (all already merged intofeat/widget-checkout). This brings ~235 files: a new@lifi/widget-checkoutpackage, Transak & Mesh on-ramp provider packages, checkout session core in@lifi/widget-provider, and playground wiring.Linear
Project: Checkout V1 (team EMB) — "Checkout flow with fiat on-ramp (Transak), CEX funding (Mesh), and deposit addresses (Intent Factory) on Widget V4."
Directly linked in the stack: EMB-464 (quote-aware Transak funding, via #803).
Covered by feature area:
Backend/QA counterparts (context): CORE-371, CORE-398, QAC-6.
Stack (merged into
feat/widget-checkout, in order)Changesets for each already landed with their sub-PR.
Behavior changes for integrators (intentional)
chains.types.WalletProviderfiltersenabledChainTypesby thechains.typesallow/deny config, so a widget configured e.g.chains.types.allow: [EVM]no longer offers Solana/Bitcoin/Sui/Tron wallets in the connect menu (previouslychains.typesonly filtered chains). Intentional per [9] feat(checkout): use any route for wallet payments, EVM-only #794 ("…and the wallet menu (which now honorschains.types)"). Only affects integrators that setchains.types.Deferred follow-ups — address in this PR before merging to
mainThese findings were surfaced during review of the stack and intentionally deferred to a single pass on the assembled branch (rather than per-PR). Checked items were fixed in
dd8893ae(verified by biome/typecheck/vitest + adversarial per-group re-review).Correctness / safety
packages/widget-checkout/src/hooks/useCheckoutTransactionStatus.ts): un-maskisError(drop&& !resolvedStatus) so the already-wired error/retry screen appears (bounded retry comes from the shared query client's default). Resolves both the "swallow-forever spinner" (from [5] feat(checkout): transaction status and details pages #786) and the "poll-stall on a mid-poll error" (from [9] feat(checkout): use any route for wallet payments, EVM-only #794).packages/widget-provider-transak/src/TransakHost.tsx): remove the chainId-1 5xx retry that silently re-requests a session for native ETH — it can fund the wrong asset. (Self-described temporary test hardcode.)TransakHost.tsx): callsetOpen(true)only on the success path (mirrorMeshHost) so failures show the inline error on the status page instead of a blank hosted dialog.packages/widget-provider-mesh/src/MeshHost.tsx): store thecreateLinkhandle and callcloseLink()inclose()/cancel(); clearfailure/depositTxHashinclose(); MMT-awareexpiresAtfor reconnect accounts.useCheckoutExchangesOverride.ts+SelectSourcePage.tsx+EnterAmountPage.tsx): snapshot-based restore of the integrator's exchanges when switching to the wallet flow, so a wallet quote never forwards a staleenabledExchanges=['smartDeposits'].SelectSourcePage.tsx): don't auto-forward to token selection when a wallet eagerly reconnects — require explicit "Pay from wallet" intent.useRoutes.tsadditiveisError+useCheckoutFlowQuote.ts): show Try-again when the routes fetch itself fails (not only the step query).useCheckoutUserId.ts): don't crash on insecure contexts wherecrypto.randomUUIDis unavailable.Backend / QA (blocking)
walletAddress → depositAddressforPOST /v1/checkout/onramp/session. Confirm the backend acceptsdepositAddress— if it still expectswalletAddress, Transak session creation is broken. Note the asymmetry:CexSessionRequest(POST /v1/checkout/cex/session) still sendswalletAddress.feat/widget-checkoutvia 3-way conflict resolution; exercise the checkout flows (Transak/Mesh on-ramp, transfer deposit, wallet-any-route, cash fiat quote, resume/abandon) end-to-end.Low-severity nits
CheckoutTransferIdCardwindow.open(..., '_blank')withoutnoopener.TransferDepositPagewarning copy vs rounded-up amount (reword to "at least"-style).env.d.tsmissingVITE_CHECKOUT_TO_ADDRESS;.env.stagingcomment points at wrong.localfile.Accepted as-is (reviewed, intentional/negligible)
resolveStatusVariantonly honorswalletDisconnectedwhenfundingSource === 'wallet'(commented as intentional).useIsCheckoutBusymemo can go stale across the 24h TTL boundary only with zero store writes (self-heals on any write).Known follow-ups (post-merge, not in this PR)
overrideExchangeswritessmartDepositsinto the persistedli.fi-widget-settingslocalStorage shared with standard widgets on the same origin. The snapshot restore shrinks the window but can't eliminate it — follow-up: exclude checkout-injected keys from persistence or namespace checkout's settings store.CheckoutTransactionPage.tsx(CheckoutDepositAutoStarter) cannot run as written (component unmounts before observing a non-Idle status) — auto-deposit is effectively once-per-tab-session-per-routeId. Left untouched to avoid changing payment auto-trigger semantics; clarify intent and rewire (or remove) deliberately.CheckoutFlowCtaButton, so routes errors show a disabled Pay button.widget-provider-transak/widget-provider-meshhave no vitest setup; host fixes rely on typecheck + review + CI.Testing
mainvia this PR.useCheckoutTransactionStatus.