Skip to content

Commit c2ea717

Browse files
chore(runway): cherry-pick feat: customize fetchInterval for remoteFeatureFlagController to 15min (#13343)
- feat: customize fetchInterval for remoteFeatureFlagController to 15min (#13341) ## **Description** - add a new app constant for the 15 min fetch interval - add fetch interval param using the constant - add test that controller builder function passes fetch interval param - remove out of scope unit tests (this was testing the controller. This is [already tested in controller itself](https://github.com/MetaMask/core/blob/main/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts) and adds a dangerous and blocking link to non local code. ## **Related issues** Fixes mobile app part of https://github.com/MetaMask/MetaMask-planning/issues/4098 ## **Manual testing steps** (Manual testing only be done by someone with Launch Darkly access) 1. activate a remote feature flag 2. Load app 3. Check feature is activated 4. disable remote feature flag 5. Wait for 15 min 6. Reload the app 7. Check feature is deactivated ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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. ## **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. [508a5f7](508a5f7) --------- Co-authored-by: metamaskbot <[email protected]>
1 parent a22e8c0 commit c2ea717

File tree

4 files changed

+55
-67
lines changed

4 files changed

+55
-67
lines changed

app/core/AppConstants.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { DEFAULT_SERVER_URL } from '@metamask/sdk-communication-layer';
55
const DEVELOPMENT = 'development';
66
const PORTFOLIO_URL =
77
process.env.MM_PORTFOLIO_URL || 'https://portfolio.metamask.io';
8-
const SECURITY_ALERTS_API_URL = process.env.SECURITY_ALERTS_API_URL ?? 'https://security-alerts.api.cx.metamask.io';
8+
const SECURITY_ALERTS_API_URL =
9+
process.env.SECURITY_ALERTS_API_URL ??
10+
'https://security-alerts.api.cx.metamask.io';
911

1012
export default {
1113
IS_DEV: process.env?.NODE_ENV === DEVELOPMENT,
@@ -137,7 +139,9 @@ export default {
137139
'https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/',
138140
STAKING_RISK_DISCLOSURE: 'https://consensys.io/staking-risk-disclosures',
139141
},
140-
DECODING_API_URL: process.env.DECODING_API_URL || 'https://signature-insights.api.cx.metamask.io/v1',
142+
DECODING_API_URL:
143+
process.env.DECODING_API_URL ||
144+
'https://signature-insights.api.cx.metamask.io/v1',
141145
ERRORS: {
142146
INFURA_BLOCKED_MESSAGE:
143147
'EthQuery - RPC Error - This service is not available in your country',
@@ -222,5 +226,6 @@ export default {
222226
FEATURE_FLAGS_API: {
223227
BASE_URL: 'https://client-config.api.cx.metamask.io',
224228
VERSION: 'v1',
229+
DEFAULT_FETCH_INTERVAL: 15 * 60 * 1000, // 15 minutes
225230
},
226231
} as const;
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { RemoteFeatureFlagControllerMessenger, RemoteFeatureFlagControllerState } from '@metamask/remote-feature-flag-controller';
1+
import {
2+
RemoteFeatureFlagControllerMessenger,
3+
RemoteFeatureFlagControllerState,
4+
} from '@metamask/remote-feature-flag-controller';
25

36
export interface RemoteFeatureFlagInitParamTypes {
47
state?: RemoteFeatureFlagControllerState;
58
messenger: RemoteFeatureFlagControllerMessenger;
69
disabled: boolean;
10+
fetchInterval?: number;
711
}

app/core/Engine/controllers/RemoteFeatureFlagController/utils.test.ts

Lines changed: 26 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ import {
55
} from '@metamask/remote-feature-flag-controller';
66
import { createRemoteFeatureFlagController } from './utils';
77

8+
const mockUpdateRemoteFeatureFlags = jest.fn().mockResolvedValue(undefined);
9+
10+
jest.mock('@metamask/remote-feature-flag-controller', () => {
11+
const originalModule = jest.requireActual(
12+
'@metamask/remote-feature-flag-controller',
13+
);
14+
return {
15+
...originalModule,
16+
RemoteFeatureFlagController: jest.fn().mockImplementation((params) => ({
17+
updateRemoteFeatureFlags: mockUpdateRemoteFeatureFlags, // Ensures it returns a resolved promise
18+
...params, // Ensure that fetchInterval and other params are stored
19+
})),
20+
};
21+
});
22+
823
describe('RemoteFeatureFlagController utils', () => {
924
let messenger: RemoteFeatureFlagControllerMessenger;
1025

@@ -15,86 +30,40 @@ describe('RemoteFeatureFlagController utils', () => {
1530
});
1631

1732
describe('createRemoteFeatureFlagController', () => {
18-
it('creates controller with initial undefined state', () => {
19-
const controller = createRemoteFeatureFlagController({
20-
state: undefined,
21-
messenger,
22-
disabled: false,
23-
});
24-
25-
expect(controller).toBeDefined();
26-
27-
// Initializing with am empty object should return an empty obj?
28-
expect(controller.state).toStrictEqual({
29-
cacheTimestamp: 0,
30-
remoteFeatureFlags: {},
31-
});
32-
});
33-
34-
it('internal state matches initial state', () => {
35-
const initialState = {
36-
remoteFeatureFlags: {
37-
testFlag: true,
38-
},
39-
cacheTimestamp: 123,
40-
};
41-
42-
const controller = createRemoteFeatureFlagController({
43-
state: initialState,
44-
messenger,
45-
disabled: false,
46-
});
47-
48-
expect(controller.state).toStrictEqual(initialState);
49-
});
50-
5133
it('calls updateRemoteFeatureFlags when enabled', () => {
52-
const spy = jest.spyOn(
53-
RemoteFeatureFlagController.prototype,
54-
'updateRemoteFeatureFlags',
55-
);
56-
5734
createRemoteFeatureFlagController({
5835
state: undefined,
5936
messenger,
6037
disabled: false,
6138
});
6239

63-
expect(spy).toHaveBeenCalled();
40+
expect(mockUpdateRemoteFeatureFlags).toHaveBeenCalled();
6441
});
6542

6643
it('does not call updateRemoteFeatureFlagscontroller when controller is disabled', () => {
67-
const spy = jest.spyOn(
68-
RemoteFeatureFlagController.prototype,
69-
'updateRemoteFeatureFlags',
70-
);
71-
7244
createRemoteFeatureFlagController({
7345
state: undefined,
7446
messenger,
7547
disabled: true,
7648
});
7749

78-
expect(spy).not.toHaveBeenCalled();
50+
expect(mockUpdateRemoteFeatureFlags).not.toHaveBeenCalled();
7951
});
8052

81-
it('controller keeps initial extra data into its state', () => {
82-
const initialState = {
83-
extraData: true,
84-
};
53+
it('passes fetchInterval to RemoteFeatureFlagController', async () => {
54+
const fetchInterval = 6000;
8555

86-
const controller = createRemoteFeatureFlagController({
87-
// @ts-expect-error giving a wrong initial state
88-
state: initialState,
56+
createRemoteFeatureFlagController({
57+
state: undefined,
8958
messenger,
9059
disabled: false,
60+
fetchInterval,
9161
});
9262

93-
expect(controller.state).toStrictEqual({
94-
cacheTimestamp: 0,
95-
extraData: true,
96-
remoteFeatureFlags: {},
97-
});
63+
// Ensure the constructor was called with fetchInterval
64+
expect(RemoteFeatureFlagController).toHaveBeenCalledWith(
65+
expect.objectContaining({ fetchInterval }),
66+
);
9867
});
9968
});
10069
});

app/core/Engine/controllers/RemoteFeatureFlagController/utils.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,39 @@ import {
99
import Logger from '../../../../util/Logger';
1010

1111
import { RemoteFeatureFlagInitParamTypes } from './types';
12+
import AppConstants from '../../../AppConstants';
1213

1314
const getFeatureFlagAppEnvironment = () => {
1415
const env = process.env.METAMASK_ENVIRONMENT;
1516
switch (env) {
16-
case 'local': return EnvironmentType.Development;
17-
case 'pre-release': return EnvironmentType.ReleaseCandidate;
18-
case 'production': return EnvironmentType.Production;
19-
default: return EnvironmentType.Development;
17+
case 'local':
18+
return EnvironmentType.Development;
19+
case 'pre-release':
20+
return EnvironmentType.ReleaseCandidate;
21+
case 'production':
22+
return EnvironmentType.Production;
23+
default:
24+
return EnvironmentType.Development;
2025
}
2126
};
2227

2328
const getFeatureFlagAppDistribution = () => {
2429
const dist = process.env.METAMASK_BUILD_TYPE;
2530
switch (dist) {
26-
case 'main': return DistributionType.Main;
27-
case 'flask': return DistributionType.Flask;
28-
default: return DistributionType.Main;
31+
case 'main':
32+
return DistributionType.Main;
33+
case 'flask':
34+
return DistributionType.Flask;
35+
default:
36+
return DistributionType.Main;
2937
}
3038
};
3139

3240
export const createRemoteFeatureFlagController = ({
3341
state,
3442
messenger,
3543
disabled,
44+
fetchInterval = AppConstants.FEATURE_FLAGS_API.DEFAULT_FETCH_INTERVAL,
3645
}: RemoteFeatureFlagInitParamTypes) => {
3746
const remoteFeatureFlagController = new RemoteFeatureFlagController({
3847
messenger,
@@ -46,6 +55,7 @@ export const createRemoteFeatureFlagController = ({
4655
distribution: getFeatureFlagAppDistribution(),
4756
},
4857
}),
58+
fetchInterval,
4959
});
5060

5161
if (disabled) {

0 commit comments

Comments
 (0)