Skip to content

fix(ramps): Remove dual-path fetching from RampsController, let client own data lifecycle#8354

Merged
imyugioh merged 8 commits intomainfrom
fix/ramps-geolocaiton-call-init-once
Apr 3, 2026
Merged

fix(ramps): Remove dual-path fetching from RampsController, let client own data lifecycle#8354
imyugioh merged 8 commits intomainfrom
fix/ramps-geolocaiton-call-init-once

Conversation

@imyugioh
Copy link
Copy Markdown
Contributor

@imyugioh imyugioh commented Mar 31, 2026

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

References

TRAM-3398

Changelog

CHANGELOG entry: See packages/ramps-controller/CHANGELOG.md Unreleased section.

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

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.

Written by Cursor Bugbot for commit 7073fe5. This will update automatically on new commits. Configure here.

@imyugioh imyugioh self-assigned this Mar 31, 2026
@imyugioh imyugioh force-pushed the fix/ramps-geolocaiton-call-init-once branch from 2e9d637 to e38a582 Compare April 1, 2026 00:50
@imyugioh imyugioh changed the title fix: coinbase order crypto currency lookup by network-aware ID mapping fix(ramps): Set the region state from geolocation's response initially only Apr 1, 2026
@imyugioh imyugioh force-pushed the fix/ramps-geolocaiton-call-init-once branch from e38a582 to e7946ce Compare April 1, 2026 01:02
@imyugioh imyugioh force-pushed the fix/ramps-geolocaiton-call-init-once branch from 36a67a9 to 29bbf4c Compare April 2, 2026 03:02
@imyugioh imyugioh changed the title fix(ramps): Set the region state from geolocation's response initially only fix(ramps): Remove dual-path fetching from RampsController, let client own data lifecycle Apr 2, 2026
@imyugioh imyugioh force-pushed the fix/ramps-geolocaiton-call-init-once branch from ab25346 to 14dc03d Compare April 2, 2026 04:50
@imyugioh imyugioh marked this pull request as ready for review April 2, 2026 05:17
@imyugioh imyugioh requested review from a team as code owners April 2, 2026 05:17
@imyugioh imyugioh force-pushed the fix/ramps-geolocaiton-call-init-once branch from a553f75 to 10a8a74 Compare April 2, 2026 05:24
@imyugioh
Copy link
Copy Markdown
Contributor Author

imyugioh commented Apr 2, 2026

@metamaskbot publish-previews

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@7.0.0-preview-10a8a74
@metamask-previews/accounts-controller@37.2.0-preview-10a8a74
@metamask-previews/address-book-controller@7.1.1-preview-10a8a74
@metamask-previews/ai-controllers@0.6.3-preview-10a8a74
@metamask-previews/analytics-controller@1.0.1-preview-10a8a74
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-10a8a74
@metamask-previews/announcement-controller@8.1.0-preview-10a8a74
@metamask-previews/app-metadata-controller@2.0.1-preview-10a8a74
@metamask-previews/approval-controller@9.0.1-preview-10a8a74
@metamask-previews/assets-controller@4.0.0-preview-10a8a74
@metamask-previews/assets-controllers@103.1.1-preview-10a8a74
@metamask-previews/base-controller@9.0.1-preview-10a8a74
@metamask-previews/base-data-service@0.1.1-preview-10a8a74
@metamask-previews/bridge-controller@70.0.1-preview-10a8a74
@metamask-previews/bridge-status-controller@70.0.5-preview-10a8a74
@metamask-previews/build-utils@3.0.4-preview-10a8a74
@metamask-previews/chain-agnostic-permission@1.5.0-preview-10a8a74
@metamask-previews/claims-controller@0.5.0-preview-10a8a74
@metamask-previews/client-controller@1.0.1-preview-10a8a74
@metamask-previews/compliance-controller@1.0.2-preview-10a8a74
@metamask-previews/composable-controller@12.0.1-preview-10a8a74
@metamask-previews/config-registry-controller@0.2.0-preview-10a8a74
@metamask-previews/connectivity-controller@0.2.0-preview-10a8a74
@metamask-previews/controller-utils@11.20.0-preview-10a8a74
@metamask-previews/core-backend@6.2.1-preview-10a8a74
@metamask-previews/delegation-controller@2.1.0-preview-10a8a74
@metamask-previews/earn-controller@11.2.1-preview-10a8a74
@metamask-previews/eip-5792-middleware@3.0.3-preview-10a8a74
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.0-preview-10a8a74
@metamask-previews/eip1193-permission-middleware@1.0.3-preview-10a8a74
@metamask-previews/ens-controller@19.1.1-preview-10a8a74
@metamask-previews/eth-block-tracker@15.0.1-preview-10a8a74
@metamask-previews/eth-json-rpc-middleware@23.1.1-preview-10a8a74
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-10a8a74
@metamask-previews/foundryup@1.0.1-preview-10a8a74
@metamask-previews/gas-fee-controller@26.1.1-preview-10a8a74
@metamask-previews/gator-permissions-controller@3.0.1-preview-10a8a74
@metamask-previews/geolocation-controller@0.1.2-preview-10a8a74
@metamask-previews/json-rpc-engine@10.2.4-preview-10a8a74
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-10a8a74
@metamask-previews/keyring-controller@25.2.0-preview-10a8a74
@metamask-previews/logging-controller@8.0.1-preview-10a8a74
@metamask-previews/message-manager@14.1.1-preview-10a8a74
@metamask-previews/messenger@1.1.0-preview-10a8a74
@metamask-previews/multichain-account-service@8.0.1-preview-10a8a74
@metamask-previews/multichain-api-middleware@2.0.0-preview-10a8a74
@metamask-previews/multichain-network-controller@3.0.6-preview-10a8a74
@metamask-previews/multichain-transactions-controller@7.0.4-preview-10a8a74
@metamask-previews/name-controller@9.1.1-preview-10a8a74
@metamask-previews/network-controller@30.0.1-preview-10a8a74
@metamask-previews/network-enablement-controller@5.0.2-preview-10a8a74
@metamask-previews/notification-services-controller@23.0.1-preview-10a8a74
@metamask-previews/permission-controller@12.3.0-preview-10a8a74
@metamask-previews/permission-log-controller@5.1.0-preview-10a8a74
@metamask-previews/perps-controller@2.0.0-preview-10a8a74
@metamask-previews/phishing-controller@17.1.1-preview-10a8a74
@metamask-previews/polling-controller@16.0.4-preview-10a8a74
@metamask-previews/preferences-controller@23.1.0-preview-10a8a74
@metamask-previews/profile-metrics-controller@3.1.3-preview-10a8a74
@metamask-previews/profile-sync-controller@28.0.2-preview-10a8a74
@metamask-previews/ramps-controller@12.1.0-preview-10a8a74
@metamask-previews/rate-limit-controller@7.0.1-preview-10a8a74
@metamask-previews/react-data-query@0.2.0-preview-10a8a74
@metamask-previews/remote-feature-flag-controller@4.2.0-preview-10a8a74
@metamask-previews/sample-controllers@4.0.4-preview-10a8a74
@metamask-previews/seedless-onboarding-controller@9.1.0-preview-10a8a74
@metamask-previews/selected-network-controller@26.1.0-preview-10a8a74
@metamask-previews/shield-controller@5.1.1-preview-10a8a74
@metamask-previews/signature-controller@39.1.2-preview-10a8a74
@metamask-previews/social-controllers@0.0.0-preview-10a8a74
@metamask-previews/storage-service@1.0.1-preview-10a8a74
@metamask-previews/subscription-controller@6.1.2-preview-10a8a74
@metamask-previews/transaction-controller@64.0.0-preview-10a8a74
@metamask-previews/transaction-pay-controller@19.0.2-preview-10a8a74
@metamask-previews/user-operation-controller@41.2.0-preview-10a8a74

@imyugioh imyugioh force-pushed the fix/ramps-geolocaiton-call-init-once branch from 10a8a74 to 4d05ac3 Compare April 2, 2026 21:07
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

@imyugioh imyugioh force-pushed the fix/ramps-geolocaiton-call-init-once branch 6 times, most recently from 6036f35 to 1d5c02c Compare April 2, 2026 22:43
@imyugioh
Copy link
Copy Markdown
Contributor Author

imyugioh commented Apr 2, 2026

@metamaskbot publish-previews

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@7.0.0-preview-1d5c02c
@metamask-previews/accounts-controller@37.2.0-preview-1d5c02c
@metamask-previews/address-book-controller@7.1.1-preview-1d5c02c
@metamask-previews/ai-controllers@0.6.3-preview-1d5c02c
@metamask-previews/analytics-controller@1.0.1-preview-1d5c02c
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-1d5c02c
@metamask-previews/announcement-controller@8.1.0-preview-1d5c02c
@metamask-previews/app-metadata-controller@2.0.1-preview-1d5c02c
@metamask-previews/approval-controller@9.0.1-preview-1d5c02c
@metamask-previews/assets-controller@4.0.0-preview-1d5c02c
@metamask-previews/assets-controllers@103.1.1-preview-1d5c02c
@metamask-previews/base-controller@9.0.1-preview-1d5c02c
@metamask-previews/base-data-service@0.1.1-preview-1d5c02c
@metamask-previews/bridge-controller@70.0.1-preview-1d5c02c
@metamask-previews/bridge-status-controller@70.0.5-preview-1d5c02c
@metamask-previews/build-utils@3.0.4-preview-1d5c02c
@metamask-previews/chain-agnostic-permission@1.5.0-preview-1d5c02c
@metamask-previews/claims-controller@0.5.0-preview-1d5c02c
@metamask-previews/client-controller@1.0.1-preview-1d5c02c
@metamask-previews/compliance-controller@2.0.0-preview-1d5c02c
@metamask-previews/composable-controller@12.0.1-preview-1d5c02c
@metamask-previews/config-registry-controller@0.2.0-preview-1d5c02c
@metamask-previews/connectivity-controller@0.2.0-preview-1d5c02c
@metamask-previews/controller-utils@11.20.0-preview-1d5c02c
@metamask-previews/core-backend@6.2.1-preview-1d5c02c
@metamask-previews/delegation-controller@2.1.0-preview-1d5c02c
@metamask-previews/earn-controller@11.2.1-preview-1d5c02c
@metamask-previews/eip-5792-middleware@3.0.3-preview-1d5c02c
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.0-preview-1d5c02c
@metamask-previews/eip1193-permission-middleware@1.0.3-preview-1d5c02c
@metamask-previews/ens-controller@19.1.1-preview-1d5c02c
@metamask-previews/eth-block-tracker@15.0.1-preview-1d5c02c
@metamask-previews/eth-json-rpc-middleware@23.1.1-preview-1d5c02c
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-1d5c02c
@metamask-previews/foundryup@1.0.1-preview-1d5c02c
@metamask-previews/gas-fee-controller@26.1.1-preview-1d5c02c
@metamask-previews/gator-permissions-controller@3.0.1-preview-1d5c02c
@metamask-previews/geolocation-controller@0.1.2-preview-1d5c02c
@metamask-previews/json-rpc-engine@10.2.4-preview-1d5c02c
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-1d5c02c
@metamask-previews/keyring-controller@25.2.0-preview-1d5c02c
@metamask-previews/logging-controller@8.0.1-preview-1d5c02c
@metamask-previews/message-manager@14.1.1-preview-1d5c02c
@metamask-previews/messenger@1.1.0-preview-1d5c02c
@metamask-previews/money-account-controller@0.1.0-preview-1d5c02c
@metamask-previews/multichain-account-service@8.0.1-preview-1d5c02c
@metamask-previews/multichain-api-middleware@2.0.0-preview-1d5c02c
@metamask-previews/multichain-network-controller@3.0.6-preview-1d5c02c
@metamask-previews/multichain-transactions-controller@7.0.4-preview-1d5c02c
@metamask-previews/name-controller@9.1.1-preview-1d5c02c
@metamask-previews/network-controller@30.0.1-preview-1d5c02c
@metamask-previews/network-enablement-controller@5.0.2-preview-1d5c02c
@metamask-previews/notification-services-controller@23.0.1-preview-1d5c02c
@metamask-previews/permission-controller@12.3.0-preview-1d5c02c
@metamask-previews/permission-log-controller@5.1.0-preview-1d5c02c
@metamask-previews/perps-controller@2.0.0-preview-1d5c02c
@metamask-previews/phishing-controller@17.1.1-preview-1d5c02c
@metamask-previews/polling-controller@16.0.4-preview-1d5c02c
@metamask-previews/preferences-controller@23.1.0-preview-1d5c02c
@metamask-previews/profile-metrics-controller@3.1.3-preview-1d5c02c
@metamask-previews/profile-sync-controller@28.0.2-preview-1d5c02c
@metamask-previews/ramps-controller@12.1.0-preview-1d5c02c
@metamask-previews/rate-limit-controller@7.0.1-preview-1d5c02c
@metamask-previews/react-data-query@0.2.0-preview-1d5c02c
@metamask-previews/remote-feature-flag-controller@4.2.0-preview-1d5c02c
@metamask-previews/sample-controllers@4.0.4-preview-1d5c02c
@metamask-previews/seedless-onboarding-controller@9.1.0-preview-1d5c02c
@metamask-previews/selected-network-controller@26.1.0-preview-1d5c02c
@metamask-previews/shield-controller@5.1.1-preview-1d5c02c
@metamask-previews/signature-controller@39.1.2-preview-1d5c02c
@metamask-previews/social-controllers@0.1.0-preview-1d5c02c
@metamask-previews/storage-service@1.0.1-preview-1d5c02c
@metamask-previews/subscription-controller@6.1.2-preview-1d5c02c
@metamask-previews/transaction-controller@64.0.0-preview-1d5c02c
@metamask-previews/transaction-pay-controller@19.0.2-preview-1d5c02c
@metamask-previews/user-operation-controller@41.2.0-preview-1d5c02c

@imyugioh imyugioh force-pushed the fix/ramps-geolocaiton-call-init-once branch from 1d5c02c to 2d102f8 Compare April 3, 2026 13:51
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHouldn't the changelog file just have the PR description and not all commits?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

this.#setResourceLoading(resourceType, false);
} else {
this.#pendingResourceCount.set(resourceType, next);
const currentGeneration =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amitabh94 good catch - this prevents a race condition during the rapid region switching - normal users will not do that, but hackers can do that :)
example: user taps R1 -> R2 quickly

  1. region A request starts -> pendingResourceCount =1 generation = 0
  2. user switches to region B -> clearPendingResourceCountForResource() deletes the count, bumps generation to 1
  3. region b request starts at this moment -> pendingResourceCount = 1, generation = 1
  4. Region A's request finishes -> its finally block runs. If we do not check generation, it would decrement count to 0 and set isLoading = false - > but at that moment region B is still loading.

with the generation check: region A captured generation 0 , count is 1 -> mismatch -> skip decrement -> isLoading stays true until region B finishes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generation counter only matters for only dependent resources, hmm - like providers, paymentmethods, tokens. these are resources that get their pending count cleared with setuserregion call

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this is about the correctness - not about time.
the old request's finally block always runs (we do not abort it - because aborting would lose the executeRequest cache benefit)

amitabh94
amitabh94 previously approved these changes Apr 3, 2026
@imyugioh imyugioh added this pull request to the merge queue Apr 3, 2026
Merged via the queue into main with commit acacfde Apr 3, 2026
337 checks passed
@imyugioh imyugioh deleted the fix/ramps-geolocaiton-call-init-once branch April 3, 2026 17:22
github-merge-queue bot pushed a commit to MetaMask/metamask-mobile that referenced this pull request Apr 3, 2026
…and payment methods (#28224)

## **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:

1. **`RampsBootstrap.tsx`** — Now fetches all three data sources at app
root: `useRampsProviders` (React Query), `useRampsPaymentMethods` (React
Query), and `getTokens` (direct controller call on region change). This
follows the agreed flow: app loads → fetch providers and tokens →
provider selected → fetch payment methods.

2. **`useRampsProviders.ts`** — Reads provider list from React Query
cache (not controller state). Invalidates all ramp queries on region
change to force fresh fetches. Passes full `Provider` object to
`setSelectedProvider` for auto-selection (avoids crash when controller
state is empty).

3. **`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 full
`PaymentMethod` object to controller (avoids crash when controller state
is empty).

4. **`paymentMethods.ts` (query config)** — `staleTime: 5min`. Query key
only includes `regionCode` and `providerId`.

5. **`providers.ts` (query config)** — `staleTime: 15min`. Removed
`forceRefresh: true` from queryFn (controller's own cache handles it).

6. **`SettingsModal.tsx`** — Uses `useRampsProviders` instead of
`useRampsController` (no more payment methods fetch on settings screen).

7. **`TokenNotAvailableModal.tsx`** — Uses `useRampsProviders` +
`useRampsTokens` instead of `useRampsController`.

8. **`RegionSelector.tsx`** — Uses `useRampsUserRegion` +
`useRampsCountries` instead of `useRampsController`.

9. **`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).

10. **Tests updated** — Removed `tokenSupportedByProvider` test, 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](https://consensyssoftware.atlassian.net/browse/TRAM-3398)

Depends on core PR:
[MetaMask/core#8354](MetaMask/core#8354)
(removes `fireAndForget` calls from controller)

## **Manual testing steps**

```gherkin
Feature: Single-owner fetching for ramp data

  Scenario: App load fetches providers, tokens, and payment methods
    Given user opens the app (password screen)
    When the app loads
    Then getProviders, getTokens, and getPaymentMethods each fire once
    And providers, tokens, and payment methods are populated in state

  Scenario: Token selection does not trigger payment methods fetch
    Given user is on the token selection screen
    When user selects a token (e.g. Ethereum)
    Then getPaymentMethods is NOT called
    And the BuildQuote screen loads with cached payment methods

  Scenario: Provider change triggers a single payment methods fetch
    Given user is on the BuildQuote screen with a provider selected
    When user changes the provider (e.g. Transak -> Coinbase)
    Then getPaymentMethods fires exactly once for the new provider
    And the payment method pill auto-switches to Debit or Credit

  Scenario: Region switch refreshes all data
    Given user is on the settings screen
    When user changes region (e.g. France -> Finland -> France)
    Then getProviders and getPaymentMethods fire for each new region
    And switching back to a previous region also triggers fresh fetches

  Scenario: Settings screen does not trigger payment methods fetch
    Given user navigates to the Buy & Sell settings screen
    Then getPaymentMethods is NOT called
    And no unnecessary API calls appear in the debug dashboard

  Scenario: Payment selection modal hides dead-end options
    Given user taps the payment method pill
    When quotes load for all payment methods
    Then payment methods with no available quote are filtered out
```

## **Screenshots/Recordings**

### **Before**

<!-- 2-3 getPaymentMethods calls per token selection visible in debug
dashboard -->

### **After**

<!-- Single getPaymentMethods call only on provider change; no calls on
settings/navigation -->

## **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)).

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] 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.

<!-- CURSOR_SUMMARY -->
---

> [!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.**
`RampsBootstrap` now 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.**
`useRampsProviders` reads the provider list from the React Query cache,
adds optional `enableSideEffects` to avoid duplicate
invalidation/auto-selection, and invalidates all `ramps` queries on
region changes. `useRampsPaymentMethods` simplifies the query to be
provider-scoped (query key is `regionCode` + `providerId`, 5-min
`staleTime`) and updates controller setters to accept full objects/null.
> 
> **UI behavior tweaks and hook decoupling.** `PaymentSelectionModal`
now 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 from `useRampsController` to narrower hooks
(`useRampsProviders`, `useRampsTokens`, `useRampsUserRegion`,
`useRampsCountries`) to prevent unrelated fetches. Dependency bump
updates `@metamask/ramps-controller` to `^13.0.0`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
75f85ca. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
micaelae pushed a commit that referenced this pull request Apr 7, 2026
…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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants