Skip to content

Commit bcdaa5c

Browse files
authored
fix: buy flow quote loading and provider selection (#28373)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** **Quotes felt slow to load** on Buy (Continue / amount screen loading, modals sluggish) partly because every quote path passed **`forceRefresh: true`** and React Query used **`staleTime: 0`**, so **`getQuotes` almost always bypassed the ramps controller cache** (15s TTL) even when token, amount, provider, and payment method were unchanged. This PR sets **`RAMPS_QUOTES_STALE_TIME_MS` (15s)** on the quotes query—aligned with **`RampsController` `DEFAULT_QUOTES_TTL`**—and **stops passing `forceRefresh`** from Build Quote, provider selection, and payment selection so controller + React Query can reuse responses within that window. **Provider list correctness:** The provider selection modal filtered the visible list using only the route **`assetId`**. When that param was absent but **`selectedToken`** was set, the UI could still show every provider while **`getQuotes`** used the token from **`selectedToken`**, inflating quote work and mismatching the list vs quotes. This change uses the same **effective asset id** as quote fetching (`paramAssetId ?? selectedToken?.assetId`) to derive **`displayProviders`** from **`supportedCryptoCurrencies`**, so only providers that support the current asset appear and are included in quote params. ## **Changelog** CHANGELOG entry: Fixed buy flow provider selection so only providers that support the selected asset are shown; improved quote responsiveness by aligning React Query cache with the ramps quotes TTL (15s) and avoiding unnecessary `forceRefresh` on each fetch. ## **Related issues** Fixes: [TRAM-3425](https://consensyssoftware.atlassian.net/browse/TRAM-3425) ## **Manual testing steps** ```gherkin Feature: Ramp buy flow quotes and provider selection Scenario: Provider list matches token support without route asset id Given user is in the MetaMask buy flow with a token selected When user opens the provider selection screen Then only providers that declare support for that token asset id are listed Scenario: Quotes reuse cache within a short window when inputs unchanged Given user is on Build Quote with token, provider, payment method, and amount set When user navigates away and back to the same screen without changing those inputs Then quote loading should not repeatedly pay full cold-fetch latency where the 15s cache applies ``` ## **Screenshots/Recordings** <div> <a href="https://www.loom.com/share/4884506a4acd4ff2a20454dc62490b09"> <p>Simulator - Money Movement Sim - 3 April 2026 - Watch Video</p> </a> <a href="https://www.loom.com/share/4884506a4acd4ff2a20454dc62490b09"> <img style="max-width:300px;" src="https://cdn.loom.com/sessions/thumbnails/4884506a4acd4ff2a20454dc62490b09-ebf6caaf52f2a4f2-full-play.gif#t=0.1"> </a> </div> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
1 parent 9e56b9f commit bcdaa5c

8 files changed

Lines changed: 65 additions & 15 deletions

File tree

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,6 @@ function BuildQuote() {
431431
redirectUrl: getRampCallbackBaseUrl(),
432432
paymentMethods: [selectedPaymentMethod.id],
433433
providers: [selectedProvider.id],
434-
forceRefresh: true,
435434
}
436435
: null,
437436
[

app/components/UI/Ramp/Views/Modals/PaymentSelectionModal/PaymentSelectionModal.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,6 @@ describe('PaymentSelectionModal', () => {
358358
'/payments/debit-credit-card-1',
359359
'/payments/debit-credit-card-2',
360360
],
361-
forceRefresh: true,
362361
});
363362
});
364363

app/components/UI/Ramp/Views/Modals/PaymentSelectionModal/PaymentSelectionModal.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ function PaymentSelectionModal() {
9595
assetId,
9696
providers: selectedProvider ? [selectedProvider.id] : undefined,
9797
paymentMethods: paymentMethodIds,
98-
forceRefresh: true,
9998
}
10099
: null,
101100
[

app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.test.tsx

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ jest.mock('react-native', () => {
6363
};
6464
});
6565

66+
const defaultTestAssetId = 'eip155:1/slip44:60';
67+
6668
const mockProviders = [
6769
{
6870
id: '/providers/transak',
@@ -77,6 +79,7 @@ const mockProviders = [
7779
height: 24,
7880
width: 90,
7981
},
82+
supportedCryptoCurrencies: { [defaultTestAssetId]: true },
8083
},
8184
{
8285
id: '/providers/moonpay',
@@ -91,6 +94,7 @@ const mockProviders = [
9194
height: 24,
9295
width: 90,
9396
},
97+
supportedCryptoCurrencies: { [defaultTestAssetId]: true },
9498
},
9599
];
96100

@@ -212,7 +216,6 @@ describe('ProviderSelectionModal', () => {
212216
assetId: 'eip155:1/slip44:60',
213217
providers: ['/providers/transak', '/providers/moonpay'],
214218
paymentMethods: ['/payments/debit-credit-card-1'],
215-
forceRefresh: true,
216219
}),
217220
);
218221
});
@@ -246,6 +249,29 @@ describe('ProviderSelectionModal', () => {
246249
expect(mockUseRampsQuotes).toHaveBeenCalledWith(null);
247250
});
248251

252+
it('filters providers by selectedToken.assetId when route assetId is omitted', () => {
253+
mockUseParams.mockReturnValue({ amount: 100, skipQuotes: true });
254+
mockUseRampsController.mockImplementation(() => ({
255+
...defaultControllerReturn,
256+
providers: [
257+
{
258+
...mockProviders[0],
259+
supportedCryptoCurrencies: { [defaultTestAssetId]: true },
260+
},
261+
{
262+
...mockProviders[1],
263+
supportedCryptoCurrencies: { [defaultTestAssetId]: false },
264+
},
265+
],
266+
}));
267+
const { getByText, queryByText } = renderWithProvider(
268+
ProviderSelectionModal,
269+
);
270+
271+
expect(getByText('Transak')).toBeOnTheScreen();
272+
expect(queryByText('MoonPay')).toBeNull();
273+
});
274+
249275
it('filters providers by assetId when provided', () => {
250276
const assetId = 'eip155:1/erc20:0x123';
251277
mockUseParams.mockReturnValue({ assetId, skipQuotes: true });
@@ -254,16 +280,25 @@ describe('ProviderSelectionModal', () => {
254280
providers: [
255281
{
256282
...mockProviders[0],
257-
supportedCryptoCurrencies: { [assetId]: true },
283+
supportedCryptoCurrencies: {
284+
[assetId]: true,
285+
[defaultTestAssetId]: false,
286+
},
258287
},
259288
{
260289
...mockProviders[1],
261-
supportedCryptoCurrencies: { [assetId]: true },
290+
supportedCryptoCurrencies: {
291+
[assetId]: true,
292+
[defaultTestAssetId]: false,
293+
},
262294
},
263295
{
264296
id: '/providers/other',
265297
name: 'Other',
266-
supportedCryptoCurrencies: { 'eip155:1/slip44:60': true },
298+
supportedCryptoCurrencies: {
299+
[defaultTestAssetId]: true,
300+
[assetId]: false,
301+
},
267302
environmentType: 'PRODUCTION',
268303
description: '',
269304
hqAddress: '',

app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/ProviderSelectionModal.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,19 @@ function ProviderSelectionModal() {
8282
'';
8383
const assetId = paramAssetId ?? selectedToken?.assetId ?? '';
8484

85+
/**
86+
* Only list (and quote) providers that support the effective asset. Uses the
87+
* same id as `getQuotes` (`paramAssetId ?? selectedToken?.assetId`), so flows
88+
* without route `assetId` still filter when `selectedToken` is set.
89+
*/
8590
const displayProviders = useMemo(() => {
86-
if (!paramAssetId) return providers;
91+
if (!assetId) {
92+
return providers;
93+
}
8794
return providers.filter(
88-
(p) => p.supportedCryptoCurrencies?.[paramAssetId] === true,
95+
(p) => p.supportedCryptoCurrencies?.[assetId] === true,
8996
);
90-
}, [providers, paramAssetId]);
97+
}, [providers, assetId]);
9198

9299
const providerIds = useMemo(
93100
() => displayProviders.map((p) => p.id),
@@ -107,7 +114,6 @@ function ProviderSelectionModal() {
107114
assetId,
108115
providers: providerIds,
109116
paymentMethods: [selectedPaymentMethod.id],
110-
forceRefresh: true,
111117
}
112118
: null,
113119
[

app/components/UI/Ramp/Views/Modals/ProviderSelectionModal/__snapshots__/ProviderSelectionModal.test.tsx.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,9 @@ exports[`ProviderSelectionModal matches snapshot 1`] = `
516516
"width": 90,
517517
},
518518
"name": "Transak",
519+
"supportedCryptoCurrencies": {
520+
"eip155:1/slip44:60": true,
521+
},
519522
},
520523
"type": "provider",
521524
},
@@ -533,6 +536,9 @@ exports[`ProviderSelectionModal matches snapshot 1`] = `
533536
"width": 90,
534537
},
535538
"name": "MoonPay",
539+
"supportedCryptoCurrencies": {
540+
"eip155:1/slip44:60": true,
541+
},
536542
},
537543
"type": "provider",
538544
},

app/components/UI/Ramp/queries/quotes.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { rampsQuotesKeys, rampsQuotesOptions } from './quotes';
1+
import {
2+
RAMPS_QUOTES_STALE_TIME_MS,
3+
rampsQuotesKeys,
4+
rampsQuotesOptions,
5+
} from './quotes';
26

37
describe('rampsQuotesOptions', () => {
48
it('creates a stable query key for quotes', () => {
@@ -28,7 +32,6 @@ describe('rampsQuotesOptions', () => {
2832
walletAddress: '0x123',
2933
paymentMethods: ['/payments/card'],
3034
providers: ['/providers/transak'],
31-
forceRefresh: true,
3235
});
3336

3437
expect(opts.queryKey).toEqual([
@@ -41,6 +44,6 @@ describe('rampsQuotesOptions', () => {
4144
'/providers/transak',
4245
]);
4346
expect(typeof opts.queryFn).toBe('function');
44-
expect(opts.staleTime).toBe(0);
47+
expect(opts.staleTime).toBe(RAMPS_QUOTES_STALE_TIME_MS);
4548
});
4649
});

app/components/UI/Ramp/queries/quotes.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import type { QuotesResponse } from '@metamask/ramps-controller';
33
import type { GetQuotesOptions } from '../hooks/useRampsQuotes';
44
import Engine from '../../../../core/Engine';
55

6+
/** Aligns with `DEFAULT_QUOTES_TTL` in RampsController (15s controller-side cache). */
7+
export const RAMPS_QUOTES_STALE_TIME_MS = 15_000;
8+
69
type RampsQuotesQueryParams = Pick<
710
GetQuotesOptions,
811
| 'assetId'
@@ -42,5 +45,5 @@ export const rampsQuotesOptions = (params: RampsQuotesQueryParams) =>
4245
forceRefresh: params.forceRefresh,
4346
ttl: params.ttl,
4447
}),
45-
staleTime: 0,
48+
staleTime: RAMPS_QUOTES_STALE_TIME_MS,
4649
});

0 commit comments

Comments
 (0)