feat(ramps-controller): accept a JSON moneyHeadlessAllProviders payload carrying provider allowlists - #9524
Conversation
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
052c607 to
40d5fb1
Compare
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
40d5fb1 to
fee1eca
Compare
…ad carrying provider allowlists
…yload An enabled object payload must now carry featureVersion '1' (HEADLESS_ALL_PROVIDERS_FEATURE_VERSION) or it resolves to disabled, so a future contract change cannot be misread by older clients. The new getHeadlessAllProvidersMinimumVersion helper surfaces the payload's minimumVersion for clients to validate against their app version with their own version-gating util; this package performs no version comparison itself. The boolean true form is unchanged.
90c0bdc to
4021b59
Compare
| function isEnabledPayload(value: Json | undefined): value is { | ||
| [key: string]: Json; | ||
| } { | ||
| return ( | ||
| typeof value === 'object' && | ||
| value !== null && | ||
| !Array.isArray(value) && | ||
| value.enabled === true && | ||
| value.featureVersion === HEADLESS_ALL_PROVIDERS_FEATURE_VERSION | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * The `minimumVersion` carried by an enabled payload, or `undefined` for the | ||
| * boolean form, a disabled payload, or a malformed field. This package cannot | ||
| * compare app versions itself; mobile validates the value through its shared | ||
| * `validatedVersionGatedFeatureFlag` util, and the LaunchDarkly `versions` | ||
| * wrapper provides the server-side gate. | ||
| * | ||
| * @param remoteFeatureFlagState - `RemoteFeatureFlagController` state (or the | ||
| * relevant subset of it). | ||
| * @returns The minimum app version the payload declares, or `undefined`. | ||
| */ | ||
| export function getHeadlessAllProvidersMinimumVersion( | ||
| remoteFeatureFlagState: HeadlessFeatureFlagsLookup | null | undefined, | ||
| ): string | undefined { | ||
| const value = resolveFlagValue(remoteFeatureFlagState); | ||
| if (!isEnabledPayload(value)) { | ||
| return undefined; | ||
| } | ||
| const { minimumVersion } = value; | ||
| return typeof minimumVersion === 'string' && minimumVersion.trim() !== '' | ||
| ? minimumVersion | ||
| : undefined; | ||
| } |
There was a problem hiding this comment.
Do we not get these utils from feature flag controller itself? Looks like we are reinventing the wheel for flag mechanism.
There was a problem hiding this comment.
These aren't functions that are already exported in remote-feature-flag-controller's index.ts. I could export it (which would also meaning version bumping remote-feature-flag-controller too instead of just ramps-controller).
transaction-pay-controller faced a similar problem before, and they chose to do it in-line instead of modifying the remote-feature-flag-controller to export a shared utility: https://github.com/MetaMask/core/blob/505289fe98/packages/transaction-pay-controller/src/utils/feature-flags.ts#L791-L802
I'm pro keeping things DRY, but I didn't want to bump remote-feature-flag-controller. I can make the change if you agree with my initial thought process of bumping remote-feature-flag-controller. @amitabh94
There was a problem hiding this comment.
We can keep it inline but this should be solved by the @MetaMask/mobile-platform team since multiple teams are running into this.
Maybe we can raise a PR for the platform team to review but I won't block this PR for this then.
|
Is the flag shape mentioned in PR description matching the actual LD flag shape? |
|
@amitabh94 I just modified the PR body to match the new changes within LaunchDarkly |
…ist and getQuotes
## Explanation
Releases `@metamask/ramps-controller@17.2.0` (minor bump).
**Changes in this release:**
- Adds `getHeadlessProviderAllowlist(remoteFeatureFlagState)` helper
resolving the provider-id allowlist from the `moneyHeadlessAllProviders`
flag's object payload.
- Adds `HEADLESS_ALL_PROVIDERS_FEATURE_VERSION` constant (`'1'`) and
`getHeadlessAllProvidersMinimumVersion(remoteFeatureFlagState)` helper
for version gating.
- Widens the `moneyHeadlessAllProviders` flag value contract to accept
`{ enabled: true, featureVersion: "1", providerIds?: string[] }`
alongside the existing boolean form. Old clients coerce the object
payload to `false` (native-only), so serving it cannot turn widening on
for a client that cannot parse it.
Also includes a housekeeping fix to the CHANGELOG and a JSDoc correction
(stale
`surface`/`HEADLESS_ALLOWLIST_SURFACES`/`HeadlessAllowlistSurface`
references that were in the original MetaMask#9524 merge before those were
removed).
## References
- Builds on MetaMask#9524 (`moneyHeadlessAllProviders` object payload support)
- Consumer bump PR: MetaMask/metamask-mobile#33362
## 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]
> **Low Risk**
> Version bumps and documentation/changelog fixes only; no new logic in
the diff.
>
> **Overview**
> **Release cut** for `@metamask/ramps-controller@17.2.0` and the
monorepo at `1149.0.0`, with `transaction-pay-controller` and the
lockfile bumped to depend on ramps-controller `^17.2.0`.
>
> The **17.2.0 changelog** is finalized and **corrected** so it matches
the shipped API: object-form `moneyHeadlessAllProviders` is documented
as `{ enabled, featureVersion: "1", providerIds? }` with helpers
`getHeadlessProviderAllowlist` and
`getHeadlessAllProvidersMinimumVersion`, and **stale** references to
per-surface allowlists (`surfaces`, `HEADLESS_ALLOWLIST_SURFACES`,
`getQuotes` `surface` option) are removed. **`featureFlags.ts` JSDoc**
is updated the same way. No runtime code changes appear in this diff
beyond version numbers and docs.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c6d5c11. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…taMask#9668) ## Explanation Removes `getHeadlessAllProvidersMinimumVersion` from `@metamask/ramps-controller`. App-version gating for `moneyHeadlessAllProviders` is owned by the LaunchDarkly `versions` wrapper (processed by `RemoteFeatureFlagController`), not a payload `minimumVersion` field. `featureVersion` fail-closed enablement and `providerIds` allowlisting are unchanged. Supersedes MetaMask#9658 (fork PR with CI issues). Changes are identical; this PR is from the org repo so the merge-queue CI can resolve the branch correctly. ## References * Related to MetaMask#9524 ## 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 - [ ] I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Breaking public API removal; consumers importing the helper must update, though runtime headless enablement and allowlisting behavior in this package is unchanged. > > **Overview** > **Breaking:** Drops the exported `getHeadlessAllProvidersMinimumVersion` helper from `@metamask/ramps-controller` (implementation, package export, and unit tests). The unreleased changelog records that **app-version gating** for `moneyHeadlessAllProviders` is expected to come from the LaunchDarkly **`versions` wrapper** handled by `RemoteFeatureFlagController`, not from a `minimumVersion` field on the flag object payload. > > `isHeadlessAllProvidersEnabled`, `getHeadlessProviderAllowlist`, and `featureVersion` fail-closed behavior are unchanged. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit aa04b15. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
…ampSurface into headless getQuotes (#33362) ## **Description** Phase 2 of Headless Buy all-providers needs QA to force a specific provider mix (for example external-browser-only) with only a LaunchDarkly edit. MetaMask/core#9524 widens the `moneyHeadlessAllProviders` flag's value contract so the same key also accepts a JSON payload `{ "enabled": true, "providerIds": [...] }`; when provider ids are listed, the controller's widened quote pick drops candidates whose provider is not listed. Old clients coerce the payload to `false` (native-only), and production serves `false` everywhere, so serving it cannot enable widening for a client that cannot parse it. This PR is the mobile half: - Bump `@metamask/ramps-controller` to consume the new contract. No mobile flag-interpretation logic is added: `useHeadlessAllProvidersEnabled` still delegates to the core helper, so the object form flows through the existing UI availability gates automatically. Plan doc: `app/components/UI/Ramp/headless/PLAN_-_ALL_PROVIDERS_SUPPORT.md`, milestone P2.M5 (revision in #32906 records the single-flag JSON design). ## **Changelog** CHANGELOG entry: null ## **Related issues** Refs: MetaMask/core#9524 ## **Manual testing steps** 1. In the LaunchDarkly dev environment (or via Settings > Feature flag override once LD serves an object variation), set `money-headless-all-providers` to `{ "enabled": true, "providerIds": ["/providers/<external-browser-provider>"] }` in a region where an in-app aggregator normally wins on reliability. 2. Drive an MM Pay money deposit (Money account > Add funds) and confirm the selected quote comes from the forced provider. 3. Set the flag back to `true` (or remove `providerIds`) and confirm selection reverts to the reliability winner. 4. Set the flag to `false` (or `{ "enabled": false }`) and confirm the flow is native-only again. ## **Screenshots/Recordings** ### **Before** N/A (dev/QA-gated quoting behavior; no UI change) ### **After** Had to allow temporarily fiat transactions in Perps in my local RC, but here's an example of perps allowing Coinbase https://github.com/user-attachments/assets/1b08a4e6-9972-40bb-9d8d-1cc627d8c3ec ## **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** > Touches MM Pay fiat availability and region gating behind a feature flag, with a short “optimistic available” window during provider fetch that could briefly show deposit UI before resolving. > > **Overview** > Bumps **`@metamask/ramps-controller`** to **^18.0.0** so mobile picks up the updated **`moneyHeadlessAllProviders`** contract (JSON payload with **`featureVersion: '1'`** and **`enabled: true`**, plus optional **`providerIds`** handled in core). **`useHeadlessAllProvidersEnabled`** still delegates to core’s **`isHeadlessAllProvidersEnabled`**; tests now cover enabling object payloads, rejecting stale/malformed values, and keeping local override behavior. > > Renames **`useHasNativeFiatProvider`** to **`useHasFiatProvider`** and updates MM Pay gates (**`useIsFiatPaymentAvailable`**, mocks) so naming/docs match flag-aware availability (native-only when the flag is off, any provider class when on). > > Fixes a **cold provider fetch** regression in **`useRegionHasFiatProvider`**: when all-providers widening is on and the catalog is still loading with an empty list after a region switch, the hook temporarily treats the region as available so add-funds entry does not disappear until providers load. > > Docs/comments only elsewhere (**`useRampsBuyLimits`**, **`useFiatConfirm`** notes that **`rampSurface`** is analytics-only). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 00d1fd2. 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: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
…ampSurface into headless getQuotes (MetaMask#33362) ## **Description** Phase 2 of Headless Buy all-providers needs QA to force a specific provider mix (for example external-browser-only) with only a LaunchDarkly edit. MetaMask/core#9524 widens the `moneyHeadlessAllProviders` flag's value contract so the same key also accepts a JSON payload `{ "enabled": true, "providerIds": [...] }`; when provider ids are listed, the controller's widened quote pick drops candidates whose provider is not listed. Old clients coerce the payload to `false` (native-only), and production serves `false` everywhere, so serving it cannot enable widening for a client that cannot parse it. This PR is the mobile half: - Bump `@metamask/ramps-controller` to consume the new contract. No mobile flag-interpretation logic is added: `useHeadlessAllProvidersEnabled` still delegates to the core helper, so the object form flows through the existing UI availability gates automatically. Plan doc: `app/components/UI/Ramp/headless/PLAN_-_ALL_PROVIDERS_SUPPORT.md`, milestone P2.M5 (revision in MetaMask#32906 records the single-flag JSON design). ## **Changelog** CHANGELOG entry: null ## **Related issues** Refs: MetaMask/core#9524 ## **Manual testing steps** 1. In the LaunchDarkly dev environment (or via Settings > Feature flag override once LD serves an object variation), set `money-headless-all-providers` to `{ "enabled": true, "providerIds": ["/providers/<external-browser-provider>"] }` in a region where an in-app aggregator normally wins on reliability. 2. Drive an MM Pay money deposit (Money account > Add funds) and confirm the selected quote comes from the forced provider. 3. Set the flag back to `true` (or remove `providerIds`) and confirm selection reverts to the reliability winner. 4. Set the flag to `false` (or `{ "enabled": false }`) and confirm the flow is native-only again. ## **Screenshots/Recordings** ### **Before** N/A (dev/QA-gated quoting behavior; no UI change) ### **After** Had to allow temporarily fiat transactions in Perps in my local RC, but here's an example of perps allowing Coinbase https://github.com/user-attachments/assets/1b08a4e6-9972-40bb-9d8d-1cc627d8c3ec ## **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** > Touches MM Pay fiat availability and region gating behind a feature flag, with a short “optimistic available” window during provider fetch that could briefly show deposit UI before resolving. > > **Overview** > Bumps **`@metamask/ramps-controller`** to **^18.0.0** so mobile picks up the updated **`moneyHeadlessAllProviders`** contract (JSON payload with **`featureVersion: '1'`** and **`enabled: true`**, plus optional **`providerIds`** handled in core). **`useHeadlessAllProvidersEnabled`** still delegates to core’s **`isHeadlessAllProvidersEnabled`**; tests now cover enabling object payloads, rejecting stale/malformed values, and keeping local override behavior. > > Renames **`useHasNativeFiatProvider`** to **`useHasFiatProvider`** and updates MM Pay gates (**`useIsFiatPaymentAvailable`**, mocks) so naming/docs match flag-aware availability (native-only when the flag is off, any provider class when on). > > Fixes a **cold provider fetch** regression in **`useRegionHasFiatProvider`**: when all-providers widening is on and the catalog is still loading with an empty list after a region switch, the hook temporarily treats the region as available so add-funds entry does not disappear until providers load. > > Docs/comments only elsewhere (**`useRampsBuyLimits`**, **`useFiatConfirm`** notes that **`rampSurface`** is analytics-only). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 00d1fd2. 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: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Explanation
moneyHeadlessAllProvidersis currently a boolean remote flag: the literaltruewidens headless fiat auto-selection quoting to every provider class (#9409). Phase 2 of the Headless Buy all-providers work needs QA to force a specific provider mix (for example external-browser-only) without a client change per test configuration.This PR widens the flag's value contract so the same LaunchDarkly key can carry provider allowlists:
{ "enabled": true, "featureVersion": "1", "providerIds": ["/providers/x"] }.isHeadlessAllProvidersEnabledreturnstruefor a payload whoseenabledis the literaltrueandfeatureVersionis"1"; every other value resolves tofalse, and the boolean forms behave exactly as before. In LaunchDarkly the variation is delivered inside aversionsenvelope (e.g.{ "versions": { "0.0.0": { "enabled": false, "featureVersion": "1" }, "8.4.0": { "enabled": true, "featureVersion": "1" } } });RemoteFeatureFlagControllerresolves that to the flat payload before writing to state, so ramps code always evaluates the flat form.getHeadlessProviderAllowlist(remoteFeatureFlagState)resolves the allowlist from the payload's top-levelproviderIds; empty or malformed lists resolve toundefined(no restriction). Unknown keys and non-string entries are ignored./providers/x) or bare form, case-insensitively, via a module-local normalizer used only for allowlist matching (provider ids still match as-is everywhere else; the v16 removal of id normalization is not reverted). If nothing survives,getQuotesreturns an emptysuccess[]withsorted/error/customActionspreserved.RemoteFeatureFlagController:getStateread pergetQuotescall, so a flag edit during the awaited fetch cannot produce a mixed read.Backwards compatibility, and why this is a minor (not breaking) change: clients on earlier versions coerce any non-boolean value to
false(native-only), so serving the object payload can never turn widening on for a client that cannot parse it, and production servesfalseeverywhere today. No consumer needs code changes to keep working; boolean-only configurations see identical behavior. The documented "any non-boolean resolves to false" contract is widened, not changed, for the values it previously rejected.References
MetaMask/metamask-mobile(app/components/UI/Ramp/headless/PLAN_-_ALL_PROVIDERS_SUPPORT.md), revised to evolve the single existing flag instead of adding a companion flag.Checklist
Note
Medium Risk
Changes which provider wins headless fiat auto-select when the flag carries allowlists; gated by remote flags and backward-compatible for boolean
true, but misconfigured LD payloads could yield empty quotes in production.Overview
Extends
moneyHeadlessAllProvidersso LaunchDarkly can send an object payload (enabled,featureVersion: '1', optionalproviderIds, optionalminimumVersion) in addition to booleantrue.isHeadlessAllProvidersEnabledtreats only literaltrueor a version-matched enabled object as on; other shapes (including{ enabled: true }withoutfeatureVersion) stay off, while boolean-only configs behave as before.New helpers
getHeadlessProviderAllowlist,getHeadlessAllProvidersMinimumVersion, andHEADLESS_ALL_PROVIDERS_FEATURE_VERSIONcentralize parsing with the same remote/local override merge.RampsController.getQuotesreads enabled + allowlist in one flag snapshot; on the widened auto-select path,#pickWidenedQuotedrops quotes from unlisted providers (case-insensitive match after normalizing/providers/vs bare ids) before fiat limits and reliability/price ranking. If every candidate is filtered out,success[]is empty whilesorted,error, andcustomActionsare unchanged.Reviewed by Cursor Bugbot for commit 7737b6d. Bugbot is set up for automated code reviews on this repo. Configure here.