Skip to content

Commit adb6c97

Browse files
feat(ramps): open order details immediately after UB2 webview callbacks
- Views/Checkout: reset to RampsOrderDetails with callbackUrl + provider params instead of awaiting getOrderFromCallback in the WebView sheet. - useTransakRouting: same pattern for Transak payment webview redirects (delegate fetch to OrderDetails). - OrderDetails: show V2 order toast after callback fetch; optional cryptocurrency route param for toast fallback; status metrics on callback resolution. - rampsNavigation: document optional cryptocurrency on order details params. Tests: Checkout, OrderDetails, useTransakRouting. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4d48e72 commit adb6c97

7 files changed

Lines changed: 167 additions & 347 deletions

File tree

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

Lines changed: 22 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fireEvent, act, waitFor } from '@testing-library/react-native';
44
import Checkout from './Checkout';
55
import renderWithProvider from '../../../../../util/test/renderWithProvider';
66
import { MetaMetricsEvents } from '../../../../../core/Analytics';
7+
import Routes from '../../../../../constants/navigation/Routes';
78
import { callbackBaseUrl } from '../../Aggregator/sdk';
89

910
jest.mock('@react-navigation/native', () => {
@@ -32,11 +33,6 @@ jest.mock('../../hooks/useRampsOrders', () => ({
3233
useRampsOrders: jest.fn(),
3334
}));
3435

35-
jest.mock('../../hooks/useRampsUnifiedV2Enabled', () => ({
36-
__esModule: true,
37-
default: jest.fn(),
38-
}));
39-
4036
jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({
4137
useAnalytics: jest.fn(),
4238
}));
@@ -51,10 +47,6 @@ jest.mock('../../../../../reducers/fiatOrders', () => ({
5147
getRampRoutingDecision: () => null,
5248
}));
5349

54-
jest.mock('../../utils/v2OrderToast', () => ({
55-
showV2OrderToast: jest.fn(),
56-
}));
57-
5850
jest.mock('../../../../../util/Logger', () => ({
5951
error: jest.fn(),
6052
log: jest.fn(),
@@ -186,10 +178,6 @@ const mockUseParams = jest.requireMock(
186178
const mockUseRampsOrders = jest.requireMock('../../hooks/useRampsOrders')
187179
.useRampsOrders as jest.Mock;
188180

189-
const mockUseRampsUnifiedV2Enabled = jest.requireMock(
190-
'../../hooks/useRampsUnifiedV2Enabled',
191-
).default as jest.Mock;
192-
193181
const mockUseAnalytics = jest.requireMock(
194182
'../../../../hooks/useAnalytics/useAnalytics',
195183
).useAnalytics as jest.Mock;
@@ -200,8 +188,6 @@ const mockAddProperties = jest.fn();
200188
const mockBuild = jest.fn();
201189

202190
describe('Checkout', () => {
203-
const mockAddOrder = jest.fn();
204-
const mockGetOrderFromCallback = jest.fn();
205191
const mockAddPrecreatedOrder = jest.fn();
206192
const mockNavigation = {
207193
setOptions: jest.fn(),
@@ -219,11 +205,8 @@ describe('Checkout', () => {
219205
providerName: 'Test Provider',
220206
});
221207
mockUseRampsOrders.mockReturnValue({
222-
addOrder: mockAddOrder,
223-
getOrderFromCallback: mockGetOrderFromCallback,
224208
addPrecreatedOrder: mockAddPrecreatedOrder,
225209
});
226-
mockUseRampsUnifiedV2Enabled.mockReturnValue(false);
227210
mockUseAnalytics.mockReturnValue({
228211
trackEvent: mockTrackEvent,
229212
createEventBuilder: mockCreateEventBuilder,
@@ -261,8 +244,7 @@ describe('Checkout', () => {
261244
});
262245
});
263246

264-
expect(mockGetOrderFromCallback).not.toHaveBeenCalled();
265-
expect(mockAddOrder).not.toHaveBeenCalled();
247+
expect(mockNavigation.reset).not.toHaveBeenCalled();
266248
});
267249

268250
it('does not invoke callback handler when hasCallbackFlow is false', async () => {
@@ -277,8 +259,7 @@ describe('Checkout', () => {
277259
fireEvent.press(getByTestId('trigger-callback-navigation'));
278260
});
279261

280-
expect(mockGetOrderFromCallback).not.toHaveBeenCalled();
281-
expect(mockAddOrder).not.toHaveBeenCalled();
262+
expect(mockNavigation.reset).not.toHaveBeenCalled();
282263
});
283264
});
284265

@@ -468,26 +449,15 @@ describe('Checkout', () => {
468449
});
469450
});
470451

471-
describe('V2 enabled flow', () => {
472-
it('calls showV2OrderToast when V2 is enabled and callback succeeds', async () => {
473-
const { showV2OrderToast } = jest.requireMock(
474-
'../../utils/v2OrderToast',
475-
) as {
476-
showV2OrderToast: jest.Mock;
477-
};
478-
const mockOrder = {
479-
providerOrderId: 'order-v2-1',
480-
cryptoCurrency: { symbol: 'ETH' },
481-
cryptoAmount: '0.5',
482-
status: 'COMPLETED',
483-
};
484-
mockGetOrderFromCallback.mockResolvedValue(mockOrder);
485-
mockUseRampsUnifiedV2Enabled.mockReturnValue(true);
452+
describe('callback success (unified buy stack)', () => {
453+
it('resets navigation to order details with callback params without fetching the order in Checkout', async () => {
454+
const callbackUrl = `${callbackBaseUrl}?orderId=123`;
486455
mockUseParams.mockReturnValue({
487456
url: 'https://provider.example.com/checkout',
488457
providerName: 'Test',
489458
providerCode: 'moonpay',
490459
walletAddress: '0xabc',
460+
cryptocurrency: 'ETH',
491461
});
492462

493463
const { getByTestId } = renderWithProvider(<Checkout />, {}, true, false);
@@ -497,41 +467,21 @@ describe('Checkout', () => {
497467
});
498468

499469
await waitFor(() => {
500-
expect(showV2OrderToast).toHaveBeenCalledWith(
501-
expect.objectContaining({
502-
orderId: 'order-v2-1',
503-
cryptocurrency: 'ETH',
504-
}),
505-
);
506-
});
507-
});
508-
});
509-
510-
describe('callback error handling', () => {
511-
it('sets error when getOrderFromCallback returns null', async () => {
512-
mockGetOrderFromCallback.mockResolvedValue(null);
513-
mockUseParams.mockReturnValue({
514-
url: 'https://provider.example.com/checkout',
515-
providerName: 'Test',
516-
providerCode: 'moonpay',
517-
walletAddress: '0xabc',
518-
});
519-
520-
const { getByTestId, getByText } = renderWithProvider(
521-
<Checkout />,
522-
{},
523-
true,
524-
false,
525-
);
526-
527-
await act(async () => {
528-
fireEvent.press(getByTestId('trigger-callback-navigation'));
529-
});
530-
531-
await waitFor(() => {
532-
expect(
533-
getByText('Order could not be retrieved from callback'),
534-
).toBeOnTheScreen();
470+
expect(mockNavigation.reset).toHaveBeenCalledWith({
471+
index: 0,
472+
routes: [
473+
{
474+
name: Routes.RAMP.RAMPS_ORDER_DETAILS,
475+
params: {
476+
callbackUrl,
477+
providerCode: 'moonpay',
478+
walletAddress: '0xabc',
479+
showCloseButton: true,
480+
cryptocurrency: 'ETH',
481+
},
482+
},
483+
],
484+
});
535485
});
536486
});
537487
});

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

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ import {
2525
type BottomSheetRef,
2626
} from '@metamask/design-system-react-native';
2727
import HeaderCompactStandard from '../../../../../component-library/components-temp/HeaderCompactStandard';
28-
import useRampsUnifiedV2Enabled from '../../hooks/useRampsUnifiedV2Enabled';
29-
import { showV2OrderToast } from '../../utils/v2OrderToast';
3028
import { useStyles } from '../../../../hooks/useStyles';
3129
import styleSheet from './Checkout.styles';
3230
import Device from '../../../../../util/device';
@@ -72,12 +70,9 @@ const Checkout = () => {
7270
const navigation = useNavigation();
7371
const params = useParams<CheckoutParams>();
7472
const { styles } = useStyles(styleSheet, {});
75-
const { addOrder, addPrecreatedOrder, getOrderFromCallback } =
76-
useRampsOrders();
73+
const { addPrecreatedOrder } = useRampsOrders();
7774
const { trackEvent, createEventBuilder } = useAnalytics();
7875
const rampRoutingDecision = useSelector(getRampRoutingDecision);
79-
const isV2Enabled = useRampsUnifiedV2Enabled();
80-
8176
const {
8277
url: uri,
8378
providerCode,
@@ -87,6 +82,7 @@ const Checkout = () => {
8782
network,
8883
userAgent,
8984
onNavigationStateChange,
85+
cryptocurrency,
9086
} = params ?? {};
9187
const effectiveOrderId = (orderIdParam ?? customOrderId)?.trim() || null;
9288

@@ -165,37 +161,21 @@ const Checkout = () => {
165161
throw new Error('No wallet address or provider code available');
166162
}
167163

168-
const rampsOrder = await getOrderFromCallback(
169-
providerCode,
170-
navState.url,
171-
walletAddress,
172-
);
173-
174-
if (!rampsOrder) {
175-
throw new Error('Order could not be retrieved from callback');
176-
}
177-
178-
addOrder(rampsOrder);
179164
dispatch(protectWalletModalVisible());
180165

181-
if (isV2Enabled) {
182-
showV2OrderToast({
183-
orderId: rampsOrder.providerOrderId,
184-
cryptocurrency:
185-
rampsOrder.cryptoCurrency?.symbol ?? params?.cryptocurrency ?? '',
186-
cryptoAmount: rampsOrder.cryptoAmount,
187-
status: rampsOrder.status,
188-
});
189-
}
190-
166+
// Unified buy stack only: leave the WebView immediately; OrderDetails
167+
// resolves the order via callback params (same pattern as external-browser return).
191168
navigation.reset({
192169
index: 0,
193170
routes: [
194171
{
195172
name: Routes.RAMP.RAMPS_ORDER_DETAILS,
196173
params: {
197-
orderId: rampsOrder.providerOrderId,
174+
callbackUrl: navState.url,
175+
providerCode,
176+
walletAddress,
198177
showCloseButton: true,
178+
...(cryptocurrency ? { cryptocurrency } : {}),
199179
},
200180
},
201181
],
@@ -213,10 +193,7 @@ const Checkout = () => {
213193
providerCode,
214194
walletAddress,
215195
navigation,
216-
addOrder,
217-
getOrderFromCallback,
218-
isV2Enabled,
219-
params?.cryptocurrency,
196+
cryptocurrency,
220197
],
221198
);
222199

app/components/UI/Ramp/Views/OrderDetails/OrderDetails.test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ jest.mock('../../../../../util/theme', () => {
4343
};
4444
});
4545

46+
jest.mock('../../utils/v2OrderToast', () => ({
47+
showV2OrderToast: jest.fn(),
48+
}));
49+
50+
const mockHandleOrderStatusChangedForMetrics = jest.fn();
51+
jest.mock(
52+
'../../../../../core/Engine/controllers/ramps-controller/event-handlers/analytics',
53+
() => ({
54+
handleOrderStatusChangedForMetrics: (...args: unknown[]) =>
55+
mockHandleOrderStatusChangedForMetrics(...args),
56+
}),
57+
);
58+
4659
const mockTrackEvent = jest.fn();
4760
jest.mock('../../../../hooks/useAnalytics/useAnalytics', () => ({
4861
useAnalytics: () => ({
@@ -206,6 +219,43 @@ describe('OrderDetails', () => {
206219
expect(mockTrackEvent).toHaveBeenCalled();
207220
});
208221

222+
it('shows V2 order toast when callback fetch succeeds', async () => {
223+
const { showV2OrderToast } = jest.requireMock(
224+
'../../utils/v2OrderToast',
225+
) as { showV2OrderToast: jest.Mock };
226+
const completedOrder = {
227+
providerOrderId: 'ord-cb-1',
228+
status: RampsOrderStatus.Completed,
229+
cryptoCurrency: { symbol: 'ETH' },
230+
cryptoAmount: '0.1',
231+
provider: { id: 'moonpay' },
232+
walletAddress: '0x123',
233+
};
234+
mockUseParams.mockReturnValue({
235+
callbackUrl: 'https://callback.example?x=1',
236+
providerCode: 'moonpay',
237+
walletAddress: '0x123',
238+
});
239+
mockGetOrderById.mockReturnValue(undefined);
240+
mockGetOrderFromCallback.mockResolvedValue(completedOrder);
241+
242+
render();
243+
244+
await waitFor(() => {
245+
expect(showV2OrderToast).toHaveBeenCalledWith(
246+
expect.objectContaining({
247+
orderId: 'ord-cb-1',
248+
cryptocurrency: 'ETH',
249+
}),
250+
);
251+
});
252+
253+
expect(mockHandleOrderStatusChangedForMetrics).toHaveBeenCalledWith({
254+
order: completedOrder,
255+
previousStatus: RampsOrderStatus.Precreated,
256+
});
257+
});
258+
209259
it('shows error state with retry when initial callback fetch fails', async () => {
210260
mockUseParams.mockReturnValue({
211261
callbackUrl: 'metamask://on-ramp/providers/paypal?orderId=abc',
@@ -222,6 +272,7 @@ describe('OrderDetails', () => {
222272
await waitFor(() => {
223273
expect(getByText('Network request failed')).toBeOnTheScreen();
224274
});
275+
expect(mockHandleOrderStatusChangedForMetrics).not.toHaveBeenCalled();
225276
expect(getByText('ramps_order_details.try_again')).toBeOnTheScreen();
226277

227278
await act(async () => {

0 commit comments

Comments
 (0)