Skip to content

Commit eec13f3

Browse files
saustrie-consensyswachuneiclaudeCopilot
authored
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

0 commit comments

Comments
 (0)