fix(ramps): Single-owner React Query fetching for providers, tokens, and payment methods#28224
Conversation
450cb9f to
beb8a1f
Compare
2cb4330 to
0fb7c5d
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
b32fb20 to
3ac83f7
Compare
3ac83f7 to
29f7519
Compare
…rap+region change, and getpaymentMethods <- whenever provider change
29f7519 to
450475b
Compare
| "@metamask/profile-metrics-controller": "^3.1.0", | ||
| "@metamask/profile-sync-controller": "^28.0.2", | ||
| "@metamask/ramps-controller": "^12.1.0", | ||
| "@metamask/ramps-controller": "npm:@metamask-previews/ramps-controller@12.1.0-preview-1d5c02c", |
There was a problem hiding this comment.
highlight: MetaMask/core#8354 - has preview publish - so using that.
…t own data lifecycle (#8354) ## Explanation Payment methods, providers, and tokens were being fetched redundantly from two independent paths: the controller's `fireAndForget` calls (inside `setSelectedToken`, `setSelectedProvider`, and `setUserRegion`) and React Query on the mobile side. This caused 2–3 duplicate API calls per user action, with a ~10s spinner on the `/payments` endpoint due to cold DB connection pooling. This PR removes all `fireAndForget` data-fetching side effects from the controller. The mobile client (React Query + RampsBootstrap) is now the single owner of when providers, tokens, and payment methods are fetched. The controller only manages state updates and selections. Changes: 1. **`#runInit` (geolocation fix)** — `forceRefresh` no longer overrides a persisted `userRegion` with the geolocation endpoint. Geolocation is only used to seed the initial region when `userRegion` is null. 2. **`setSelectedToken`** — Removed `fireAndForget(getPaymentMethods(...))` and `resetResource('paymentMethods')`. Token change no longer triggers payment methods fetch or clears payment methods state. Payment methods are provider-scoped, not token-scoped. 3. **`setSelectedProvider`** — Removed `fireAndForget(getPaymentMethods(...))`, `resetResource('paymentMethods')`, and `tokenSupportedByProvider` gate. Now accepts a full `Provider` object (not just ID) to avoid dependency on `state.providers.data` being populated. Silently ignores when provider ID is not found instead of throwing. 4. **`setUserRegion`** — Removed `fireAndForget(getTokens(...))` and `fireAndForget(getProviders(...))`. The mobile client handles all data fetching via React Query (providers, payment methods) and direct controller calls (tokens) from RampsBootstrap. 5. **`setSelectedPaymentMethod`** — Now accepts a full `PaymentMethod` object (not just ID) to avoid dependency on `state.paymentMethods.data` being populated. Silently sets `null` when payment method ID is not found instead of throwing. 6. **`getPaymentMethods` response handler** — Always selects the first (highest-scored) payment method when new data arrives, preventing dead-end states where a payment method with no quotes stays selected after provider switch. 7. **`#fireAndForget`** — Removed (no remaining callers). 8. **`executeRequest` generation counter** — Added `#pendingResourceGeneration` map to prevent stale in-flight requests from corrupting `isLoading` state. When `setUserRegion` resets dependent resource counts, the generation is bumped. Orphaned finally blocks from the previous generation skip their decrement instead of prematurely clearing `isLoading`. ## Link to metamask-mobile Depends on mobile PR: [MetaMask/metamask-mobile#28224](MetaMask/metamask-mobile#28224) ## References [TRAM-3398](https://consensyssoftware.atlassian.net/browse/TRAM-3398) ## Changelog CHANGELOG entry: See `packages/ramps-controller/CHANGELOG.md` Unreleased section. ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Behavior changes in `RampsController` selection/region flows (no longer auto-fetching or clearing dependent data) could affect consumers that relied on controller-side side effects. Adds new stale-request invalidation for `executeRequest`, which impacts loading/error state handling across resources. > > **Overview** > **Shifts ramps data lifecycle ownership to the client** by removing controller-side `fireAndForget` fetching from `setUserRegion`, `setSelectedToken`, and `setSelectedProvider`, and by no longer clearing `paymentMethods` on token/provider changes. > > `setSelectedProvider` and `setSelectedPaymentMethod` now accept either an ID or a full object and **stop throwing when backing resource data isn’t loaded/not found** (silently leaving selection `null`). `init` is fixed to **never override a persisted `userRegion` with geolocation**, even on `forceRefresh`. > > Request tracking is hardened by adding a per-resource generation counter so stale in-flight requests can’t incorrectly decrement ref-counted `isLoading` after dependent-resource resets; tests are updated/added to cover these races and the new selection semantics. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7073fe5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
| import useRampsProviders from './hooks/useRampsProviders'; | ||
| import useRampsPaymentMethods from './hooks/useRampsPaymentMethods'; | ||
| import { selectUserRegion } from '../../../selectors/rampsController'; | ||
| import Engine from '../../../core/Engine'; |
There was a problem hiding this comment.
Payment methods hook is new here
selectUserRegion + Engine are for the token fetch below
| function RampsBootstrap(): null { | ||
| useRampsSmartRouting(); | ||
| useRampsProviders(); | ||
| useRampsProviders({ enableSideEffects: true }); |
There was a problem hiding this comment.
enableSideEffects: true means only RampsBootStrap runs auto-selection and cache invalidation
Other screens that call useRampsProviders - just read - no side effects
| Engine.context.RampsController.getTokens(userRegion.regionCode, 'buy'); | ||
| } | ||
| }, [userRegion?.regionCode]); | ||
|
|
There was a problem hiding this comment.
tokens are fetched directly - no react query - because the controller needs them for bvalidation in setSelectedToken - fires only once when region becomes available & re-fires if region changes
| import HeaderCompactStandard from '../../../../../../component-library/components-temp/HeaderCompactStandard'; | ||
| import MenuItem from '../../../components/MenuItem'; | ||
| import { useRampsController } from '../../../hooks/useRampsController'; | ||
| import { useRampsProviders } from '../../../hooks/useRampsProviders'; |
There was a problem hiding this comment.
settings only needs provider data - before useRampsController was pulling in everything and triggering paymentMethods fetches as a side effect
| import { useStyles } from '../../../../../hooks/useStyles'; | ||
| import { useRampsController } from '../../../hooks/useRampsController'; | ||
| import { useRampsProviders } from '../../../hooks/useRampsProviders'; | ||
| import { useRampsTokens } from '../../../hooks/useRampsTokens'; |
There was a problem hiding this comment.
split into 2 focused hooks instead of 1 monolithic one
| refetchType: 'none', | ||
| }); | ||
| } | ||
| }, [enableSideEffects, regionCode, queryClient]); |
There was a problem hiding this comment.
if region is changed, mark all ramp queries as stale. without refetchType: none, invalidateQueries would trigger a second fetch on top of the o1 react query - that is already fires from the query key change.
the stale-marking matters when the user switches Back to a previous region - cached data gets refetched instead of served stale.
| () => providersQuery?.data ?? providersStateData ?? [], | ||
| [providersQuery?.data, providersStateData], | ||
| ); | ||
|
|
There was a problem hiding this comment.
react query cache is the primary source - fall back to controller state for initial render
|
|
||
| const setSelectedProvider = useCallback( | ||
| (provider: Provider | null, options?: { autoSelected?: boolean }) => | ||
| (provider: Provider | null, setOptions?: { autoSelected?: boolean }) => |
There was a problem hiding this comment.
passes the full provider obj(just reference id only - no memory booming) - this avoids depending on state.providers.data being populated (it might not be after a region reset)
| error: | ||
| providersQuery?.error instanceof Error | ||
| ? providersQuery.error.message | ||
| : providersStateError, |
There was a problem hiding this comment.
loading and error now come from react query first, fall back to controller state
| if (paymentMethods.length === 0) { | ||
| // Filter out payment methods that have no available quote once quotes | ||
| // have loaded. This avoids showing dead-end options to the user. | ||
| const visiblePaymentMethods = |
There was a problem hiding this comment.
once quotes finish loading, filter out any payment method that has no real quote.
that is a dead-end option we do not want to show
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection: Performance Test Selection: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 75f85ca. Configure here.
| mockDeterminePreferredProvider.mockReturnValue(null); | ||
|
|
||
| renderHook(() => useRampsProviders(), { | ||
| renderHook(() => useRampsProviders({ enableSideEffects: true }), { |
There was a problem hiding this comment.
Negative auto-selection tests now test wrong condition
Medium Severity
Three negative tests for the preferred provider auto-selection effect — "does not call determinePreferredProvider when providers is empty" (line 333), "when selectedProvider is already set" (line 344), and "when providers is undefined" (line 358) — all call useRampsProviders() without enableSideEffects: true. Since enableSideEffects defaults to false, the auto-selection effect is gated by that flag and never runs, regardless of the provider/selection state. These tests pass trivially and no longer verify the conditions they describe. If someone later removes the providers.length > 0 or !selectedProvider guards, these tests would still pass, providing a false safety net.
Reviewed by Cursor Bugbot for commit 75f85ca. Configure here.
|
|
✅ E2E Fixture Validation — Schema is up to date |





Description
Payment methods, providers, and tokens were being fetched redundantly from two independent paths (controller
fireAndForget+ React Query), causing 2–3 duplicate API calls per user action with a ~10s spinner.This PR makes the mobile client the single fetch owner for all ramp data. React Query handles providers and payment methods with proper caching and invalidation. Tokens are fetched directly from RampsBootstrap. Screens that don't need payment methods no longer trigger fetches.
Changes:
RampsBootstrap.tsx— Now fetches all three data sources at app root:useRampsProviders(React Query),useRampsPaymentMethods(React Query), andgetTokens(direct controller call on region change). This follows the agreed flow: app loads → fetch providers and tokens → provider selected → fetch payment methods.useRampsProviders.ts— Reads provider list from React Query cache (not controller state). Invalidates all ramp queries on region change to force fresh fetches. Passes fullProviderobject tosetSelectedProviderfor auto-selection (avoids crash when controller state is empty).useRampsPaymentMethods.ts— Query key reduced to[regionCode, providerId]only. Token/fiat are passed to the API call but not the key, so token changes don't trigger refetches. Passes fullPaymentMethodobject to controller (avoids crash when controller state is empty).paymentMethods.ts(query config) —staleTime: 5min. Query key only includesregionCodeandproviderId.providers.ts(query config) —staleTime: 15min. RemovedforceRefresh: truefrom queryFn (controller's own cache handles it).SettingsModal.tsx— UsesuseRampsProvidersinstead ofuseRampsController(no more payment methods fetch on settings screen).TokenNotAvailableModal.tsx— UsesuseRampsProviders+useRampsTokensinstead ofuseRampsController.RegionSelector.tsx— UsesuseRampsUserRegion+useRampsCountriesinstead ofuseRampsController.PaymentSelectionModal.tsx— Filters out payment methods with no available quote once quotes load, preventing dead-end options (e.g. Apple Pay shown but no provider returns a quote for it).Tests updated — Removed
tokenSupportedByProvidertest, updated query key and staleTime assertions.Changelog
CHANGELOG entry: Fixed duplicate payment method, provider, and token API calls during buy flow; React Query is now the single fetch owner for ramp data
Related issues
Fixes: TRAM-3398
Depends on core PR: MetaMask/core#8354 (removes
fireAndForgetcalls from controller)Manual testing steps
Screenshots/Recordings
Before
After
Pre-merge author checklist
Pre-merge reviewer checklist
Note
Medium Risk
Changes ramp buy-flow data fetching/caching and controller interaction patterns (query keys, invalidation, and provider/payment-method selection), which could affect availability/performance across regions and providers. UI impact is limited but touches core buy-flow bootstrap and selection logic.
Overview
Makes the mobile client the single fetch owner for ramps buy data.
RampsBootstrapnow preloads providers (with side effects), payment methods, and triggers token fetching on region availability so downstream screens don’t cause redundant requests.Reworks React Query behavior for providers and payment methods.
useRampsProvidersreads the provider list from the React Query cache, adds optionalenableSideEffectsto avoid duplicate invalidation/auto-selection, and invalidates allrampsqueries on region changes.useRampsPaymentMethodssimplifies the query to be provider-scoped (query key isregionCode+providerId, 5-minstaleTime) and updates controller setters to accept full objects/null.UI behavior tweaks and hook decoupling.
PaymentSelectionModalnow hides payment methods that have no non-custom-action quote once quotes load, showing the “no payment methods available” state instead. Several screens (SettingsModal,TokenNotAvailableModal,RegionSelector) switch fromuseRampsControllerto narrower hooks (useRampsProviders,useRampsTokens,useRampsUserRegion,useRampsCountries) to prevent unrelated fetches. Dependency bump updates@metamask/ramps-controllerto^13.0.0.Reviewed by Cursor Bugbot for commit 75f85ca. Bugbot is set up for automated code reviews on this repo. Configure here.