Skip to content

Commit c782487

Browse files
fix(ramp): share headless dismissal helper
1 parent 3dadee9 commit c782487

4 files changed

Lines changed: 164 additions & 62 deletions

File tree

app/components/UI/Ramp/Views/Checkout/Checkout.tsx

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ import {
3939
failSession,
4040
getSession,
4141
} from '../../headless/sessionRegistry';
42-
import { setHeadlessEntryCardTouchThrough } from '../../headless/headlessEntryNavigation';
42+
import {
43+
dismissHeadlessFlow,
44+
setHeadlessEntryCardTouchThrough,
45+
} from '../../headless/headlessEntryNavigation';
4346
import { useStyles } from '../../../../hooks/useStyles';
4447
import styleSheet from './Checkout.styles';
4548
import Device from '../../../../../util/device';
@@ -89,12 +92,6 @@ export const createCheckoutNavDetails = createNavigationDetails<CheckoutParams>(
8992
Routes.RAMP.CHECKOUT,
9093
);
9194

92-
interface NavigationWithParents {
93-
getParent?: () => NavigationWithParents | undefined;
94-
goBack?: () => void;
95-
pop?: () => void;
96-
}
97-
9895
const Checkout = () => {
9996
const sheetRef = useRef<BottomSheetRef>(null);
10097
const dispatch = useDispatch();
@@ -200,24 +197,8 @@ const Checkout = () => {
200197
effectiveOrderId,
201198
]);
202199

203-
const dismissHeadlessFlow = useCallback(() => {
204-
const currentNavigation = navigation as NavigationWithParents;
205-
const parentNavigation = currentNavigation.getParent?.();
206-
const outerNavigation = parentNavigation?.getParent?.();
207-
208-
if (outerNavigation?.goBack) {
209-
outerNavigation.goBack();
210-
return;
211-
}
212-
if (outerNavigation?.pop) {
213-
outerNavigation.pop();
214-
return;
215-
}
216-
if (parentNavigation?.pop) {
217-
parentNavigation.pop();
218-
return;
219-
}
220-
currentNavigation.goBack?.();
200+
const dismissActiveHeadlessFlow = useCallback(() => {
201+
dismissHeadlessFlow(navigation);
221202
}, [navigation]);
222203

223204
const failHeadlessCheckout = useCallback(
@@ -226,10 +207,10 @@ const Checkout = () => {
226207
return false;
227208
}
228209
hasTerminatedHeadlessSessionRef.current = true;
229-
dismissHeadlessFlow();
210+
dismissActiveHeadlessFlow();
230211
return true;
231212
},
232-
[headlessSessionId, dismissHeadlessFlow],
213+
[headlessSessionId, dismissActiveHeadlessFlow],
233214
);
234215

235216
useEffect(() => {
@@ -338,7 +319,7 @@ const Checkout = () => {
338319
if (headlessSessionId) {
339320
hasTerminatedHeadlessSessionRef.current = true;
340321
closeSession(headlessSessionId, { reason: 'user_dismissed' });
341-
dismissHeadlessFlow();
322+
dismissActiveHeadlessFlow();
342323
return;
343324
}
344325
// @ts-expect-error navigation prop mismatch
@@ -380,7 +361,7 @@ const Checkout = () => {
380361
hasTerminatedHeadlessSessionRef.current = true;
381362
closeSession(headlessSessionId, { reason: 'completed' });
382363
closeSourceRef.current = 'callback_success';
383-
dismissHeadlessFlow();
364+
dismissActiveHeadlessFlow();
384365
return;
385366
}
386367

@@ -430,7 +411,7 @@ const Checkout = () => {
430411
isV2Enabled,
431412
params?.cryptocurrency,
432413
headlessSessionId,
433-
dismissHeadlessFlow,
414+
dismissActiveHeadlessFlow,
434415
failHeadlessCheckout,
435416
recordUrlChange,
436417
createEventBuilder,
@@ -459,11 +440,11 @@ const Checkout = () => {
459440
if (headlessSessionId) {
460441
hasTerminatedHeadlessSessionRef.current = true;
461442
closeSession(headlessSessionId, { reason: 'user_dismissed' });
462-
dismissHeadlessFlow();
443+
dismissActiveHeadlessFlow();
463444
return;
464445
}
465446
sheetRef.current?.onCloseBottomSheet();
466-
}, [handleCancelPress, headlessSessionId, dismissHeadlessFlow]);
447+
}, [handleCancelPress, headlessSessionId, dismissActiveHeadlessFlow]);
467448

468449
const handleNavigationStateChangeWithDedup = useCallback(
469450
(navState: { url: string }) => {
@@ -545,7 +526,7 @@ const Checkout = () => {
545526
}
546527
hasTerminatedHeadlessSessionRef.current = true;
547528
closeSession(headlessSessionId, { reason: 'user_dismissed' });
548-
dismissHeadlessFlow();
529+
dismissActiveHeadlessFlow();
549530
};
550531
fireClosedRef.current = () => {
551532
if (!hasTrackedScreenViewRef.current) return;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {
2+
dismissHeadlessFlow,
3+
setHeadlessEntryCardTouchThrough,
4+
} from './headlessEntryNavigation';
5+
6+
describe('headlessEntryNavigation', () => {
7+
describe('setHeadlessEntryCardTouchThrough', () => {
8+
it('sets the headless entry card to touch-through', () => {
9+
const setOptions = jest.fn();
10+
const navigation = {
11+
getParent: () => ({
12+
getParent: () => ({
13+
setOptions,
14+
}),
15+
}),
16+
};
17+
18+
expect(setHeadlessEntryCardTouchThrough(navigation, true)).toBe(true);
19+
20+
expect(setOptions).toHaveBeenCalledWith({
21+
cardStyle: {
22+
backgroundColor: 'transparent',
23+
pointerEvents: 'none',
24+
},
25+
});
26+
});
27+
28+
it('restores the headless entry card to interactive', () => {
29+
const setOptions = jest.fn();
30+
const navigation = {
31+
getParent: () => ({
32+
getParent: () => ({
33+
setOptions,
34+
}),
35+
}),
36+
};
37+
38+
expect(setHeadlessEntryCardTouchThrough(navigation, false)).toBe(true);
39+
40+
expect(setOptions).toHaveBeenCalledWith({
41+
cardStyle: {
42+
backgroundColor: 'transparent',
43+
pointerEvents: 'auto',
44+
},
45+
});
46+
});
47+
48+
it('returns false when the headless entry navigator cannot be found', () => {
49+
expect(setHeadlessEntryCardTouchThrough(undefined, true)).toBe(false);
50+
expect(
51+
setHeadlessEntryCardTouchThrough({ getParent: () => undefined }, true),
52+
).toBe(false);
53+
});
54+
});
55+
56+
describe('dismissHeadlessFlow', () => {
57+
it('prefers the outer navigator goBack', () => {
58+
const outerGoBack = jest.fn();
59+
const outerPop = jest.fn();
60+
const parentPop = jest.fn();
61+
const currentGoBack = jest.fn();
62+
const navigation = {
63+
goBack: currentGoBack,
64+
getParent: () => ({
65+
pop: parentPop,
66+
getParent: () => ({
67+
goBack: outerGoBack,
68+
pop: outerPop,
69+
}),
70+
}),
71+
};
72+
73+
expect(dismissHeadlessFlow(navigation)).toBe(true);
74+
75+
expect(outerGoBack).toHaveBeenCalledTimes(1);
76+
expect(outerPop).not.toHaveBeenCalled();
77+
expect(parentPop).not.toHaveBeenCalled();
78+
expect(currentGoBack).not.toHaveBeenCalled();
79+
});
80+
81+
it('falls back through outer pop, parent pop, then current goBack', () => {
82+
const outerPop = jest.fn();
83+
expect(
84+
dismissHeadlessFlow({
85+
getParent: () => ({
86+
getParent: () => ({
87+
pop: outerPop,
88+
}),
89+
}),
90+
}),
91+
).toBe(true);
92+
expect(outerPop).toHaveBeenCalledTimes(1);
93+
94+
const parentPop = jest.fn();
95+
expect(
96+
dismissHeadlessFlow({
97+
getParent: () => ({
98+
pop: parentPop,
99+
getParent: () => undefined,
100+
}),
101+
}),
102+
).toBe(true);
103+
expect(parentPop).toHaveBeenCalledTimes(1);
104+
105+
const currentGoBack = jest.fn();
106+
expect(dismissHeadlessFlow({ goBack: currentGoBack })).toBe(true);
107+
expect(currentGoBack).toHaveBeenCalledTimes(1);
108+
});
109+
110+
it('returns false when there is no navigation action available', () => {
111+
expect(dismissHeadlessFlow(undefined)).toBe(false);
112+
expect(dismissHeadlessFlow({ getParent: () => undefined })).toBe(false);
113+
});
114+
});
115+
});

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ type HeadlessEntryPointerEvents = 'auto' | 'none';
22

33
interface NavigationNode {
44
getParent?: () => NavigationNode | undefined;
5+
goBack?: () => void;
6+
pop?: () => void;
57
setOptions?: (options: {
68
cardStyle?: {
79
backgroundColor: 'transparent';
@@ -27,3 +29,28 @@ export const setHeadlessEntryCardTouchThrough = (
2729
});
2830
return true;
2931
};
32+
33+
export const dismissHeadlessFlow = (
34+
navigation: NavigationNode | undefined,
35+
): boolean => {
36+
const parentNavigation = navigation?.getParent?.();
37+
const outerNavigation = parentNavigation?.getParent?.();
38+
39+
if (outerNavigation?.goBack) {
40+
outerNavigation.goBack();
41+
return true;
42+
}
43+
if (outerNavigation?.pop) {
44+
outerNavigation.pop();
45+
return true;
46+
}
47+
if (parentNavigation?.pop) {
48+
parentNavigation.pop();
49+
return true;
50+
}
51+
if (navigation?.goBack) {
52+
navigation.goBack();
53+
return true;
54+
}
55+
return false;
56+
};

app/components/UI/Ramp/hooks/useTransakRouting.ts

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
failSession,
3434
getSession,
3535
} from '../headless/sessionRegistry';
36+
import { dismissHeadlessFlow } from '../headless/headlessEntryNavigation';
3637

3738
interface RampStackParamList {
3839
/** `baseRouteParams` (e.g. `headlessSessionId`) are merged onto this route in resets — see `navigateToVerifyIdentityCallback`. */
@@ -106,12 +107,6 @@ interface UseTransakRoutingConfig {
106107
baseRouteParams?: Record<string, unknown>;
107108
}
108109

109-
interface NavigationWithParents {
110-
getParent?: () => NavigationWithParents | undefined;
111-
goBack?: () => void;
112-
pop?: () => void;
113-
}
114-
115110
export const useTransakRouting = (config?: UseTransakRoutingConfig) => {
116111
const baseRoute = config?.baseRoute;
117112
const baseRouteParams = config?.baseRouteParams;
@@ -151,24 +146,8 @@ export const useTransakRouting = (config?: UseTransakRoutingConfig) => {
151146
const processingOrderIdRef = useRef<string | null>(null);
152147
const { addOrder, refreshOrder } = useRampsOrders();
153148

154-
const dismissHeadlessFlow = useCallback(() => {
155-
const currentNavigation = navigation as unknown as NavigationWithParents;
156-
const parentNavigation = currentNavigation.getParent?.();
157-
const outerNavigation = parentNavigation?.getParent?.();
158-
159-
if (outerNavigation?.goBack) {
160-
outerNavigation.goBack();
161-
return;
162-
}
163-
if (outerNavigation?.pop) {
164-
outerNavigation.pop();
165-
return;
166-
}
167-
if (parentNavigation?.pop) {
168-
parentNavigation.pop();
169-
return;
170-
}
171-
currentNavigation.goBack?.();
149+
const dismissActiveHeadlessFlow = useCallback(() => {
150+
dismissHeadlessFlow(navigation);
172151
}, [navigation]);
173152

174153
const {
@@ -370,7 +349,7 @@ export const useTransakRouting = (config?: UseTransakRoutingConfig) => {
370349
);
371350
}
372351
closeSession(headlessSessionId, { reason: 'completed' });
373-
dismissHeadlessFlow();
352+
dismissActiveHeadlessFlow();
374353
return;
375354
}
376355
navigation.reset({
@@ -383,7 +362,7 @@ export const useTransakRouting = (config?: UseTransakRoutingConfig) => {
383362
],
384363
});
385364
},
386-
[navigation, headlessSessionId, dismissHeadlessFlow],
365+
[navigation, headlessSessionId, dismissActiveHeadlessFlow],
387366
);
388367

389368
const navigateToAdditionalVerificationCallback = useCallback(
@@ -431,7 +410,7 @@ export const useTransakRouting = (config?: UseTransakRoutingConfig) => {
431410
if (!orderId) {
432411
if (headlessSessionId) {
433412
closeSession(headlessSessionId, { reason: 'user_dismissed' });
434-
dismissHeadlessFlow();
413+
dismissActiveHeadlessFlow();
435414
}
436415
return;
437416
}
@@ -502,7 +481,7 @@ export const useTransakRouting = (config?: UseTransakRoutingConfig) => {
502481
message: 'useTransakRouting: Failed to process order after checkout',
503482
});
504483
if (failSession(headlessSessionId, error)) {
505-
dismissHeadlessFlow();
484+
dismissActiveHeadlessFlow();
506485
}
507486
}
508487
},
@@ -515,7 +494,7 @@ export const useTransakRouting = (config?: UseTransakRoutingConfig) => {
515494
regionIsoCode,
516495
trackEvent,
517496
headlessSessionId,
518-
dismissHeadlessFlow,
497+
dismissActiveHeadlessFlow,
519498
],
520499
);
521500

0 commit comments

Comments
 (0)