Skip to content

refactor(ramp): extract useContinueWithQuote hook (Phase 4)#29213

Merged
wachunei merged 7 commits into
mainfrom
poc/headless-buy-phase-4
Apr 23, 2026
Merged

refactor(ramp): extract useContinueWithQuote hook (Phase 4)#29213
wachunei merged 7 commits into
mainfrom
poc/headless-buy-phase-4

Conversation

@saustrie-consensys

@saustrie-consensys saustrie-consensys commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

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)

  • Phases 1–3 + 3.1 — unchanged from #29144 (playground, useHeadlessBuy, sessionRegistry, startHeadlessBuy, BuildQuote headlessSessionId plumbing).
  • Phase 4 — this PR — pure refactor of the post-quote continuation logic. Extract handleWidgetProviderContinue (~111 lines) and handleNativeProviderContinue (~60 lines) plus the local navigateAfterExternalBrowser helper out of BuildQuote.tsx into a new useContinueWithQuote(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 to app/components/UI/Ramp.

Diff vs previous POC branches (incremental)

Compare Stat (approx.) What it adds
main...poc/headless-buy-phase-1 Phase 1 only Playground screen, route, Settings row, PLAN.md scaffold.
poc/headless-buy-phase-1...poc/headless-buy-phase-2 +~2.3k lines useHeadlessBuy, types, barrel, playground wiring to hook (getQuotes, amount, quotes UI, sandbox, i18n).
poc/headless-buy-phase-2...poc/headless-buy-phase-3 +~1k lines sessionRegistry + tests, startHeadlessBuy + tests, BuildQuote param + nav test, playground session lifecycle UI + tests, PLAN.md updates.
poc/headless-buy-phase-3...poc/headless-buy-phase-4 +314/−251 (net −228 in BuildQuote.tsx; +465 in hook + tests) useContinueWithQuote.ts + useContinueWithQuote.test.ts, BuildQuote refactor to consume it, BuildQuote.test.tsx trim to wiring, PLAN.md Phase 4 ticked.

Full branch vs main: main...poc/headless-buy-phase-4.

What Phase 4 actually does

  • New app/components/UI/Ramp/hooks/useContinueWithQuote.ts:
    • Signature: useContinueWithQuote(): { continueWithQuote: (quote: Quote, ctx: { amount: number; assetId: string }) => Promise<void> }.
    • Internally dispatches via isNativeProvider(quote) to a private native path (Transak: checkExistingTokengetBuyQuoterouteAfterAuthentication, or EnterEmail / VerifyIdentity when unauthenticated) or widget path (fetch widget URL via getBuyWidgetData, then either in-app Checkout or external browser via Linking / InAppBrowser with a navigateAfterExternalBrowser reset).
    • Error contract: on failure the hook calls reportRampsError (preserves Logger/Sentry side effect) exactly once per failure and throws an Error whose message is 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. Callers catch to drive their own UI.
    • Not managed by the hook: isContinueLoading, rampsError, and RAMPS_CONTINUE_BUTTON_CLICKED analytics. Those stay with the caller.
  • Refactored app/components/UI/Ramp/Views/BuildQuote/BuildQuote.tsx:
    • Drops the two handlers, navigateAfterExternalBrowser, and the now-unused useTransakController / useTransakRouting / selectHasAgreedTransakNativePolicy / getBuyWidgetData / addPrecreatedOrder wiring (~18 imports removed).
    • handleContinuePress is now a thin wrapper: fires RAMPS_CONTINUE_BUTTON_CLICKED analytics, then awaits continueWithQuote(selectedQuote, { amount, assetId }) inside try/catch/finally around isContinueLoading and rampsError.
  • Tests:
    • Three behavior describes migrated verbatim from BuildQuote.test.tsx into useContinueWithQuote.test.ts (native provider, widget provider, navigateAfterExternalBrowser), plus a new error contract describe asserting the thrown Error.message matches reportRampsError's return value and that reporting fires exactly once.
    • BuildQuote.test.tsx gets a slim handleContinuePress wiring describe that mocks useContinueWithQuote and asserts (a) correct args, (b) analytics fires before the hook call, (c) rejection surfaces in the rampsError banner, (d) no call when selectedProvider is null.
  • PLAN.md: Phase 4 checkbox ticked. No body edits.

Intentionally out of scope for this PR (follow-up phases)

  • Phase 4b — Headless Host screen + parameterized useTransakRouting reset base. The hook still reads currency / selectedToken.chainId / selectedPaymentMethod.id / selectedProvider.name from useRampsController — 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.
  • Phase 5 — Skip BuildQuote in headless mode; Headless Host consumes the hook directly.
  • Phase 6 — Bypass order-processing redirect + fire onOrderCreated.

Changelog

CHANGELOG entry: (Internal) Extracted post-quote continuation logic from BuildQuote into a new useContinueWithQuote hook — no user-visible change.

Related issues

Fixes:

Jira: TRAM-3527

Manual testing steps

Feature: Buy flow regression (Phase 4 is a pure refactor — behavior must match main)

  Scenario: Aggregator widget provider (in-app Checkout)
    Given the user is on BuildQuote with a valid amount and an aggregator provider (e.g. MoonPay) selected
    When the user taps Continue
    Then the in-app Checkout WebView opens with the widget URL and correct provider branding
    And no rampsError banner is shown

  Scenario: Aggregator widget provider (external browser / PayPal custom action)
    Given the user is on BuildQuote with a valid amount and a widget provider with useExternalBrowser=true
    When the user taps Continue
    Then the widget URL opens in the external browser (or in-app browser when available)
    And on success the app resets to the order-details screen with the callback URL
    And on cancel the app resets back to BuildQuote

  Scenario: Native provider (Transak), authenticated
    Given the user is on BuildQuote with a valid amount and Transak selected
    And the user already has a valid Transak token
    When the user taps Continue
    Then a fresh Transak buy quote is fetched and the post-authentication flow advances (KYC / webview / bank details)

  Scenario: Native provider (Transak), unauthenticated — first time
    Given the user is on BuildQuote with a valid amount and Transak selected
    And the user has NOT agreed to the Transak native policy
    When the user taps Continue
    Then the VerifyIdentity screen opens with amount / currency / assetId

  Scenario: Native provider (Transak), unauthenticated — returning
    Given the user is on BuildQuote with a valid amount and Transak selected
    And the user has previously agreed to the Transak native policy
    When the user taps Continue
    Then the EnterEmail screen opens with amount / currency / assetId

  Scenario: Widget fetch error surfaces the same banner as before
    Given the user is on BuildQuote with a valid amount and a widget provider selected
    And the widget-data endpoint fails (e.g. network error, missing URL)
    When the user taps Continue
    Then the rampsError banner shows the same user-facing message as on main
    And the continue button becomes enabled again (spinner stops)

  Scenario: Headless Playground flow unchanged
    Given the user is on the Headless Buy playground (internal build)
    When the user taps Start headless buy and then Cancel headless session
    Then the session lifecycle + event log behaves exactly as on Phase 3 (BuildQuote opens, cancel fires onClose consumer_cancelled)

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)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

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 BuildQuote into a new reusable useContinueWithQuote hook that handles both native (Transak) and widget/aggregator flows, including external-browser/InAppBrowser routing and navigation resets.

BuildQuote is simplified to fire analytics, toggle loading, call continueWithQuote(selectedQuote, { amount, assetId }), and display the thrown user-facing error message. Tests are reorganized accordingly: detailed continuation behavior moves to useContinueWithQuote.test.ts, while BuildQuote.test.tsx now focuses on wiring/ordering/error surfacing. PLAN.md marks Phase 4 complete.

Reviewed by Cursor Bugbot for commit d3aa850. Bugbot is set up for automated code reviews on this repo. Configure here.

wachunei and others added 5 commits April 20, 2026 13:42
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>
@saustrie-consensys saustrie-consensys self-assigned this Apr 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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.

@saustrie-consensys saustrie-consensys added team-money-movement issues related to Money Movement features size-XL labels Apr 22, 2026
@saustrie-consensys saustrie-consensys marked this pull request as ready for review April 22, 2026 20:12
@saustrie-consensys saustrie-consensys requested review from a team as code owners April 22, 2026 20:12

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread app/components/UI/Ramp/hooks/useContinueWithQuote.ts
@wachunei

Copy link
Copy Markdown
Member

@copilot resolve the merge conflicts in this pull request

1 similar comment
@wachunei

Copy link
Copy Markdown
Member

@copilot resolve the merge conflicts in this pull request

…licts

Co-authored-by: wachunei <1024246+wachunei@users.noreply.github.com>

Copilot AI commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in b53044a. Two conflicts in BuildQuote.tsx (import and const declarations) and one in PLAN.md:

  • Import conflict — dropped selectProviderAutoSelected (Phase 4 moved native logic to useContinueWithQuote; main also removed it) and Device (not used in Phase 4's slimmed-down component).
  • Const conflict — dropped both providerAutoSelected and hasAgreedTransakNativePolicy declarations (both are now inside useContinueWithQuote).
  • PLAN.md conflict — kept Phase 4 as [x] (completed by this branch).

@wachunei wachunei removed request for a team and wachunei April 23, 2026 14:09
@github-actions

Copy link
Copy Markdown
Contributor

AI PR Analysis

🚫 Merge safe: false | 🟠 Risk: high

Merge decision: AI analysis did not complete — manual review required before merging.

AI analysis did not complete. Manual review recommended.

View run

@github-actions github-actions Bot added the risk:high AI analysis: high risk label Apr 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeRamps
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 92%
click to see 🤖 AI reasoning details

E2E Test Selection:
The PR is a pure refactoring of the Ramp (on-ramp/off-ramp) buy flow. The core logic for handling the "Continue" button press in BuildQuote — routing through native Transak flows or widget provider flows — has been extracted from BuildQuote.tsx into a new reusable hook useContinueWithQuote. The behavior is identical; only the code organization changed (Phase 4 of the headless ramp architecture plan).

Key observations:

  1. useContinueWithQuote.ts is a new file containing the extracted logic from BuildQuote's handleNativeProviderContinue and handleWidgetProviderContinue functions
  2. BuildQuote.tsx now delegates to continueWithQuote() from the new hook instead of calling inline handlers
  3. All test files are updated to reflect the new hook boundary
  4. The PLAN.md marks Phase 4 as complete

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:
This is a pure code refactoring — extracting inline handler functions into a reusable hook. No new rendering, no new data fetching, no new state management, and no changes to UI components or animations. There is no performance impact from this change.

View GitHub Actions results

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

E2E Fixture Validation — Schema is up to date
12 value mismatches detected (expected — fixture represents an existing user).
View details

@wachunei wachunei enabled auto-merge April 23, 2026 15:34
@amitabh94

Copy link
Copy Markdown
Contributor

Should we add a video to make sure there are no regressions for UB2 although this looks like a simple refactor?

@wachunei

Copy link
Copy Markdown
Member

Should we add a video to make sure there are no regressions for UB2 although this looks like a simple refactor?

added

@wachunei wachunei added this pull request to the merge queue Apr 23, 2026
Merged via the queue into main with commit eec13f3 Apr 23, 2026
98 of 100 checks passed
@wachunei wachunei deleted the poc/headless-buy-phase-4 branch April 23, 2026 19:34
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 23, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.76.0 Issue or pull request that will be included in release 7.76.0 label Apr 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.76.0 Issue or pull request that will be included in release 7.76.0 risk:high AI analysis: high risk size-XL team-money-movement issues related to Money Movement features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants