feat(ramps-controller): consume moneyHeadlessAllProviders flag controller-side; share provider/quote/error helpers#9409
Merged
Conversation
…mers Expose the provider-availability, quote-classification, and error-normalization logic the controller uses internally as pure, tested helpers so headless-buy consumers (e.g. metamask-mobile) can share it instead of re-deriving it: - providerServesAsset, getProvidersServingAsset, regionHasProviderForAsset, isFiatDepositAvailable - isExternalBrowserQuote, isCustomActionQuote, isInAppOnlyQuote - getErrorMessage, extractExplicitTypedError, normalizeToTypedError (+ TypedError) RampsController now derives its own region asset-matching and quote filtering from these helpers so the shared surface stays behaviourally identical to the controller's selection.
75ef2c5 to
685219b
Compare
Contributor
Author
|
@metamaskbot publish-preview |
Contributor
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
…oviders boolean flag read controller-side
Contributor
Author
|
@metamaskbot publish-preview |
10 tasks
10 tasks
…ller-ui-logic # Conflicts: # packages/ramps-controller/CHANGELOG.md # packages/ramps-controller/package.json # packages/ramps-controller/src/RampsController.test.ts # packages/ramps-controller/src/RampsController.ts # yarn.lock
Contributor
Author
|
@metamaskbot publish-preview |
Contributor
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
wenfix
approved these changes
Jul 13, 2026
saustrie-consensys
added a commit
to MetaMask/metamask-mobile
that referenced
this pull request
Jul 13, 2026
… LD granular control (#33170) ## **Description** Docs-only update to the Headless Buy all-providers plan (`app/components/UI/Ramp/headless/PLAN_-_ALL_PROVIDERS_SUPPORT.md`). Stacked on #32906 (base branch `docs/headless-buy-phase2-update`) because both PRs edit the same file; this one layers a Phase 1 accuracy pass and a new Phase 2 LaunchDarkly design on top of that PR's Phase 2 rewrites. ### Phase 1 accuracy pass Phase 1 now describes the implementation as it exists across core #9353 (merged), mobile #32810 (merged), mobile #32682 (in review), and the core #9409 / mobile #32890 rework (in review): - The multivariate scope design (`moneyHeadlessProviderScope`, `off | in-app | all`, the `ProviderScope` type, the `getProviderScope` constructor injection, the persisted `fiatOrders.providerScope` Redux setting, `getEffectiveProviderScope` / `useFiatProviderScope`, and the HeadlessPlayground provider-scope toggle) is gone. The gate is one boolean LaunchDarkly flag, `moneyHeadlessAllProviders`, consumed by `RampsController` itself: a per-call `RemoteFeatureFlagController:getState` read that fails closed, where only the literal `true` widens to all provider classes and the dev override is Settings > Feature flag override via `localOverrides`. P1.M2 is retitled and rewritten for this. - P1.M0 (`failSession` terminal) is marked done (mobile #32810), and P1.M1 is rewritten as the widened quoting capability that shipped (`#pickWidenedQuote`, fiat-limit enforcement, default redirect URL, relaxed availability gates). - Stale scope references were fixed everywhere they appeared (intro, checklist, Background, Design principles, Quote-layer, Architecture diagram, Phase 2 preconditions, P2.M4, P2.M6, P2.M8, Deferred, Test plan, Open risks), including the `#pickInAppQuote` to `#pickWidenedQuote` rename and the removal of the in-app pre-filter. - Phase 1 gets a single acceptance criterion: a user can complete a Transak (Aggregator) buy in Brazil end to end through the headless flow. ### Phase 2: granular LaunchDarkly control (P2.M5 rework) The old P2.M5 ("widen via the flag's `all` value") was obsolete: there is no `all` value and the boolean already covers widening. It is reworked into a granular LaunchDarkly control design: an optional `moneyHeadlessProviderAllowlists` companion JSON flag carrying provider-id allowlists, with optional per-surface lists (money account, perps, predictions). After one-time client support lands, QA can force specific provider mixes (for example external-browser-only) purely by editing the flag in the LD dashboard, with no code change per test configuration. ## **Changelog** CHANGELOG entry: null ## **Related issues** Refs: MetaMask/core#9409 Refs: #32890 Refs: #32906 ## **Manual testing steps** N/A. Docs-only change to a planning markdown file; no code paths are affected. ## **Screenshots/Recordings** N/A ### **Before** N/A ### **After** N/A ## **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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **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] > **Low Risk** > Markdown planning doc only; no application code, flags, or tests are modified. > > **Overview** > Updates **`PLAN_-_ALL_PROVIDERS_SUPPORT.md`** so Phase 1 matches shipped/in-review work and Phase 2 documents the next flag design—no runtime behavior changes. > > **Phase 1** is reframed from multivariate scope (`off | in-app | all`) and in-app-only quoting to the boolean **`moneyHeadlessAllProviders`** read in **`RampsController`** (literal `true` widens; everything else fails closed). The doc drops **`ProviderScope`**, Redux **`providerScope`**, and playground toggles; marks **P1.M0** done; rewrites **P1.M1** around **`#pickWidenedQuote`**, fiat limits, default redirect URL, and relaxed gates; and adds a Brazil Transak end-to-end acceptance criterion. > > **Phase 2 P2.M5** is reworked from “flip flag to `all`” into optional companion **`moneyHeadlessProviderAllowlists`** JSON (top-level and per-surface provider lists) so QA can constrain mixes via LaunchDarkly without per-config releases. Related sections (**P2.M6**, **P2.M8**, test plan, risks) are updated for flag-only gating and **`#pickWidenedQuote`** naming. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d163c06. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
pull Bot
pushed a commit
to dmrazzy/core
that referenced
this pull request
Jul 13, 2026
## Explanation Releases `@metamask/ramps-controller` 17.0.0. - **BREAKING:** `RampsController` now reads the `moneyHeadlessAllProviders` boolean remote feature flag itself, through the `RemoteFeatureFlagController:getState` messenger action resolved on each auto-selection `getQuotes` call. This replaces the `getProviderScope` constructor option and the exported `ProviderScope` type. Consumers should delegate that action to the controller's messenger; when it is missing, the read fails closed and quoting stays native-only. - When the flag is `true`, the auto-selection quote path (`autoSelectProvider` / `restrictToKnownOrNativeProviders`) widens to every supporting provider class (native, in-app WebView aggregator, and external-browser / custom-action) and returns the best quote at `success[0]`, enforcing per-provider fiat limits. - Adds exported pure helpers so consumers resolve gating and classification exactly like the controller: `MONEY_HEADLESS_ALL_PROVIDERS_FLAG_KEY` / `isHeadlessAllProvidersEnabled`, provider-availability helpers (`providerServesAsset`, `getProvidersServingAsset`, `regionHasProviderForAsset`, `isFiatDepositAvailable`), quote-classification helpers (`isExternalBrowserQuote`, `isCustomActionQuote`, `isInAppOnlyQuote`), and error-normalization helpers (`getErrorMessage`, `extractExplicitTypedError`, `normalizeToTypedError`). - Adds `@metamask/remote-feature-flag-controller` `^4.2.2` as a runtime dependency. `@metamask/transaction-pay-controller` is intentionally not released here. Everything it consumes from this package (`Quote`, `RampsOrder`, `RampsOrderStatus`) is unchanged since 16.0.0, and its runtime interaction goes through the messenger to the client-instantiated controller. Its workspace dependency range moves to `^17.0.0` in this PR via yarn constraints, with no version bump; the published range update ships with its next release. ## References - Sole change in this release: MetaMask#9409 - Consumer PR adopting the breaking change: MetaMask/metamask-mobile#32890 ## 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) - [x] 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** > The published ramps-controller major is breaking for hosts that still pass `getProviderScope` or rely on `in-app` scope; this PR only versions and documents that release, with limited direct runtime change in-repo beyond dependency ranges. > > **Overview** > **Monorepo release 1119.0.0** cuts **`@metamask/ramps-controller` 17.0.0** (from 16.0.0) and records that release in its changelog. The diff itself is versioning and lockfile alignment, not new controller logic. > > **`@metamask/transaction-pay-controller`** is updated only as a workspace consumer: dependency range moves to **`^17.0.0`** with an Unreleased changelog note; its package version stays **24.0.3** (no TPC publish in this PR). > > What ships in **ramps-controller 17.0.0** (documented in the new changelog section): **breaking** removal of **`getProviderScope`** / **`ProviderScope`** in favor of the **`moneyHeadlessAllProviders`** remote feature flag via **`RemoteFeatureFlagController:getState`**; auto-selection widens to all provider classes when the flag is true; plus exported pure helpers for flag resolution, provider availability, quote classification, and error normalization, and a **`remote-feature-flag-controller`** runtime dependency. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 26ff4f0. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
saustrie-consensys
added a commit
to MetaMask/metamask-mobile
that referenced
this pull request
Jul 13, 2026
…AllProviders remote flag (#32890) ## **Description** Wires the Headless Buy provider expansion to a remote LaunchDarkly flag so it can be activated without shipping a code change, completing the "flag-gated activation" step from the all-providers plan (`app/components/UI/Ramp/headless/PLAN_-_ALL_PROVIDERS_SUPPORT.md`). Stacked on #32682 (Phase 1). Phase 1 gates the widening purely client-side: a `fiatOrders.providerScope` Redux setting plus a hardcoded production hard-off inside `getEffectiveProviderScope`. This PR replaces all of that with a single boolean flag consumed by the controller itself: - The flag is `moneyHeadlessAllProviders` (boolean). `true` widens the headless fiat quote path to every provider class (native, in-app WebView aggregator, external-browser / custom-action); `false` or missing keeps the native-only default, in every environment including production. The intermediate `in-app` scope from the earlier multivariate design is removed. - `RampsController` (core PR MetaMask/core#9409, released as `@metamask/ramps-controller@17.0.0` and consumed here as `^17.0.0`; the earlier preview-build pin is removed) now reads the flag itself through the `RemoteFeatureFlagController:getState` messenger action per quote call. The `getProviderScope` injection in `ramps-controller-init.ts` is gone; the ramps controller messenger delegates `RemoteFeatureFlagController:getState`. - UI hooks (`useHasNativeFiatProvider`, `useRegionHasFiatProvider`) derive the boolean via a thin `useHeadlessAllProvidersEnabled` hook that calls the core-exported `isHeadlessAllProvidersEnabled` helper against `RemoteFeatureFlagController` state, then feed it into the core-exported availability helpers. No flag interpretation logic lives in mobile. - Deleted: the `ProviderScope` / `FiatProviderScope` types and every scope-shaped API; the persisted `fiatOrders.providerScope` setting (action, reducer case, selector, state key); `getEffectiveProviderScope` / `useFiatProviderScope`; the `selectMoneyHeadlessProviderScope` selector directory; and the HeadlessPlayground provider-scope toggle (including its locale strings). - Dev override: the core helper merges `localOverrides` over `remoteFeatureFlags`, so flipping `moneyHeadlessAllProviders` in Settings > Feature flag override (non-production builds) overrides the remote value for both the UI gates and the controller's quote widening. The playground toggle is no longer needed. - No state migration: `fiatOrders.providerScope` was introduced on the unmerged Phase 1 branch (#32682) and never shipped in a release, so no persisted production state references it; a stale key from dev/RC installs is carried harmlessly and read by nothing. ### LaunchDarkly flag (dashboard changes handled separately) - Create dashboard key `money-headless-all-providers`: boolean, default variation `false`. Serve `false` in Production for now; the client also resolves to native-only if the flag is missing or non-boolean. - Key casing: LaunchDarkly dashboard keys are kebab-case and the ClientConfig API serves them to clients camelized, so the code reads `moneyHeadlessAllProviders` (same convention as existing flags; see the naming table in `docs/perps/perps-feature-flags.md`). - Retire the previously planned multivariate `moneyHeadlessProviderScope` (string `off` | `in-app` | `all`); no client reads it after this PR. ## **Changelog** <!-- Dev/RC-gated, production hard-off by default; not end-user-facing yet. --> CHANGELOG entry: null ## **Related issues** Refs: #32682 ## **Manual testing steps** ```gherkin Feature: Remote activation of Headless Buy all-providers widening Scenario: flag true widens to all providers Given a dev/RC build And the moneyHeadlessAllProviders flag resolves to true (remote or via override) When the user runs a Money "Add funds" deposit in an aggregator-only region Then aggregator providers are quoted and the aggregator WebView flow opens (the flow is no longer native-only) Scenario: flag false or missing stays native-only Given the moneyHeadlessAllProviders flag is false or not served When the user runs a Money "Add funds" deposit Then only native providers are offered, in every environment Scenario: dev override via Settings Given a non-production build with the remote flag false or unset When the user opens Settings > Feature flag override And sets moneyHeadlessAllProviders to true Then the widened all-providers flow is active (localOverrides wins for both the UI availability gates and the controller quote widening) ``` ## **Screenshots/Recordings** N/A. No user-facing UI change; behavior is identical to Phase 1 until the flag is served `true`. ### **Before** N/A ### **After** N/A ## **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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **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 how Headless Buy widens provider quoting and deposit eligibility gates across Money flows; mis-flagging could expose aggregator paths in production, though default-off and shared core helpers reduce UI/controller drift risk. > > **Overview** > Replaces the dev/RC **provider scope** Redux toggle (`off` / `in-app` / `all`) with a single remote boolean **`moneyHeadlessAllProviders`**, aligned with `@metamask/ramps-controller@17.0.0`. > > **Controller & wiring:** `RampsController` no longer receives `getProviderScope` from mobile init; it reads the flag via **`RemoteFeatureFlagController:getState`** (new messenger delegation). **`getDefaultRedirectUrl`** for widened aggregator quotes is unchanged. > > **UI gates:** New **`useHeadlessAllProvidersEnabled`** binds Redux feature-flag state to core **`isHeadlessAllProvidersEnabled`** (supports Settings **`localOverrides`**). **`useHasNativeFiatProvider`** and **`useRegionHasFiatProvider`** delegate to core **`isFiatDepositAvailable`** / **`regionHasProviderForAsset`** so UI and controller stay in sync. **`getAggregatorRedirectConfig`** uses core **`isExternalBrowserQuote`**; **`toHeadlessBuyError`** uses shared **`extractExplicitTypedError`**. > > **Removed:** `fiatOrders.providerScope`, `providerScope` utils/hooks/tests, Headless Playground scope UI + locale strings, and related reducer tests. > > Default behavior stays **native-only** until the flag is literally `true` (remote or override). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b91c61d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Explanation
Headless Buy provider expansion previously reached
RampsControllerthrough an injectedgetProviderScopecallback returning a multivariate scope (off|in-app|all), andmetamask-mobilere-derived gating logic the controller already owns privately. This PR replaces the scope concept with a single boolean remote feature flag that the controller reads itself, and lifts the shared logic into pure, tested, exported helpers so consumers cannot drift from the controller's own selection.Flag semantics (
moneyHeadlessAllProviders, boolean; this is the client-side key, which the ClientConfig API camelizes from the kebab-case LaunchDarkly dashboard keymoney-headless-all-providers):true: the auto-selection quote path (autoSelectProvider/restrictToKnownOrNativeProviders) widens to every supporting provider class: native, in-app WebView aggregator, and external-browser / custom-action. The best quote is returned atsuccess[0], enforcing per-provider fiat limits.false, missing, or any non-boolean value: today's native-only behavior. The intermediatein-appscope is removed, along with the exportedProviderScopetype and thegetProviderScopeconstructor option (breaking).Controller-side consumption:
RemoteFeatureFlagController:getStateis added to the controller's allowed messenger actions (same pattern asConfigRegistryController/bridge-controller), and a private#isAllProvidersEnabled()resolves the flag pergetQuotescall, so a remote fetch or a local dev override takes effect at runtime. The read fails closed: if the action is not delegated, quoting stays native-only.featureFlagsmodule exportsMONEY_HEADLESS_ALL_PROVIDERS_FLAG_KEYand the pureisHeadlessAllProvidersEnabled(remoteFeatureFlagState)helper (plus aHeadlessFeatureFlagsLookuptype). The helper owns the key lookup, thelocalOverrides-over-remoteFeatureFlagsmerge (mobile's Settings > Feature flag override screen writeslocalOverrides, and publishedRemoteFeatureFlagControllerversions such as4.2.2do not fold overrides intoremoteFeatureFlagsstate), and defensive coercion: only the literal booleantrueenables. Mobile UI hooks call the same helper, so the UI gate and the controller resolve the flag identically.Shared pure helpers (consumed by the controller internally so they stay behaviourally identical to its own selection):
providerAvailability:providerServesAsset,getProvidersServingAsset,regionHasProviderForAsset,isFiatDepositAvailable. The latter two take anallProvidersEnabled: booleaninstead of a scope.quoteClassification:isExternalBrowserQuote,isCustomActionQuote,isInAppOnlyQuote.errorNormalization:getErrorMessage,extractExplicitTypedError,normalizeToTypedError(+TypedError<Code>). The deliberately host-specific bits (deeplink schemes, redirect URLs, session lifecycle, error taxonomy values) stay in the consumer.All 601 package tests pass with 100% coverage, including: widening on/off via the flag,
localOverrideshonored in both directions, garbage flag values coerced tofalse, an unwiredRemoteFeatureFlagController:getStatefailing closed, and per-call flag reads picking up runtime changes.References
app/components/UI/Ramp/headless/PLAN_-_ALL_PROVIDERS_SUPPORT.mdin metamask-mobile)Checklist
Made with Cursor