Commit eec13f3
refactor(ramp): extract useContinueWithQuote hook (Phase 4) (#29213)
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->
## **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](#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](#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`](https://github.com/MetaMask/metamask-mobile/compare/main...poc/headless-buy-phase-1?expand=0)
| Phase 1 only | Playground screen, route, Settings row, `PLAN.md`
scaffold. |
|
[`poc/headless-buy-phase-1...poc/headless-buy-phase-2`](https://github.com/MetaMask/metamask-mobile/compare/poc/headless-buy-phase-1...poc/headless-buy-phase-2?expand=0)
| +~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`](https://github.com/MetaMask/metamask-mobile/compare/poc/headless-buy-phase-2...poc/headless-buy-phase-3?expand=0)
| +~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`](https://github.com/MetaMask/metamask-mobile/compare/poc/headless-buy-phase-3...poc/headless-buy-phase-4?expand=0)
| +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`](https://github.com/MetaMask/metamask-mobile/compare/main...poc/headless-buy-phase-4?expand=0).
### 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: `checkExistingToken` → `getBuyQuote` →
`routeAfterAuthentication`, 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:
## **Manual testing steps**
```gherkin
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.
https://github.com/user-attachments/assets/22fcea1f-2b8b-4536-a6ec-0b0ff2aa569f
https://github.com/user-attachments/assets/ba4f1b6d-edf4-43c5-9ab2-84128e21ba3d
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
#### Performance checks (if applicable)
- [x] I've tested on Android
- Ideally on a mid-range device; emulator is acceptable
- [x] I've tested with a power user scenario
- Use these [power-user
SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [x] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **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.
<!-- Generated with the help of the pr-description AI skill -->
<!-- CURSOR_SUMMARY -->
---
> [!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.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d3aa850. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Pedro Pablo Aste Kompen <wachunei@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wachunei <1024246+wachunei@users.noreply.github.com>1 parent d5fcd00 commit eec13f3
5 files changed
Lines changed: 901 additions & 586 deletions
File tree
- app/components/UI/Ramp
- Views/BuildQuote
- headless
- hooks
0 commit comments