Skip to content

Commit 2f282b9

Browse files
committed
fix(ramps): route external-browser callbacks through order details
1 parent 6946419 commit 2f282b9

6 files changed

Lines changed: 231 additions & 51 deletions

File tree

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -476,12 +476,6 @@ describe('BuildQuote', () => {
476476
type: 'success',
477477
url: 'metamask://on-ramp/providers/moonpay?orderId=ord-123',
478478
});
479-
mockGetOrderFromCallback.mockResolvedValue({
480-
providerOrderId: 'ord-123',
481-
status: 'Pending',
482-
cryptoAmount: '0.05',
483-
cryptoCurrency: { symbol: 'ETH' },
484-
});
485479
mockGetBuyWidgetData.mockResolvedValue({
486480
url: 'https://widget.example.com/checkout',
487481
browser: 'IN_APP_OS_BROWSER',
@@ -497,14 +491,18 @@ describe('BuildQuote', () => {
497491
});
498492

499493
await waitFor(() => {
500-
expect(mockAddOrder).toHaveBeenCalled();
494+
expect(mockAddOrder).not.toHaveBeenCalled();
495+
expect(mockGetOrderFromCallback).not.toHaveBeenCalled();
501496
expect(mockNavigationReset).toHaveBeenCalledWith({
502497
index: 0,
503498
routes: [
504499
{
505500
name: Routes.RAMP.RAMPS_ORDER_DETAILS,
506501
params: {
507-
orderId: 'ord-123',
502+
callbackUrl:
503+
'metamask://on-ramp/providers/moonpay?orderId=ord-123',
504+
providerCode: 'moonpay',
505+
walletAddress: '0x1234567890123456789012345678901234567890',
508506
showCloseButton: true,
509507
},
510508
},

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

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
getWidgetRedirectConfig,
2121
} from '../../utils/buildQuoteWithRedirectUrl';
2222
import { computeAmountUpdate } from '../../utils/computeAmountUpdate';
23-
import { extractOrderCode } from '../../utils/extractOrderCode';
2423
import { getRampCallbackBaseUrl } from '../../utils/getRampCallbackBaseUrl';
2524
import { getNavigateAfterExternalBrowserRoutes } from '../../utils/rampsNavigation';
2625
import { reportRampsError } from '../../utils/reportRampsError';
@@ -176,8 +175,6 @@ function BuildQuote() {
176175
paymentMethods,
177176
getBuyWidgetData,
178177
addPrecreatedOrder,
179-
addOrder,
180-
getOrderFromCallback,
181178
paymentMethodsLoading,
182179
paymentMethodsFetching,
183180
paymentMethodsStatus,
@@ -683,36 +680,17 @@ function BuildQuote() {
683680
return;
684681
}
685682

686-
try {
687-
const order = await getOrderFromCallback(
688-
providerCode,
689-
result.url,
690-
effectiveWallet,
691-
);
692-
693-
if (!order || isBailedOrderStatus(order.status)) {
694-
navigateAfterExternalBrowser({ returnDestination: 'buildQuote' });
695-
return;
696-
}
697-
698-
addOrder(order);
699-
700-
const rawOrderId = order.providerOrderId ?? effectiveOrderId;
701-
if (!rawOrderId) {
702-
navigateAfterExternalBrowser({ returnDestination: 'buildQuote' });
703-
return;
704-
}
705-
706-
const orderCode = extractOrderCode(rawOrderId);
707-
navigateAfterExternalBrowser({
708-
returnDestination: 'order',
709-
orderCode,
710-
providerCode,
711-
walletAddress: effectiveWallet || undefined,
712-
});
713-
} catch {
683+
if (!effectiveWallet) {
714684
navigateAfterExternalBrowser({ returnDestination: 'buildQuote' });
685+
return;
715686
}
687+
688+
navigateAfterExternalBrowser({
689+
returnDestination: 'order',
690+
callbackUrl: result.url,
691+
providerCode,
692+
walletAddress: effectiveWallet,
693+
});
716694
} finally {
717695
InAppBrowser.closeAuth();
718696
}
@@ -757,8 +735,6 @@ function BuildQuote() {
757735
navigation,
758736
getBuyWidgetData,
759737
addPrecreatedOrder,
760-
getOrderFromCallback,
761-
addOrder,
762738
navigateAfterExternalBrowser,
763739
]);
764740

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,29 @@ import { RampsOrderStatus } from '@metamask/ramps-controller';
1111

1212
const mockSetOptions = jest.fn();
1313
const mockNavigate = jest.fn();
14+
const mockSetParams = jest.fn();
15+
const mockReset = jest.fn();
1416
jest.mock('@react-navigation/native', () => ({
1517
...jest.requireActual('@react-navigation/native'),
1618
useNavigation: () => ({
1719
setOptions: mockSetOptions,
1820
navigate: mockNavigate,
1921
goBack: jest.fn(),
22+
setParams: mockSetParams,
23+
reset: mockReset,
2024
}),
2125
}));
2226

2327
const mockGetOrderById = jest.fn();
2428
const mockRefreshOrder = jest.fn();
29+
const mockGetOrderFromCallback = jest.fn();
30+
const mockAddOrder = jest.fn();
2531
jest.mock('../../hooks/useRampsOrders', () => ({
2632
useRampsOrders: () => ({
2733
getOrderById: mockGetOrderById,
2834
refreshOrder: mockRefreshOrder,
35+
getOrderFromCallback: mockGetOrderFromCallback,
36+
addOrder: mockAddOrder,
2937
}),
3038
}));
3139

@@ -187,4 +195,32 @@ describe('OrderDetails', () => {
187195
const result = createRampsOrderDetailsNavDetails();
188196
expect(result[0]).toBe(Routes.RAMP.RAMPS_ORDER_DETAILS);
189197
});
198+
199+
it('shows error state with retry when initial callback fetch fails', async () => {
200+
mockUseParams.mockReturnValue({
201+
callbackUrl: 'metamask://on-ramp/providers/paypal?orderId=abc',
202+
providerCode: 'paypal',
203+
walletAddress: '0x123',
204+
});
205+
mockGetOrderById.mockReturnValue(undefined);
206+
mockGetOrderFromCallback.mockRejectedValue(
207+
new Error('Network request failed'),
208+
);
209+
210+
const { getByText } = render();
211+
212+
await waitFor(() => {
213+
expect(getByText('Network request failed')).toBeOnTheScreen();
214+
});
215+
expect(getByText('ramps_order_details.try_again')).toBeOnTheScreen();
216+
217+
fireEvent.press(getByText('ramps_order_details.try_again'));
218+
expect(mockGetOrderFromCallback).toHaveBeenCalledTimes(2);
219+
expect(mockGetOrderFromCallback).toHaveBeenNthCalledWith(
220+
2,
221+
'paypal',
222+
'metamask://on-ramp/providers/paypal?orderId=abc',
223+
'0x123',
224+
);
225+
});
190226
});

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

Lines changed: 140 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
normalizeProviderCode,
1616
RampsOrderStatus,
1717
} from '@metamask/ramps-controller';
18+
import { isBailedOrderStatus } from '../BuildQuote/BuildQuote';
1819
import { extractOrderCode } from '../../utils/extractOrderCode';
20+
import { getNavigateAfterExternalBrowserRoutes } from '../../utils/rampsNavigation';
1921
import Button, {
2022
ButtonVariants,
2123
ButtonSize,
@@ -37,8 +39,11 @@ import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
3739
import { MetaMetricsEvents } from '../../../../../core/Analytics';
3840
import { RampsOrderDetailsSelectorsIDs } from './OrderDetails.testIds';
3941
interface RampsOrderDetailsParams {
40-
orderId: string;
42+
orderId?: string;
4143
showCloseButton?: boolean;
44+
callbackUrl?: string;
45+
providerCode?: string;
46+
walletAddress?: string;
4247
}
4348

4449
export const createRampsOrderDetailsNavDetails =
@@ -69,19 +74,75 @@ const styles = StyleSheet.create({
6974

7075
const OrderDetails = () => {
7176
const params = useParams<RampsOrderDetailsParams>();
72-
const { getOrderById, refreshOrder } = useRampsOrders();
77+
const { getOrderById, refreshOrder, getOrderFromCallback, addOrder } =
78+
useRampsOrders();
7379
const orderCode = params.orderId ? extractOrderCode(params.orderId) : '';
7480
const order = getOrderById(orderCode);
7581
const isPending = order ? PENDING_STATUSES.has(order.status) : false;
82+
const hasCallbackParams = Boolean(
83+
params.callbackUrl && params.providerCode && params.walletAddress,
84+
);
7685

77-
const [isLoading, setIsLoading] = useState(isPending);
86+
const [isLoading, setIsLoading] = useState(isPending || hasCallbackParams);
7887
const [error, setError] = useState<string | null>(null);
7988
const theme = useTheme();
8089
const { colors } = theme;
8190
const navigation = useNavigation();
8291
const { trackEvent, createEventBuilder } = useAnalytics();
8392

8493
const [isRefreshing, setIsRefreshing] = useState(false);
94+
const hasFetchedFromCallback = useRef(false);
95+
96+
const handleRetryCallbackFetch = useCallback(async () => {
97+
if (!params.callbackUrl || !params.providerCode || !params.walletAddress) {
98+
return;
99+
}
100+
try {
101+
setError(null);
102+
setIsLoading(true);
103+
const fetchedOrder = await getOrderFromCallback(
104+
params.providerCode,
105+
params.callbackUrl,
106+
params.walletAddress,
107+
);
108+
if (!fetchedOrder || isBailedOrderStatus(fetchedOrder.status)) {
109+
navigation.reset({
110+
index: 0,
111+
routes: getNavigateAfterExternalBrowserRoutes({
112+
returnDestination: 'buildQuote',
113+
}),
114+
});
115+
return;
116+
}
117+
addOrder(fetchedOrder);
118+
navigation.setParams({
119+
orderId: fetchedOrder.providerOrderId,
120+
callbackUrl: undefined,
121+
providerCode: undefined,
122+
walletAddress: undefined,
123+
});
124+
} catch (fetchError) {
125+
Logger.error(fetchError as Error, {
126+
message:
127+
'RampsOrderDetails: error fetching order from callback URL (retry)',
128+
callbackUrl: params.callbackUrl,
129+
});
130+
setError(
131+
fetchError instanceof Error && fetchError.message
132+
? fetchError.message
133+
: strings('ramps_order_details.error_message'),
134+
);
135+
} finally {
136+
setIsLoading(false);
137+
}
138+
}, [
139+
params.callbackUrl,
140+
params.providerCode,
141+
params.walletAddress,
142+
getOrderFromCallback,
143+
addOrder,
144+
navigation,
145+
]);
85146

86147
useEffect(() => {
87148
navigation.setOptions(
@@ -148,12 +209,79 @@ const OrderDetails = () => {
148209
}, [order, refreshOrder]);
149210

150211
useEffect(() => {
151-
if (isPending) {
212+
if (isPending && !hasCallbackParams) {
152213
handleOnRefresh();
153214
}
154215
// eslint-disable-next-line react-hooks/exhaustive-deps
155216
}, []);
156217

218+
useEffect(() => {
219+
if (
220+
!hasCallbackParams ||
221+
hasFetchedFromCallback.current ||
222+
!params.callbackUrl ||
223+
!params.providerCode ||
224+
!params.walletAddress
225+
) {
226+
return;
227+
}
228+
hasFetchedFromCallback.current = true;
229+
230+
const providerCode = params.providerCode;
231+
const callbackUrl = params.callbackUrl;
232+
const walletAddress = params.walletAddress;
233+
if (!providerCode || !callbackUrl || !walletAddress) return;
234+
235+
const fetchFromCallback = async () => {
236+
try {
237+
setError(null);
238+
const fetchedOrder = await getOrderFromCallback(
239+
providerCode,
240+
callbackUrl,
241+
walletAddress,
242+
);
243+
if (!fetchedOrder || isBailedOrderStatus(fetchedOrder.status)) {
244+
navigation.reset({
245+
index: 0,
246+
routes: getNavigateAfterExternalBrowserRoutes({
247+
returnDestination: 'buildQuote',
248+
}),
249+
});
250+
return;
251+
}
252+
addOrder(fetchedOrder);
253+
navigation.setParams({
254+
orderId: fetchedOrder.providerOrderId,
255+
callbackUrl: undefined,
256+
providerCode: undefined,
257+
walletAddress: undefined,
258+
});
259+
} catch (fetchError) {
260+
Logger.error(fetchError as Error, {
261+
message: 'RampsOrderDetails: error fetching order from callback URL',
262+
callbackUrl,
263+
});
264+
setError(
265+
fetchError instanceof Error && fetchError.message
266+
? fetchError.message
267+
: strings('ramps_order_details.error_message'),
268+
);
269+
} finally {
270+
setIsLoading(false);
271+
}
272+
};
273+
274+
fetchFromCallback();
275+
}, [
276+
hasCallbackParams,
277+
params.callbackUrl,
278+
params.providerCode,
279+
params.walletAddress,
280+
getOrderFromCallback,
281+
addOrder,
282+
navigation,
283+
]);
284+
157285
if (isLoading) {
158286
return (
159287
<ScreenLayout>
@@ -166,11 +294,10 @@ const OrderDetails = () => {
166294
);
167295
}
168296

169-
if (!order) {
170-
return <ScreenLayout />;
171-
}
172-
173297
if (error) {
298+
const onRetry = hasCallbackParams
299+
? handleRetryCallbackFetch
300+
: handleOnRefresh;
174301
return (
175302
<ScreenLayout>
176303
<ScreenLayout.Body>
@@ -198,14 +325,18 @@ const OrderDetails = () => {
198325
size={ButtonSize.Lg}
199326
width={ButtonWidthTypes.Full}
200327
label={strings('ramps_order_details.try_again')}
201-
onPress={handleOnRefresh}
328+
onPress={onRetry}
202329
/>
203330
</Box>
204331
</ScreenLayout.Body>
205332
</ScreenLayout>
206333
);
207334
}
208335

336+
if (!order) {
337+
return <ScreenLayout />;
338+
}
339+
209340
return (
210341
<ScreenLayout testID={RampsOrderDetailsSelectorsIDs.CONTAINER}>
211342
<ScrollView

0 commit comments

Comments
 (0)