Skip to content

Commit ce55b5c

Browse files
chore(runway): cherry-pick fix: cp-7.65.0 MUSD-308 add progressive rollout flag support for validatedVersionGatedFeatureFlag util (#25985)
- fix: cp-7.65.0 MUSD-308 add progressive rollout flag support for validatedVersionGatedFeatureFlag util (#25983) <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR adds progressive rollout flag compatibility to `validatedVersionGatedFeatureFlag` util. Without this, `validatedVersionGatedFeatureFlag` incorrectly returns `undefined`. <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: updated validatedVersionGatedFeatureFlag to support progressive rollout flag format ## **Related issues** Fixes: [MUSD-308: Fix progressive rollout flags compatibility with "validatedVersionGatedFeatureFlag" util](https://consensyssoftware.atlassian.net/browse/MUSD-308) ## **Manual testing steps** ```gherkin Feature: Progressive feature flag support for validatedVersionGatedFeatureFlag Scenario: User correctly sees feature Given user is part of selectedGroup threshold for flag to be enabled When user opens app Then user sees mUSD conversion feature ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> N/A ### **After** <!-- [screenshots/recordings] --> N/A ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, well-tested change to a feature-flag normalization utility; main risk is behavior change for callers passing non-standard flag shapes. > > **Overview** > `validatedVersionGatedFeatureFlag` now accepts `unknown` input and supports a second “progressive rollout” runtime shape where the flag is wrapped under a `value` property, unwrapping and validating before applying the version gate. > > Adds an internal `unwrapVersionGatedFeatureFlag` helper plus new unit tests covering wrapped enabled/disabled flags, version pass/fail, and malformed/missing wrapped values (while preserving the override behavior). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b04b201. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> [c89260c](c89260c) Co-authored-by: Matthew Grainger <46547583+Matt561@users.noreply.github.com>
1 parent 5bde573 commit ce55b5c

2 files changed

Lines changed: 99 additions & 9 deletions

File tree

app/util/remoteFeatureFlag/index.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,18 @@ describe('validatedVersionGatedFeatureFlag', () => {
210210
expect(result).toBe(true);
211211
});
212212

213+
it('returns true when flag is wrapped in a value property and version check passes', () => {
214+
const wrappedFlag = {
215+
value: {
216+
enabled: true,
217+
minimumVersion: '1.0.0',
218+
},
219+
};
220+
221+
const result = validatedVersionGatedFeatureFlag(wrappedFlag);
222+
expect(result).toBe(true);
223+
});
224+
213225
it('returns false when flag is enabled but version check fails', () => {
214226
const flagWithHigherVersion: VersionGatedFeatureFlag = {
215227
enabled: true,
@@ -219,11 +231,35 @@ describe('validatedVersionGatedFeatureFlag', () => {
219231
expect(result).toBe(false);
220232
});
221233

234+
it('returns false when wrapped flag is enabled but version check fails', () => {
235+
const wrappedFlag = {
236+
value: {
237+
enabled: true,
238+
minimumVersion: '99.0.0',
239+
},
240+
};
241+
242+
const result = validatedVersionGatedFeatureFlag(wrappedFlag);
243+
expect(result).toBe(false);
244+
});
245+
222246
it('returns false when flag is disabled regardless of version', () => {
223247
const result = validatedVersionGatedFeatureFlag(validDisabledFlag);
224248
expect(result).toBe(false);
225249
});
226250

251+
it('returns false when wrapped flag is disabled regardless of version', () => {
252+
const wrappedFlag = {
253+
value: {
254+
enabled: false,
255+
minimumVersion: '1.0.0',
256+
},
257+
};
258+
259+
const result = validatedVersionGatedFeatureFlag(wrappedFlag);
260+
expect(result).toBe(false);
261+
});
262+
227263
it('returns false when flag is disabled with higher version', () => {
228264
const disabledFlagWithHigherVersion: VersionGatedFeatureFlag = {
229265
enabled: false,
@@ -272,6 +308,27 @@ describe('validatedVersionGatedFeatureFlag', () => {
272308
expect(result).toBeUndefined();
273309
});
274310

311+
it('returns undefined when flag is wrapped but value is malformed', () => {
312+
const wrappedMalformedFlag = {
313+
value: {
314+
enabled: 'true',
315+
minimumVersion: '1.0.0',
316+
},
317+
};
318+
319+
const result = validatedVersionGatedFeatureFlag(wrappedMalformedFlag);
320+
expect(result).toBeUndefined();
321+
});
322+
323+
it('returns undefined when flag is wrapped but value is missing', () => {
324+
const wrappedMissingValueFlag = {
325+
value: undefined,
326+
};
327+
328+
const result = validatedVersionGatedFeatureFlag(wrappedMissingValueFlag);
329+
expect(result).toBeUndefined();
330+
});
331+
275332
it('returns undefined when enabled property is missing', () => {
276333
const malformedFlag = {
277334
minimumVersion: '1.0.0',

app/util/remoteFeatureFlag/index.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,53 @@ export const hasMinimumRequiredVersion = (minRequiredVersion: string) => {
3333
return compareVersions.compare(currentVersion, minRequiredVersion, '>=');
3434
};
3535

36-
export const validatedVersionGatedFeatureFlag = (
37-
remoteFlag: VersionGatedFeatureFlag,
38-
) => {
39-
// If failed to fetch remote flag or flag is overridden or misconfigured return undefined to trigger fallback
36+
/**
37+
* Normalizes version-gated remote feature flags from two possible runtime shapes.
38+
*
39+
* 1) Direct shape:
40+
* { enabled: true, minimumVersion: '7.65.0' }
41+
*
42+
* 2) Progressive rollout shape :
43+
* { name: undefined | string, value: { enabled: true, minimumVersion: '7.65.0' } }
44+
*
45+
*/
46+
const unwrapVersionGatedFeatureFlag = (
47+
remoteFlag: unknown,
48+
): VersionGatedFeatureFlag | undefined => {
49+
if (isVersionGatedFeatureFlag(remoteFlag)) {
50+
return remoteFlag;
51+
}
52+
4053
if (
41-
isRemoteFeatureFlagOverrideActivated ||
42-
!remoteFlag ||
43-
typeof remoteFlag.enabled !== 'boolean' ||
44-
typeof remoteFlag.minimumVersion !== 'string'
54+
typeof remoteFlag === 'object' &&
55+
remoteFlag !== null &&
56+
'value' in remoteFlag
4557
) {
58+
const wrappedValue = (remoteFlag as { name?: string; value?: unknown })
59+
.value;
60+
if (isVersionGatedFeatureFlag(wrappedValue)) {
61+
return wrappedValue;
62+
}
63+
}
64+
65+
return undefined;
66+
};
67+
68+
export const validatedVersionGatedFeatureFlag = (remoteFlag: unknown) => {
69+
// If remote flag is overridden, return undefined to trigger caller fallback
70+
if (isRemoteFeatureFlagOverrideActivated) {
71+
return undefined;
72+
}
73+
74+
// Support both direct flags and progressive rollout wrapper objects
75+
const normalizedFlag = unwrapVersionGatedFeatureFlag(remoteFlag);
76+
77+
if (!normalizedFlag) {
4678
return undefined;
4779
}
4880

4981
return (
50-
remoteFlag.enabled && hasMinimumRequiredVersion(remoteFlag.minimumVersion)
82+
normalizedFlag.enabled &&
83+
hasMinimumRequiredVersion(normalizedFlag.minimumVersion)
5184
);
5285
};

0 commit comments

Comments
 (0)