refactor(ramp): extract useContinueWithQuote hook (Phase 4)#29213
Conversation
Add a phased plan under app/components/UI/Ramp/headless/ outlining how to expose the Unified Buy v2 flow to external consumers via a useHeadlessBuy hook and a dev-only playground screen, including the session registry pattern, the Headless Host stack-base solution for the Transak auth loop, and the order-callback path replacing the order processing redirect.
Introduces a dev-mode-only playground screen reachable from the Buy/Sell settings (gated by isInternalBuild) to incrementally prototype the useHeadlessBuy hook. The screen currently surfaces useRampsController state in collapsible sections for region, providers, tokens and payment methods, lets the user select a provider/token, attempts to default to mUSD on Linea and the Transak native provider when available, and shows a summary of the active selections.
Introduce a read-only `useHeadlessBuy` hook that wraps `useRampsController` and exposes the catalog (tokens, providers, paymentMethods, countries), `userRegion`, `orders`, `getOrderById`, an aggregated `isLoading` / `errors` surface, and a `getQuotes(params)` helper that resolves the wallet address from the asset's chain id so external callers can fetch quotes without pre-seeding controller state. Wire the playground to the new hook: amount input, country/payment-method pickers, "Get quotes" button, and a quote list that renders the resolved payment method name, fee breakdown, reliability score and tag badges. A clearly delimited "headless consumer simulation" section uses hardcoded asset/payment/provider IDs that can be overridden from the pickers above (with Reset links) so the hook is exercised in isolation from the controller.
Add a module-level session registry that holds non-serializable headless buy callbacks keyed by sessionId, and a startHeadlessBuy API on useHeadlessBuy that creates a session and navigates into the existing BuildQuote screen with the headlessSessionId on params. The screen does not branch on the param yet — Phase 3 only plumbs the id and validates the lifecycle end-to-end through the playground's event log. Includes the Phase 3.1 follow-up: startHeadlessBuy no longer writes to RampsController. The previous pre-seed called setSelectedPaymentMethod and setSelectedProvider with raw ids, but those setters take full PaymentMethod / Provider objects and the catalog can still be loading at that point. Inputs now live on the HeadlessSession only; the destination screen resolves them when the catalog is hydrated. Playground gains a noticeable headless-consumer simulation block with a "Start headless buy" button, a cancel control and an event log so we can exercise the lifecycle (started → onOrderCreated / onError / onClose → ended) without leaving the dev sandbox. PLAN.md is updated with Phase 3.1 (this fix), an expanded Phase 4 description, a new Phase 5b (quote-first headless start path) and an expanded Phase 8 that captures the auto-onClose-on-dismissal work and the closeSession idempotency contract.
Lift the aggregator and native "continue with quote" logic out of
BuildQuote into a new useContinueWithQuote hook so both BuildQuote and
the upcoming Headless Host (Phase 5) can drive the post-quote flow.
The hook exposes `continueWithQuote(quote, { amount, assetId })` with a
throw-on-failure contract: each failure path calls reportRampsError
(preserving the Logger/Sentry side effect) exactly once and throws an
Error whose message is a user-facing string. Callers catch to drive
their own UI. Loading state and the RAMPS_CONTINUE_BUTTON_CLICKED
analytics event stay with the caller.
Widget path uses two sequential try/catch blocks — fetch, then use —
so the no-URL branch can report with its own context without the outer
catch double-reporting.
BuildQuote drops the two handlers (~180 lines), the
navigateAfterExternalBrowser helper, and the now-unused
useTransakController / useTransakRouting /
selectHasAgreedTransakNativePolicy / getBuyWidgetData / addPrecreatedOrder
wiring; handleContinuePress is now a thin wrapper that fires analytics
then awaits the hook inside try/catch/finally around isContinueLoading
and rampsError.
Tests: the native-provider, widget-provider, and
navigateAfterExternalBrowser describes move from BuildQuote.test.tsx
into useContinueWithQuote.test.ts alongside new error-contract tests.
BuildQuote.test.tsx keeps a small handleContinuePress wiring describe
that mocks the hook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c41a11c. Configure here.
|
@copilot resolve the merge conflicts in this pull request |
1 similar comment
|
@copilot resolve the merge conflicts in this pull request |
…licts Co-authored-by: wachunei <1024246+wachunei@users.noreply.github.com>
Resolved in b53044a. Two conflicts in
|
AI PR Analysis🚫 Merge safe: false | 🟠 Risk: high
AI analysis did not complete. Manual review recommended. |
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection: Key observations:
The risk is medium because: the critical "Continue" button path in the buy flow was refactored, and any subtle behavioral difference (e.g., error handling, loading state management) could break the on-ramp purchase flow. The unit tests cover the hook thoroughly, but E2E tests are needed to validate the full user journey. SmokeRamps is the correct and only tag needed — it directly tests fiat on-ramp and off-ramp features including the buy flow that was modified. No other feature areas are impacted by this change. Performance Test Selection: |
|
|
✅ E2E Fixture Validation — Schema is up to date |
|
Should we add a video to make sure there are no regressions for UB2 although this looks like a simple refactor? |
added |




Description
This draft PR stacks the Headless Buy proof-of-concept work through Phase 4 on top of
main. It continues the incremental sequence started by #29144 (Phases 1–3 + 3.1) and is intended for incremental review and CI validation before follow-up phases (Headless Host, skip-BuildQuote, routing callbacks).Scope vs
main(full branch)useHeadlessBuy,sessionRegistry,startHeadlessBuy, BuildQuoteheadlessSessionIdplumbing).handleWidgetProviderContinue(~111 lines) andhandleNativeProviderContinue(~60 lines) plus the localnavigateAfterExternalBrowserhelper out ofBuildQuote.tsxinto a newuseContinueWithQuote(quote, ctx)hook so both BuildQuote and the upcoming Headless Host (Phase 5) can drive the post-quote flow without copy-paste. No user-visible change and no new public surface — the hook is internal toapp/components/UI/Ramp.Diff vs previous POC branches (incremental)
main...poc/headless-buy-phase-1PLAN.mdscaffold.poc/headless-buy-phase-1...poc/headless-buy-phase-2useHeadlessBuy,types, barrel, playground wiring to hook (getQuotes, amount, quotes UI, sandbox, i18n).poc/headless-buy-phase-2...poc/headless-buy-phase-3sessionRegistry+ tests,startHeadlessBuy+ tests, BuildQuote param + nav test, playground session lifecycle UI + tests,PLAN.mdupdates.poc/headless-buy-phase-3...poc/headless-buy-phase-4BuildQuote.tsx; +465 in hook + tests)useContinueWithQuote.ts+useContinueWithQuote.test.ts, BuildQuote refactor to consume it, BuildQuote.test.tsx trim to wiring,PLAN.mdPhase 4 ticked.Full branch vs
main:main...poc/headless-buy-phase-4.What Phase 4 actually does
app/components/UI/Ramp/hooks/useContinueWithQuote.ts:useContinueWithQuote(): { continueWithQuote: (quote: Quote, ctx: { amount: number; assetId: string }) => Promise<void> }.isNativeProvider(quote)to a private native path (Transak:checkExistingToken→getBuyQuote→routeAfterAuthentication, or EnterEmail / VerifyIdentity when unauthenticated) or widget path (fetch widget URL viagetBuyWidgetData, then either in-app Checkout or external browser viaLinking/InAppBrowserwith anavigateAfterExternalBrowserreset).reportRampsError(preserves Logger/Sentry side effect) exactly once per failure and throws anErrorwhosemessageis a user-facing string. Widget path uses two sequential try/catch blocks (fetch, then use) so the no-URL branch can report with its own context without the outer catch double-reporting. Callerscatchto drive their own UI.isContinueLoading,rampsError, andRAMPS_CONTINUE_BUTTON_CLICKEDanalytics. Those stay with the caller.app/components/UI/Ramp/Views/BuildQuote/BuildQuote.tsx:navigateAfterExternalBrowser, and the now-unuseduseTransakController/useTransakRouting/selectHasAgreedTransakNativePolicy/getBuyWidgetData/addPrecreatedOrderwiring (~18 imports removed).handleContinuePressis now a thin wrapper: firesRAMPS_CONTINUE_BUTTON_CLICKEDanalytics, then awaitscontinueWithQuote(selectedQuote, { amount, assetId })insidetry/catch/finallyaroundisContinueLoadingandrampsError.BuildQuote.test.tsxintouseContinueWithQuote.test.ts(native provider, widget provider,navigateAfterExternalBrowser), plus a newerror contractdescribe asserting the thrownError.messagematchesreportRampsError's return value and that reporting fires exactly once.BuildQuote.test.tsxgets a slimhandleContinuePress wiringdescribe that mocksuseContinueWithQuoteand asserts (a) correct args, (b) analytics fires before the hook call, (c) rejection surfaces in the rampsError banner, (d) no call whenselectedProvideris null.PLAN.md: Phase 4 checkbox ticked. No body edits.Intentionally out of scope for this PR (follow-up phases)
useTransakRoutingreset base. The hook still readscurrency/selectedToken.chainId/selectedPaymentMethod.id/selectedProvider.namefromuseRampsController— these will return null for headless callers who don't pre-seed the controller (Phase 3.1). A// TODO(phase-5)comment in the hook flags the debt.onOrderCreated.Changelog
CHANGELOG entry: (Internal) Extracted post-quote continuation logic from BuildQuote into a new
useContinueWithQuotehook — no user-visible change.Related issues
Fixes:
Jira: TRAM-3527
Manual testing steps
Screenshots/Recordings
Before
N/A
After
N/A — Phase 4 is a pure refactor. No UI changes.
native.mp4
agg.mp4
Pre-merge author checklist
Performance checks (if applicable)
trace()for usage andaddTokenfor an exampleFor performance guidelines and tooling, see the Performance Guide.
Pre-merge reviewer checklist
Note
Medium Risk
Refactors core buy-flow continuation/navigation (native Transak + widget/external-browser paths) into a shared hook; behavior should be unchanged but small mismatches could break checkout routing or error surfacing.
Overview
Extracts the post-quote “Continue” logic from
BuildQuoteinto a new reusableuseContinueWithQuotehook that handles both native (Transak) and widget/aggregator flows, including external-browser/InAppBrowser routing and navigation resets.BuildQuoteis simplified to fire analytics, toggle loading, callcontinueWithQuote(selectedQuote, { amount, assetId }), and display the thrown user-facing error message. Tests are reorganized accordingly: detailed continuation behavior moves touseContinueWithQuote.test.ts, whileBuildQuote.test.tsxnow focuses on wiring/ordering/error surfacing.PLAN.mdmarks Phase 4 complete.Reviewed by Cursor Bugbot for commit d3aa850. Bugbot is set up for automated code reviews on this repo. Configure here.