diff --git a/packages/remote-feature-flag-controller/CHANGELOG.md b/packages/remote-feature-flag-controller/CHANGELOG.md index 2bebdf0741c..56ddba48112 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 `^11.17.0` to `^11.18.0` ([#7583](https://github.com/MetaMask/core/pull/7583)) +### Fixed + +- Add optional `prevClientVersion` constructor argument to invalidate cached flags when the client version changes ([#7827](https://github.com/MetaMask/core/pull/7827)) + ## [4.0.0] ### Changed 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 3de6ce24ed7..2a329ce3e83 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 @@ -65,6 +65,7 @@ function createController( disabled: boolean; getMetaMetricsId: () => string; clientVersion: string; + prevClientVersion: string; }> = {}, ): RemoteFeatureFlagController { return new RemoteFeatureFlagController({ @@ -77,6 +78,7 @@ function createController( options.getMetaMetricsId ?? ((): typeof MOCK_METRICS_ID => MOCK_METRICS_ID), clientVersion: options.clientVersion ?? MOCK_BASE_VERSION, + prevClientVersion: options.prevClientVersion, }); } @@ -186,6 +188,94 @@ describe('RemoteFeatureFlagController', () => { expect(controller.state.remoteFeatureFlags).toStrictEqual(MOCK_FLAGS); }); + it('resets cache and fetch when clientVersion changes', async () => { + const versionedFlags = { + exploreFeature: { + versions: { + '7.62.0': { enabled: false }, + '7.64.0': { enabled: true }, + }, + }, + }; + const clientConfigApiService = buildClientConfigApiService({ + remoteFeatureFlags: versionedFlags, + }); + + /** + * Test Util - Arrange, Act, Assert for client version change + * + * @param opts - The options for the arrangeActAssertClientVersionChange test + * @param opts.clientVersion - The client version to use + * @param opts.prevClientVersion - The previous client version to use + * @param opts.controllerState - The controller state to use + * @param opts.expectedFeatureFlagState - The expected feature flag state + * @returns The controller state after the arrangeActAssertClientVersionChange test + */ + const arrangeActAssertClientVersionChange = async (opts: { + clientVersion: string; + prevClientVersion?: string; + controllerState?: RemoteFeatureFlagControllerState; + expectedFeatureFlagState: boolean; + }): Promise => { + const { + clientVersion, + prevClientVersion, + controllerState, + expectedFeatureFlagState, + } = opts; + + // Arrange - setup controller + jest.clearAllMocks(); + const controller = createController({ + clientConfigApiService, + clientVersion, + prevClientVersion, + state: controllerState, + }); + + // Assert - controller cache is set to 0 (either by initial state or reset by client version change) + expect(controller.state.cacheTimestamp).toBe(0); + + // Act / Assert - We should make a network request and update the cache (cache is reset to 0) + await controller.updateRemoteFeatureFlags(); + expect( + clientConfigApiService.fetchRemoteFeatureFlags, + ).toHaveBeenCalledTimes(1); + + // Act / Assert - subsequent fetches should not call network again (cache is populated) + await controller.updateRemoteFeatureFlags(); + expect( + clientConfigApiService.fetchRemoteFeatureFlags, + ).toHaveBeenCalledTimes(1); + + // Assert - flag state is as expected + expect( + controller.state.remoteFeatureFlags.exploreFeature, + ).toStrictEqual({ + enabled: expectedFeatureFlagState, + }); + + // Assert - cache timestamp has been updated + expect(controller.state.cacheTimestamp).toBeGreaterThan(0); + + return controller.state; + }; + + // Test: New controller initialized (no previous state) + const controllerState = await arrangeActAssertClientVersionChange({ + clientVersion: '7.62.0', + expectedFeatureFlagState: false, + }); + + // Test: Updated controller with a previous client version + await arrangeActAssertClientVersionChange({ + clientVersion: '7.64.0', + prevClientVersion: '7.62.0', + controllerState, + expectedFeatureFlagState: true, + }); + }); + it('makes a network request to fetch when cache is expired, and then updates the cache', async () => { const clientConfigApiService = buildClientConfigApiService({ cacheTimestamp: Date.now() - 10000, 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 c26fddf174c..f05d3147ab1 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 @@ -168,6 +168,7 @@ export class RemoteFeatureFlagController extends BaseController< * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false. * @param options.getMetaMetricsId - Returns metaMetricsId. * @param options.clientVersion - The current client version for version-based feature flag filtering. Must be a valid 3-part SemVer version string. + * @param options.prevClientVersion - The previous client version for feature flag cache invalidation. */ constructor({ messenger, @@ -177,6 +178,7 @@ export class RemoteFeatureFlagController extends BaseController< disabled = false, getMetaMetricsId, clientVersion, + prevClientVersion, }: { messenger: RemoteFeatureFlagControllerMessenger; state?: Partial; @@ -185,6 +187,7 @@ export class RemoteFeatureFlagController extends BaseController< fetchInterval?: number; disabled?: boolean; clientVersion: string; + prevClientVersion?: string; }) { if (!isValidSemVerVersion(clientVersion)) { throw new Error( @@ -192,13 +195,24 @@ export class RemoteFeatureFlagController extends BaseController< ); } + const initialState: RemoteFeatureFlagControllerState = { + ...getDefaultRemoteFeatureFlagControllerState(), + ...state, + }; + + const hasClientVersionChanged = + isValidSemVerVersion(prevClientVersion) && + prevClientVersion !== clientVersion; + super({ name: controllerName, metadata: remoteFeatureFlagControllerMetadata, messenger, state: { - ...getDefaultRemoteFeatureFlagControllerState(), - ...state, + ...initialState, + cacheTimestamp: hasClientVersionChanged + ? 0 + : initialState.cacheTimestamp, }, });