You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(ramp): add Phase 9.5 (Host visual treatment) and Phase 9 timeout open question
Captures decisions and open questions from the May 6 2026 design thread
(https://consensys.slack.com/archives/C0AK3NXRM7W/p1778072992397499) on
how MetaMask Pay consumes the headless flow.
- New Phase 9.5: HeadlessHost visual treatment. Pedro confirmed the
Host must stay mounted (routing landing pad + nativeFlowError
surface) but doesn't have to be visible. Two shapes evaluated:
transparent overlay with consumer-rendered spinner (Pedro's pick) or
bottom-sheet with the Host's own spinner. Final shape pending
Lucas's design rec (May 13). Phase 8's dismissal contract is
unaffected — back-press still fires onClose(user_dismissed).
- Phase 9 Update: append the timeout open question Barbara raised
(reply 36). Two API shapes worth considering during Phase 9
implementation: timeoutMs on awaitOrderTerminalState, or a
registry-side per-session timeout that fires
onError(TIMED_OUT) + onClose. Not blocking for v1.
- Resolve a naming conflict: the auto-select-best-provider utility was
tentatively listed as "Phase 9.5"; renamed to "follow-up phase" so
the new visual-treatment phase can own the 9.5 number.
@@ -553,7 +554,9 @@ Phase 9 is now an MVP requirement, not playground polish. MetaMask Pay's `Transa
553
554
554
555
The original Phase 9 surface (`getOrder`, `refreshOrder` + a "Refresh order" playground button) doesn't fit a controller consumer. The Phase 9 API should add an **imperative `awaitOrderTerminalState(orderId)` Promise** so TPC can `await` settlement instead of polling itself. Polling and the playground button stay as additional surfaces; the Promise is the load-bearing API.
555
556
556
-
The Apr 28 progress sync also called out a missing **auto-select-best-provider utility** ("Need utility function to auto-select best provider rather than requiring explicit provider ID"). Either fold into Phase 9 alongside `getOrder`, or split as Phase 9.5 — to be decided during Phase 9 implementation.
557
+
The Apr 28 progress sync also called out a missing **auto-select-best-provider utility** ("Need utility function to auto-select best provider rather than requiring explicit provider ID"). Either fold into Phase 9 alongside `getOrder` or split as a follow-up phase — to be decided during Phase 9 implementation.
558
+
559
+
**Open question** (Barbara, [May 6 design thread](https://consensys.slack.com/archives/C0AK3NXRM7W/p1778072992397499)): does ramps need an internal timeout — distinct from the registry's 1-hour GC — so the consumer isn't pinned to a "loading forever" state when a quote stalls? Two shapes worth considering during Phase 9 implementation: (i) `awaitOrderTerminalState(orderId, { timeoutMs })` rejects with a timed-out error so the consumer can decide what to surface; (ii) the registry grows a per-session timeout that fires `onError({ code: 'TIMED_OUT' })` + `onClose` if no terminal event arrives within N seconds. Pedro's reply 37 in the same thread noted Phase 3's `cancel()` is today's escape hatch; not blocking for v1 but worth resolving before MMPay launch.
557
560
558
561
Goal: complete the hook surface.
559
562
@@ -564,6 +567,40 @@ Goal: complete the hook surface.
564
567
565
568
---
566
569
570
+
## Phase 9.5 — HeadlessHost visual treatment
571
+
572
+
Goal: make production HeadlessHost stop rendering user-visible chrome (header, spinner, cancel button, error text). The Host stays mounted — it is still the routing landing pad and the `nativeFlowError` surface from Phase 4b — but becomes either fully transparent or a bottom-sheet, so the consumer (TPC / MMPay) renders the only user-visible loading UI during a headless buy.
573
+
574
+
Driver: [May 6 2026 design thread](https://consensys.slack.com/archives/C0AK3NXRM7W/p1778072992397499). Pedro confirmed the Host can't be removed (it's the routing base and Phase 7's error surface) but it doesn't have to be visible. Two shapes were evaluated: (a) transparent overlay with consumer-rendered spinner, (b) bottom-sheet with the Host's own spinner and cancel action. Pedro favored (a). Final shape pending Lucas's design recommendation (deadline May 13).
575
+
576
+
### Settled in the same thread
577
+
578
+
- **Back navigation stays available during loading** (Lucas, May 8 16:54 reply 39). Phase 8's `useHeadlessSessionDismissal` already fires `onClose({ reason: 'user_dismissed' })` on unmount, so the consumer's back-press still produces a terminal callback regardless of Host visibility.
- Cancel-session (Phase 3 `cancel()`) is the escape hatch for stuck quotes (Pedro, reply 37).
581
+
582
+
### Implementation sketch
583
+
584
+
- Strip `HeaderCompactStandard`, `ActivityIndicator`, the loader/no-session/error `Text` blocks, and the bottom `Cancel` button from [HeadlessHost.tsx](../Views/HeadlessHost/HeadlessHost.tsx).
585
+
- The wrapping `SafeAreaView` becomes either fully transparent (option a) or sized to the bottom-sheet spec (option b, per Lucas's design).
586
+
- The local `errorMessage` state becomes redundant once the consumer renders all UI; remove it. `failSession` / `closeSession` calls stay — the consumer receives the error via `onError`.
587
+
- Keep the orchestration `useEffect`, the `nativeFlowError` handler, and `useHeadlessSessionDismissal` untouched — they are behavior, not chrome.
588
+
- Update `HeadlessHost.test.tsx`: drop the visual-presence assertions, keep all orchestration / dismissal / `nativeFlowError` tests.
589
+
590
+
### Out of scope
591
+
592
+
- The MMPay consumer-side loading UI (TPC PR [`MetaMask/core#8628`](https://github.com/MetaMask/core/pull/8628) and downstream MMPay UI work).
593
+
- Internal Host timeout — see the Open question in the Phase 9 Update.
- [UB2 loading-states reference](https://www.figma.com/design/ItZzm9CzSAjOWQTUKsOdSk/BUY?node-id=4347-3909) (Lorenzo, reply 23) for design parity.
599
+
600
+
Deliverable: HeadlessHost is invisible (or bottom-sheet) and MMPay's TPC renders the only user-visible loading UI during a headless buy. Phase 8's dismissal contract continues to work unchanged.
> Goal 1 (primary): implement the deferred Phase 5b — `startHeadlessBuy({ assetId, amount, paymentMethodId, providerId? })` raw-params start mode, where the Host fetches quotes itself and auto-picks one. See the existing "Phase 5b (deferred)" section above for the full spec.
0 commit comments