Skip to content

Commit 89c4209

Browse files
feat(ramp): add transparent HEADLESS_ENTRY route (Phase 9.5 Fix #1)
After the Phase 9.5 chrome strip made HeadlessHost invisible, Goktug's testing showed the user still sees a solid-white screen because the headless flow enters via RAMP.TOKEN_SELECTION — an opaque V2 stack card that sits between the consumer screen and the now-transparent Host. Resolution: add a new outer slot RAMP.HEADLESS_ENTRY in MainNavigator that mounts the same TokenListRoutes component as TOKEN_SELECTION, but wraps it in the JS-stack transparent-overlay preset that BridgeModals, EarnModals, MoneyModals, StakeModals, and PerpsModals already use: options={{ ...clearStackNavigatorOptionsWithTransitionAnimation, presentation: 'transparentModal', }} useHeadlessBuy.startHeadlessBuy now navigates to HEADLESS_ENTRY instead of TOKEN_SELECTION; the nested-screen descriptor that lands directly on HEADLESS_HOST stays unchanged. UB2's entry through TOKEN_SELECTION is untouched. Slack thread: https://consensys.slack.com/archives/C0AK3NXRM7W/p1778577403631269
1 parent ef11a28 commit 89c4209

7 files changed

Lines changed: 49 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Changed
1111

1212
- Aligned previously base-enabled custom network logos (Stable, Flow, XDC, Fraxtal, Hemi, Plasma, Lukso, Rootstock, MSU, Lens, Plume) to a square format consistent with Popular networks (#29943)
13+
- Made the Ramp Headless Host invisible so consumer-rendered loading UI is visible during a headless buy; added a transparent `RAMP.HEADLESS_ENTRY` outer route so the headless flow no longer renders an opaque V2 stack card on top of the consumer screen (Phase 9.5).
1314

1415
## [7.75.1]
1516

app/components/Nav/Main/MainNavigator.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,14 @@ const MainNavigator = () => {
11731173
name={Routes.RAMP.TOKEN_SELECTION}
11741174
component={TokenListRoutes}
11751175
/>
1176+
<Stack.Screen
1177+
name={Routes.RAMP.HEADLESS_ENTRY}
1178+
component={TokenListRoutes}
1179+
options={{
1180+
...clearStackNavigatorOptionsWithTransitionAnimation,
1181+
presentation: 'transparentModal',
1182+
}}
1183+
/>
11761184
<Stack.Screen
11771185
name={Routes.RAMP.BUY}
11781186
options={{

app/components/UI/Ramp/headless/PLAN.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,33 @@ Driver: [May 6 2026 design thread](https://consensys.slack.com/archives/C0AK3NXR
621621
622622
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.
623623
624+
### Phase 9.5 follow-ups (driven by Goktug's May 12 2026 testing feedback)
625+
626+
Goktug tested the chrome-stripped Host on May 12 and filed three complaints in the [follow-up thread](https://consensys.slack.com/archives/C0AK3NXRM7W/p1778577403631269).
627+
628+
#### Fix #1 — Transparent stack wrapper (`RAMP.HEADLESS_ENTRY`)
629+
630+
After the chrome strip, the user still saw a solid-white screen because [MainNavigator.js:1172-1175](../../Nav/Main/MainNavigator.js) mounts the headless flow under `RAMP.TOKEN_SELECTION`, an opaque V2 stack card. The card sits between the consumer screen and the transparent Host.
631+
632+
Resolution: add a new outer slot `RAMP.HEADLESS_ENTRY` (in [Routes.ts](../../../../constants/navigation/Routes.ts)) that mounts the same `TokenListRoutes` component but with the JS-stack transparent-overlay preset:
633+
634+
```js
635+
<Stack.Screen
636+
name={Routes.RAMP.HEADLESS_ENTRY}
637+
component={TokenListRoutes}
638+
options={{
639+
...clearStackNavigatorOptionsWithTransitionAnimation,
640+
presentation: 'transparentModal',
641+
}}
642+
/>
643+
```
644+
645+
This is the same pattern MainNavigator already uses for `BridgeModals`, `EarnModals`, `MoneyModals`, `StakeModals`, and `PerpsModals`. UB2's entry through `RAMP.TOKEN_SELECTION` stays untouched.
646+
647+
`useHeadlessBuy.startHeadlessBuy` now navigates to `RAMP.HEADLESS_ENTRY` instead of `RAMP.TOKEN_SELECTION`; the nested-screen descriptor (`{ screen: TOKEN_SELECTION, params: { screen: HEADLESS_HOST, ... } }`) is unchanged so it still lands directly on the Host.
648+
649+
Safety net for the unverified `beforeRemove` + `transparentModal` interaction: Phase 8's `useHeadlessSessionDismissal` fires `closeSession({ reason: 'user_dismissed' })` on unmount when `HEADLESS_HOST` is no longer in the navigator tree. `closeSession` idempotency means both paths can fire without producing duplicate `onClose` callbacks.
650+
624651
---
625652
626653
## Phase 10 — Implement deferred Phase 5b + playground polish

app/components/UI/Ramp/headless/useHeadlessBuy.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ jest.mock('../../../../constants/navigation/Routes', () => ({
3535
BUY: 'RampBuy',
3636
TOKEN_SELECTION: 'RampTokenSelection',
3737
HEADLESS_HOST: 'RampHeadlessHost',
38+
HEADLESS_ENTRY: 'RampHeadlessEntry',
3839
},
3940
},
4041
}));
@@ -1261,7 +1262,7 @@ describe('useHeadlessBuy', () => {
12611262
if (!started) {
12621263
throw new Error('startHeadlessBuy did not return a session');
12631264
}
1264-
expect(mockNavigate).toHaveBeenCalledWith('RampTokenSelection', {
1265+
expect(mockNavigate).toHaveBeenCalledWith('RampHeadlessEntry', {
12651266
screen: 'RampTokenSelection',
12661267
params: {
12671268
screen: 'RampHeadlessHost',

app/components/UI/Ramp/headless/useHeadlessBuy.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -496,16 +496,19 @@ export function useHeadlessBuy(): HeadlessBuyResult {
496496
// The Headless Host is registered inside the Unified Buy v2 stack
497497
// (`app/components/UI/Ramp/routes.tsx` → `MainRoutes`) so it lives
498498
// next to every post-auth reset target (`Checkout`, `BasicInfo`,
499-
// `KycWebview`, …). From outside that stack we have to enter the
500-
// V2 stack via its outer mount point (`RAMP.TOKEN_SELECTION` in
501-
// `MainNavigator.js`, which renders `TokenListRoutes`) and hand
502-
// React Navigation a nested-screen descriptor:
503-
// RAMP.TOKEN_SELECTION (outer)
499+
// `KycWebview`, …). From outside that stack we enter via the
500+
// dedicated transparent-modal mount point — `RAMP.HEADLESS_ENTRY`
501+
// in `MainNavigator.js`, which renders the same `TokenListRoutes`
502+
// as `RAMP.TOKEN_SELECTION` but wraps it in a transparent overlay
503+
// (preset: `clearStackNavigatorOptionsWithTransitionAnimation` +
504+
// `presentation: 'transparentModal'`) so the consumer's screen
505+
// stays visible behind the headless flow.
506+
// RAMP.HEADLESS_ENTRY (outer transparent modal)
504507
// → RAMP.TOKEN_SELECTION (RootStack slot wrapping `MainRoutes`)
505508
// → RAMP.HEADLESS_HOST (target screen on the inner stack)
506509
// Resetting the Host's nearest navigator (the `MainRoutes` inner
507510
// stack) then resolves all the `useTransakRouting` targets.
508-
navigation.navigate(Routes.RAMP.TOKEN_SELECTION, {
511+
navigation.navigate(Routes.RAMP.HEADLESS_ENTRY, {
509512
screen: Routes.RAMP.TOKEN_SELECTION,
510513
params: {
511514
screen: Routes.RAMP.HEADLESS_HOST,

app/constants/navigation/Routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const Routes = {
2323
ACTIVATION_KEY_FORM: 'RampActivationKeyForm',
2424
HEADLESS_PLAYGROUND: 'RampHeadlessPlayground',
2525
HEADLESS_HOST: 'RampHeadlessHost',
26+
HEADLESS_ENTRY: 'RampHeadlessEntry',
2627
AMOUNT_INPUT: 'RampAmountInput',
2728
ENTER_EMAIL: 'RampEnterEmail',
2829
OTP_CODE: 'RampOtpCode',

app/core/NavigationService/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ export interface RootStackParamList extends ParamListBase {
264264
RampBuy: RampBuySellParams | undefined;
265265
RampSell: RampBuySellParams | undefined;
266266
RampTokenSelection: undefined;
267+
RampHeadlessEntry: undefined;
267268
GetStarted: undefined;
268269
/**
269270
* BuildQuote route is shared between:

0 commit comments

Comments
 (0)