From cfab3c3264d8588240e3e3283a4b7cfdfc5cddfc Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Tue, 4 Aug 2026 01:50:26 +0100 Subject: [PATCH 1/3] feat(snap-networks-utils): add shared RemoteFeatureFlagProvider --- packages/snap-networks-utils/CHANGELOG.md | 2 +- packages/snap-networks-utils/package.json | 1 + .../RemoteFeatureFlagProvider.test.ts | 111 ++++++++++++++++++ .../RemoteFeatureFlagProvider.ts | 68 +++++++++++ yarn.lock | 1 + 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.test.ts create mode 100644 packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts diff --git a/packages/snap-networks-utils/CHANGELOG.md b/packages/snap-networks-utils/CHANGELOG.md index 1e0bd139..dd896718 100644 --- a/packages/snap-networks-utils/CHANGELOG.md +++ b/packages/snap-networks-utils/CHANGELOG.md @@ -9,6 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Initial package release ([#82](https://github.com/MetaMask/internal-snaps/pull/82), [#79](https://github.com/MetaMask/internal-snaps/pull/79)) +- Initial package release ([#99](https://github.com/MetaMask/internal-snaps/pull/99), [#82](https://github.com/MetaMask/internal-snaps/pull/82), [#79](https://github.com/MetaMask/internal-snaps/pull/79)) [Unreleased]: https://github.com/MetaMask/internal-snaps/ diff --git a/packages/snap-networks-utils/package.json b/packages/snap-networks-utils/package.json index d094babb..e8955cf6 100644 --- a/packages/snap-networks-utils/package.json +++ b/packages/snap-networks-utils/package.json @@ -62,6 +62,7 @@ }, "dependencies": { "@metamask/assets-controller": "^13.0.0", + "@metamask/remote-feature-flag-controller": "^5.0.0", "@metamask/snaps-sdk": "^11.2.0", "@metamask/utils": "^11.9.0" }, diff --git a/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.test.ts b/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.test.ts new file mode 100644 index 00000000..7ded1ecd --- /dev/null +++ b/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.test.ts @@ -0,0 +1,111 @@ +import type { RemoteFeatureFlagControllerState } from '@metamask/remote-feature-flag-controller'; + +import type { RemoteFeatureFlagProviderMessenger } from './RemoteFeatureFlagProvider'; +import { RemoteFeatureFlagProvider } from './RemoteFeatureFlagProvider'; + +const FLAG_KEY_A = 'flag-a'; +const FLAG_KEY_B = 'flag-b'; + +const DEFAULT_STATE: RemoteFeatureFlagControllerState = { + remoteFeatureFlags: { + [FLAG_KEY_A]: 'value-a', + [FLAG_KEY_B]: 2, + }, + cacheTimestamp: 0, +}; + +type WithRemoteFeatureFlagProviderCallback = (payload: { + remoteFeatureFlagProvider: RemoteFeatureFlagProvider; + mockMessenger: jest.Mocked; +}) => Promise | ReturnValue; + +/** + * Wraps tests for RemoteFeatureFlagProvider by creating a fresh provider with a + * mock messenger. The callback receives the provider and mock for test configuration. + * + * @param testFunction - The test body receiving the provider and mocks. + * @returns The return value of the callback. + */ +async function withRemoteFeatureFlagProvider( + testFunction: WithRemoteFeatureFlagProviderCallback, +): Promise { + const mockMessenger: jest.Mocked = { + call: jest.fn().mockResolvedValue(DEFAULT_STATE), + }; + + const remoteFeatureFlagProvider = new RemoteFeatureFlagProvider({ + messenger: mockMessenger, + }); + + return await testFunction({ + remoteFeatureFlagProvider, + mockMessenger, + }); +} + +describe('RemoteFeatureFlagProvider', () => { + describe('getFeatureFlag', () => { + it('calls RemoteFeatureFlagController:getState and returns the flag value', async () => { + await withRemoteFeatureFlagProvider( + async ({ remoteFeatureFlagProvider, mockMessenger }) => { + const value = + await remoteFeatureFlagProvider.getFeatureFlag(FLAG_KEY_A); + + expect(mockMessenger.call).toHaveBeenCalledWith( + 'RemoteFeatureFlagController:getState', + ); + expect(mockMessenger.call).toHaveBeenCalledTimes(1); + expect(value).toBe('value-a'); + }, + ); + }); + + it('returns undefined when the flag key is missing', async () => { + await withRemoteFeatureFlagProvider( + async ({ remoteFeatureFlagProvider }) => { + const value = + await remoteFeatureFlagProvider.getFeatureFlag('missing-flag'); + + expect(value).toBeUndefined(); + }, + ); + }); + }); + + describe('getFeatureFlags', () => { + it('calls RemoteFeatureFlagController:getState once and returns a keyed map', async () => { + await withRemoteFeatureFlagProvider( + async ({ remoteFeatureFlagProvider, mockMessenger }) => { + const values = await remoteFeatureFlagProvider.getFeatureFlags([ + FLAG_KEY_A, + FLAG_KEY_B, + 'missing-flag', + ]); + + expect(mockMessenger.call).toHaveBeenCalledWith( + 'RemoteFeatureFlagController:getState', + ); + expect(mockMessenger.call).toHaveBeenCalledTimes(1); + expect(values).toStrictEqual({ + [FLAG_KEY_A]: 'value-a', + [FLAG_KEY_B]: 2, + 'missing-flag': undefined, + }); + }, + ); + }); + + it('returns an empty map when no keys are requested', async () => { + await withRemoteFeatureFlagProvider( + async ({ remoteFeatureFlagProvider, mockMessenger }) => { + const values = await remoteFeatureFlagProvider.getFeatureFlags([]); + + expect(mockMessenger.call).toHaveBeenCalledWith( + 'RemoteFeatureFlagController:getState', + ); + expect(values).toStrictEqual({}); + }, + ); + }); + }); +}); diff --git a/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts b/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts new file mode 100644 index 00000000..a358e8bb --- /dev/null +++ b/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts @@ -0,0 +1,68 @@ +import type { Messenger } from '@metamask/messenger'; +import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller'; +import { AsyncMessenger } from '@metamask/snaps-sdk'; +import type { Json } from '@metamask/utils'; + +/** + * Namespace for the {@link RemoteFeatureFlagProvider} messenger. + */ +export const REMOTE_FEATURE_FLAG_PROVIDER_NAME = + 'RemoteFeatureFlagProvider' as const; + +/** + * Actions from other messengers that {@link RemoteFeatureFlagProvider} calls. + */ +export type RemoteFeatureFlagProviderAllowedActions = + RemoteFeatureFlagControllerGetStateAction; + +/** + * Messenger restricted to actions consumed by {@link RemoteFeatureFlagProvider}. + */ +export type RemoteFeatureFlagProviderMessenger = AsyncMessenger< + Messenger< + typeof REMOTE_FEATURE_FLAG_PROVIDER_NAME, + RemoteFeatureFlagProviderAllowedActions + > +>; + +export class RemoteFeatureFlagProvider { + readonly #messenger: RemoteFeatureFlagProviderMessenger; + + constructor({ + messenger, + }: { + messenger: RemoteFeatureFlagProviderMessenger; + }) { + this.#messenger = messenger; + } + + /** + * Returns a single remote feature flag value, or `undefined` if missing. + * + * @param flagKey - LaunchDarkly / client-config feature flag key. + * @returns Flag value, or `undefined`. + */ + async getFeatureFlag(flagKey: string): Promise { + const { remoteFeatureFlags } = await this.#messenger.call('RemoteFeatureFlagController:getState'); + + return remoteFeatureFlags[flagKey]; + } + + /** + * Returns remote feature flag values for the given keys, keyed by flag key. + * Missing keys are included with value `undefined`. + * + * @param flagKeys - Feature flag keys to resolve. + * @returns Map of flag key → value. + */ + async getFeatureFlags( + flagKeys: string[], + ): Promise> { + const { remoteFeatureFlags } = await this.#messenger.call('RemoteFeatureFlagController:getState'); + + return flagKeys.reduce>((acc, flagKey) => { + acc[flagKey] = remoteFeatureFlags[flagKey]; + return acc; + }, {}); + } +} diff --git a/yarn.lock b/yarn.lock index 6e5ceba7..d12f3131 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3299,6 +3299,7 @@ __metadata: "@metamask/assets-controller": "npm:^13.0.0" "@metamask/auto-changelog": "npm:^6.1.1" "@metamask/messenger": "npm:^2.0.0" + "@metamask/remote-feature-flag-controller": "npm:^5.0.0" "@metamask/snaps-sdk": "npm:^11.2.0" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" From 43ec7ee10461df482ca07c39e3d6f582be922626 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Tue, 4 Aug 2026 08:36:53 +0100 Subject: [PATCH 2/3] fix(snap-networks-utils): format RemoteFeatureFlagProvider for oxfmt --- .../remote-feature-flag/RemoteFeatureFlagProvider.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts b/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts index a358e8bb..c61b192c 100644 --- a/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts +++ b/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts @@ -43,7 +43,9 @@ export class RemoteFeatureFlagProvider { * @returns Flag value, or `undefined`. */ async getFeatureFlag(flagKey: string): Promise { - const { remoteFeatureFlags } = await this.#messenger.call('RemoteFeatureFlagController:getState'); + const { remoteFeatureFlags } = await this.#messenger.call( + 'RemoteFeatureFlagController:getState', + ); return remoteFeatureFlags[flagKey]; } @@ -55,10 +57,10 @@ export class RemoteFeatureFlagProvider { * @param flagKeys - Feature flag keys to resolve. * @returns Map of flag key → value. */ - async getFeatureFlags( - flagKeys: string[], - ): Promise> { - const { remoteFeatureFlags } = await this.#messenger.call('RemoteFeatureFlagController:getState'); + async getFeatureFlags(flagKeys: string[]): Promise> { + const { remoteFeatureFlags } = await this.#messenger.call( + 'RemoteFeatureFlagController:getState', + ); return flagKeys.reduce>((acc, flagKey) => { acc[flagKey] = remoteFeatureFlags[flagKey]; From a0892371000a474f9d8f4e704b98cb01d7a368b8 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Tue, 4 Aug 2026 09:42:07 +0100 Subject: [PATCH 3/3] fix(snap-networks-utils): format getFeatureFlags signature for oxfmt --- .../remote-feature-flag/RemoteFeatureFlagProvider.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts b/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts index c61b192c..6488a589 100644 --- a/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts +++ b/packages/snap-networks-utils/src/providers/remote-feature-flag/RemoteFeatureFlagProvider.ts @@ -57,7 +57,9 @@ export class RemoteFeatureFlagProvider { * @param flagKeys - Feature flag keys to resolve. * @returns Map of flag key → value. */ - async getFeatureFlags(flagKeys: string[]): Promise> { + async getFeatureFlags( + flagKeys: string[], + ): Promise> { const { remoteFeatureFlags } = await this.#messenger.call( 'RemoteFeatureFlagController:getState', );