Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/snap-networks-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
1 change: 1 addition & 0 deletions packages/snap-networks-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ReturnValue> = (payload: {
remoteFeatureFlagProvider: RemoteFeatureFlagProvider;
mockMessenger: jest.Mocked<RemoteFeatureFlagProviderMessenger>;
}) => Promise<ReturnValue> | 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<ReturnValue>(
testFunction: WithRemoteFeatureFlagProviderCallback<ReturnValue>,
): Promise<ReturnValue> {
const mockMessenger: jest.Mocked<RemoteFeatureFlagProviderMessenger> = {
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({});
},
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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<Json | undefined> {
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<Record<string, Json | undefined>> {
const { remoteFeatureFlags } = await this.#messenger.call(
'RemoteFeatureFlagController:getState',
);

return flagKeys.reduce<Record<string, Json | undefined>>((acc, flagKey) => {
acc[flagKey] = remoteFeatureFlags[flagKey];
return acc;
}, {});
}
}
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading