diff --git a/packages/remote-feature-flag-controller/CHANGELOG.md b/packages/remote-feature-flag-controller/CHANGELOG.md index 0b7be74d54..0ef1c8e274 100644 --- a/packages/remote-feature-flag-controller/CHANGELOG.md +++ b/packages/remote-feature-flag-controller/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774)) +### Fixed + +- Support `thresholdVersion: 2` threshold feature flag entries that return the selected `value` directly while preserving the existing threshold wrapper shape for unversioned entries ([#8908](https://github.com/MetaMask/core/pull/8908)) + ## [4.2.1] ### Changed diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller-types.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller-types.ts index 6bf8e5ea2f..81b953ba8f 100644 --- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller-types.ts +++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller-types.ts @@ -41,6 +41,8 @@ export type FeatureFlagScope = { export type FeatureFlagScopeValue = { name: string; + thresholdName?: string; + thresholdVersion?: number; scope: FeatureFlagScope; value: Json; }; diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts index 142995b345..f5f1b3fc23 100644 --- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts +++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts @@ -425,6 +425,31 @@ describe('RemoteFeatureFlagController', () => { }); }); + describe('feature flag value normalization', () => { + it('preserves direct feature flag config objects without value metadata', async () => { + const directConfig = { + enabled: true, + minimumVersion: '13.10.0', + }; + const clientConfigApiService = buildClientConfigApiService({ + remoteFeatureFlags: { + directConfig, + }, + }); + const { controller, messenger } = createController({ + clientConfigApiService, + }); + + await messenger.call( + 'RemoteFeatureFlagController:updateRemoteFeatureFlags', + ); + + expect(controller.state.remoteFeatureFlags.directConfig).toStrictEqual( + directConfig, + ); + }); + }); + describe('threshold feature flags', () => { it('processes threshold feature flags based on provided metaMetricsId', async () => { const clientConfigApiService = buildClientConfigApiService({ @@ -448,6 +473,74 @@ describe('RemoteFeatureFlagController', () => { }); }); + it('preserves selected legacy threshold object value wrappers', async () => { + const thresholdFlagValue = { + enabled: true, + minimumVersion: '13.10.0', + attemptsMax: 5, + }; + const mockFlags = { + thresholdObjectFlag: [ + { + name: 'enabled', + scope: { type: 'threshold', value: 1.0 }, + value: thresholdFlagValue, + }, + ], + }; + const clientConfigApiService = buildClientConfigApiService({ + remoteFeatureFlags: mockFlags, + }); + const { controller, messenger } = createController({ + clientConfigApiService, + getMetaMetricsId: () => MOCK_METRICS_ID, + }); + + await messenger.call( + 'RemoteFeatureFlagController:updateRemoteFeatureFlags', + ); + + expect( + controller.state.remoteFeatureFlags.thresholdObjectFlag, + ).toStrictEqual({ + name: 'enabled', + value: thresholdFlagValue, + }); + }); + + it('returns selected threshold version 2 values without wrapper metadata', async () => { + const thresholdFlagValue = { + enabled: true, + minimumVersion: '13.10.0', + attemptsMax: 5, + }; + const mockFlags = { + thresholdObjectFlag: [ + { + thresholdName: 'enabled', + thresholdVersion: 2, + scope: { type: 'threshold', value: 1.0 }, + value: thresholdFlagValue, + }, + ], + }; + const clientConfigApiService = buildClientConfigApiService({ + remoteFeatureFlags: mockFlags, + }); + const { controller, messenger } = createController({ + clientConfigApiService, + getMetaMetricsId: () => MOCK_METRICS_ID, + }); + + await messenger.call( + 'RemoteFeatureFlagController:updateRemoteFeatureFlags', + ); + + expect( + controller.state.remoteFeatureFlags.thresholdObjectFlag, + ).toStrictEqual(thresholdFlagValue); + }); + it('preserves non-threshold feature flags unchanged', async () => { const clientConfigApiService = buildClientConfigApiService({ remoteFeatureFlags: MOCK_FLAGS_WITH_THRESHOLD, diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts index 8f8fbbb4db..94d97c45e2 100644 --- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts +++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts @@ -25,6 +25,10 @@ import { isVersionFeatureFlag, getVersionData } from './utils/version'; export const controllerName = 'RemoteFeatureFlagController'; export const DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day +enum ThresholdVersion { + DirectValue = 2, +} + // === STATE === export type RemoteFeatureFlagControllerState = { @@ -118,6 +122,17 @@ export function getDefaultRemoteFeatureFlagControllerState(): RemoteFeatureFlagC }; } +function normalizeThresholdValue(featureFlag: FeatureFlagScopeValue): Json { + if (featureFlag.thresholdVersion === ThresholdVersion.DirectValue) { + return featureFlag.value; + } + + return { + name: featureFlag.name, + value: featureFlag.value, + }; +} + /** * The RemoteFeatureFlagController manages the retrieval and caching of remote feature flags. * It fetches feature flags from a remote API, caches them, and provides methods to access @@ -371,11 +386,9 @@ export class RemoteFeatureFlagController extends BaseController< return threshold <= featureFlag.scope.value; }, ); + if (selectedGroup) { - processedValue = { - name: selectedGroup.name, - value: selectedGroup.value, - }; + processedValue = normalizeThresholdValue(selectedGroup); } }