Skip to content

Commit 35328c8

Browse files
asalsyscursoragent
andauthored
chore: update version gated feature flag docs (#32159)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> <!-- mms-check directive vocabulary — read by .github/scripts/shared/pr-template-checks.ts at module load to build the validation plan. Directives are invisible in rendered markdown and must NOT be removed or edited without updating the validator registry. type=text Section must contain non-placeholder prose. type=changelog Section must have a valid CHANGELOG entry: line. type=issue-link Section must have a Fixes:/Closes:/Refs: line with a value. type=manual-testing Section must have real testing steps or an explicit N/A. type=screenshot Section must have evidence (image/URL) or an explicit N/A. type=checklist Section must have all checkboxes consciously checked. required=true|false Whether a missing/invalid section runs the validator at all. blocking=true|false Whether a failure of this check fails the CI workflow. Default: false — failures are shown as warnings in the sticky comment but do not block the PR. Sections without a directive are checked for structural presence only. --> ## **Description** <!-- mms-check: type=text required=true --> Version-gated remote feature flags are evaluated in many places across the codebase, but guidance was scattered across team-specific docs and there was no single canonical pattern for agents and contributors to follow. This PR centralizes that guidance: 1. **New canonical guide** — `docs/readme/version-gated-feature-flags.md` documents the SSOT (`app/util/remoteFeatureFlag`), the required selector pattern using `validatedVersionGatedFeatureFlag`, non-standard flag shapes, and forbidden duplicate helpers. 2. **Cursor agent rule** — `.cursor/rules/version-gated-feature-flags.mdc` enforces the same patterns for AI-assisted edits under `app/**/*.{ts,tsx}`. 3. **Cross-doc updates** — Perps, Predict, mUSD, and Compliance docs now link to the canonical guide and show correct import/usage examples instead of implying ad-hoc version checks. 4. **Repo hygiene** — `.gitignore` is updated to track `*.mdc` Cursor rules (matching the existing carve-out pattern for other IDE config), and `AGENTS.md` references the new guide in the development guidelines table and quick reference. No runtime or selector code changes — documentation and agent guidance only. ## **Changelog** <!-- mms-check: type=changelog required=true blocking=true --> CHANGELOG entry: null ## **Related issues** <!-- mms-check: type=issue-link required=true --> Refs: fixes: [MCWP-653](https://consensyssoftware.atlassian.net/browse/MCWP-653) ## **Manual testing steps** <!-- mms-check: type=manual-testing required=true --> N/A — documentation-only change. Verified by reading rendered markdown links and confirming `.cursor/rules/version-gated-feature-flags.mdc` is tracked after the `.gitignore` carve-out. ## **Screenshots/Recordings** <!-- mms-check: type=screenshot required=true --> N/A — no UI changes. ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** <!-- mms-check: type=checklist required=true --> - [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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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. [MCWP-653]: https://consensyssoftware.atlassian.net/browse/MCWP-653?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Docs and agent guidance only; no runtime, selector, or feature-flag behavior changes. > > **Overview** > Adds a **canonical guide** at `docs/readme/version-gated-feature-flags.md` and a matching **Cursor rule** (`.cursor/rules/version-gated-feature-flags.mdc`) so contributors and agents use `validatedVersionGatedFeatureFlag` from `app/util/remoteFeatureFlag` in selectors instead of duplicating version checks. > > **Cross-links and examples** in Perps, Predict, mUSD, and Compliance docs now point at that guide and show the shared import pattern. **`AGENTS.md`** adds the guide to the dev guidelines table and quick reference. > > **`.gitignore`** is adjusted so tracked `*.mdc` rules under `.cursor/rules/` can be committed (carve-out for IDE config). No app or selector code changes. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0388626. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0a7dfc9 commit 35328c8

9 files changed

Lines changed: 240 additions & 17 deletions
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
description: Use shared version-gated feature flag helpers; never duplicate hasMinimumRequiredVersion or validatedVersionGatedFeatureFlag
3+
globs: app/**/*.{ts,tsx}
4+
alwaysApply: false
5+
---
6+
7+
# Version-Gated Feature Flags
8+
9+
> Canonical human guide: [`docs/readme/version-gated-feature-flags.md`](../../docs/readme/version-gated-feature-flags.md)
10+
11+
## Canonical source (SSOT)
12+
13+
All version-gated feature flag logic lives in `app/util/remoteFeatureFlag/index.ts`:
14+
15+
- `validatedVersionGatedFeatureFlag` — preferred for remote flags shaped as `{ enabled, minimumVersion }` (or progressive-rollout wrappers)
16+
- `hasMinimumRequiredVersion` — version comparison only; import when needed, never redefine
17+
18+
## Required patterns
19+
20+
### ✅ Use `validatedVersionGatedFeatureFlag` in selectors
21+
22+
```typescript
23+
import { validatedVersionGatedFeatureFlag, VersionGatedFeatureFlag } from '../../../util/remoteFeatureFlag';
24+
25+
export const selectMyFeatureEnabled = createSelector(
26+
selectRemoteFeatureFlags,
27+
(remoteFeatureFlags) => {
28+
const remoteFlag = remoteFeatureFlags?.myFeature as unknown as VersionGatedFeatureFlag;
29+
return validatedVersionGatedFeatureFlag(remoteFlag) ?? false;
30+
},
31+
);
32+
```
33+
34+
### ✅ Non-standard flag shapes — map, don't duplicate
35+
36+
Configs that use `active` instead of `enabled` (e.g. `depositConfig`) must map to the canonical shape and call the shared helper:
37+
38+
```typescript
39+
import { validatedVersionGatedFeatureFlag } from '../../../util/remoteFeatureFlag';
40+
41+
return (
42+
validatedVersionGatedFeatureFlag({
43+
enabled: depositConfig.active ?? false,
44+
minimumVersion: depositConfig.minimumVersion ?? '',
45+
}) ?? false
46+
);
47+
```
48+
49+
### ✅ Import `hasMinimumRequiredVersion` only from the util
50+
51+
```typescript
52+
import { hasMinimumRequiredVersion } from '../../../util/remoteFeatureFlag';
53+
```
54+
55+
Never copy the `compare-versions` + `getVersion` pattern into feature modules.
56+
57+
## Forbidden patterns
58+
59+
### ❌ Do not define local version-gating helpers
60+
61+
```typescript
62+
// FORBIDDEN — duplicate of app/util/remoteFeatureFlag
63+
function hasMinimumRequiredVersion(minVersion: string) { ... }
64+
const validatedVersionGatedFeatureFlag = (flag: unknown) => { ... }
65+
```
66+
67+
### ❌ Do not inline version checks for feature flags
68+
69+
```typescript
70+
// FORBIDDEN
71+
import { getVersion } from 'react-native-device-info';
72+
import compareVersions from 'compare-versions';
73+
74+
const ok = compareVersions.compare(getVersion(), flag.minimumVersion, '>=');
75+
```
76+
77+
### ❌ Do not add duplicate utils under feature folders
78+
79+
Do not create files like:
80+
81+
- `app/components/UI/**/utils/hasMinimumRequiredVersion.ts`
82+
- `app/core/redux/slices/**/hasMinimumRequiredVersion.ts`
83+
84+
## Where logic belongs
85+
86+
| Layer | Responsibility |
87+
|-------|----------------|
88+
| `RemoteFeatureFlagController` | Fetch/store raw flags — **no** client version gating |
89+
| `app/util/remoteFeatureFlag` | Version comparison + `validatedVersionGatedFeatureFlag` |
90+
| Selectors (`featureFlagController`, `**/selectors/featureFlags`) | Call shared helpers; return `boolean` |
91+
| Hooks / components | `useSelector(selectXEnabled)` — not local version math |
92+
93+
## Enforcement
94+
95+
- **REJECT** new local definitions of `hasMinimumRequiredVersion` or `validatedVersionGatedFeatureFlag`
96+
- **REJECT** new `compare-versions` + `getVersion` usage for feature-flag gating outside `app/util/remoteFeatureFlag`
97+
- **REQUIRE** imports from `app/util/remoteFeatureFlag` for all version-gated flag checks

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,12 @@ release-signoffs.json
220220
.claude/skills/
221221
.claude/commands/
222222
.agents/skills/
223-
.cursor/rules/
223+
.cursor/rules/*
224+
!.cursor/rules/*.mdc
224225
.cursor/skills/
225226
.cursor/commands/
226227
# Carve-outs: IDE/bugbot config that must stay tracked
228+
!.cursor/rules/*.mdc
227229
!.cursor/BUGBOT.md
228230
!.cursor/hooks.json
229231
!.cursor/worktrees.json

AGENTS.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,12 @@ scripts/ # Build and automation scripts
101101

102102
**Detailed guidelines are in `docs/testing/`** (canonical, in-repo) and the `mms-*` skill set installed via `yarn skills` (Cursor / Codex / Claude harnesses):
103103

104-
| Guide | Scope |
105-
| ------------------------------------------------------------------------------ | ------------------------------------------------- |
106-
| [`docs/testing/unit-testing.md`](docs/testing/unit-testing.md) | `*.test.*` files — test patterns, mocking, AAA |
107-
| [`docs/testing/e2e-testing.md`](docs/testing/e2e-testing.md) | Detox smoke/regression — Page Objects, gestures |
108-
| [`docs/testing/component-view-tests.md`](docs/testing/component-view-tests.md) | `*.view.test.tsx` — framework, presets, renderers |
104+
| Guide | Scope |
105+
| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
106+
| [`docs/testing/unit-testing.md`](docs/testing/unit-testing.md) | `*.test.*` files — test patterns, mocking, AAA |
107+
| [`docs/testing/e2e-testing.md`](docs/testing/e2e-testing.md) | Detox smoke/regression — Page Objects, gestures |
108+
| [`docs/testing/component-view-tests.md`](docs/testing/component-view-tests.md) | `*.view.test.tsx` — framework, presets, renderers |
109+
| [`docs/readme/version-gated-feature-flags.md`](docs/readme/version-gated-feature-flags.md) | Version-gated remote flags — `validatedVersionGatedFeatureFlag` in selectors |
109110

110111
General coding, UI, deeplink-handler, and PR-creation guidance now lives in the centralized `mms-*` skill set installed via `yarn skills` (see `.agents/skills/mms-*` after sync).
111112

@@ -115,6 +116,7 @@ General coding, UI, deeplink-handler, and PR-creation guidance now lives in the
115116
- **Components**: Design system first → `component-library` second → custom last
116117
- **Styling**: Use `useTailwind()` hook, `Box`/`Text` components, design tokens
117118
- **Testing**: Mandatory for all code, AAA pattern, mock everything external
119+
- **Version-gated feature flags**: Use `validatedVersionGatedFeatureFlag` from `app/util/remoteFeatureFlag` in selectors — see [`docs/readme/version-gated-feature-flags.md`](docs/readme/version-gated-feature-flags.md) and [`.cursor/rules/version-gated-feature-flags.mdc`](.cursor/rules/version-gated-feature-flags.mdc)
118120
- **Commands**: ONLY use yarn (never npm/npx)
119121

120122
## Environment Setup

docs/compliance.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ To enable compliance:
5050
1. Set `complianceEnabled: true` in LaunchDarkly (or via the local feature flag override screen in dev builds).
5151
2. Compliance status will be fetched per-address whenever a gated action is triggered.
5252

53-
> **Version gating:** The feature flag uses `validatedVersionGatedFeatureFlag`, which compares against the **native binary version** reported by `react-native-device-info`'s `getVersion()` — not `package.json`. If your device has a stale build installed, the version check may fail even if the flag is enabled. Do a clean native rebuild to pick up the correct version.
53+
> **Version gating:** The feature flag uses `validatedVersionGatedFeatureFlag` in `selectComplianceEnabled`, which compares against the **native binary version** reported by `react-native-device-info`'s `getVersion()` — not `package.json`. If your device has a stale build installed, the version check may fail even if the flag is enabled. Do a clean native rebuild to pick up the correct version.
5454
5555
### Feature flag selector
5656

@@ -60,6 +60,8 @@ import { selectComplianceEnabled } from 'app/selectors/featureFlagController/com
6060
const isEnabled = useSelector(selectComplianceEnabled);
6161
```
6262

63+
The selector resolves the remote `{ enabled, minimumVersion }` payload via `validatedVersionGatedFeatureFlag`. See [`docs/readme/version-gated-feature-flags.md`](./readme/version-gated-feature-flags.md).
64+
6365
## Usage in Flows
6466

6567
### Option 1: `gate()` from `useComplianceGate` (recommended for action guards)

docs/musd/musd-convert-list.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,9 +461,14 @@ export const selectMusdConversionStatuses = createSelector(
461461

462462
**Path:** `app/components/UI/Earn/selectors/featureFlags/index.ts`
463463

464-
Add new version-gated flag with local fallback (following the same pattern as other mUSD flags):
464+
Add new version-gated flag with local fallback (see [`docs/readme/version-gated-feature-flags.md`](../../readme/version-gated-feature-flags.md)):
465465

466466
```typescript
467+
import {
468+
validatedVersionGatedFeatureFlag,
469+
type VersionGatedFeatureFlag,
470+
} from '../../../../util/remoteFeatureFlag';
471+
467472
export const selectMusdQuickConvertEnabledFlag = createSelector(
468473
selectRemoteFeatureFlags,
469474
(remoteFeatureFlags) => {

docs/perps/perps-feature-flags.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Framework for controlling Perps feature availability through LaunchDarkly with l
77
**Key Design Principles:**
88

99
- LaunchDarkly is the single source of truth for feature enablement
10-
- Version-gated flags ensure features only activate on compatible app versions
10+
- Version-gated flags use `validatedVersionGatedFeatureFlag` from `app/util/remoteFeatureFlag` (see [Version-gated feature flags](../readme/version-gated-feature-flags.md))
1111
- Local environment variables provide development/testing fallback
1212
- Graceful degradation when LaunchDarkly is unavailable
1313

@@ -74,6 +74,11 @@ See [Perps A/B Testing Framework](./perps-ab-testing.md) for variant-based flags
7474
**File:** `app/components/UI/Perps/selectors/featureFlags/index.ts`
7575

7676
```typescript
77+
import {
78+
validatedVersionGatedFeatureFlag,
79+
type VersionGatedFeatureFlag,
80+
} from '../../../../util/remoteFeatureFlag';
81+
7782
/**
7883
* Selector for My Feature flag
7984
* Controls visibility of My Feature in the UI
@@ -230,12 +235,14 @@ These flags are managed via `FeatureFlagConfigurationService` and control runtim
230235

231236
### Version Gating
232237

233-
The `minimumVersion` field ensures features only activate on compatible app versions:
238+
The `minimumVersion` field ensures features only activate on compatible app versions. **Do not** compare versions in selectors or components — use `validatedVersionGatedFeatureFlag`:
234239

235-
- **Format:** Semantic version string (e.g., `"7.60.0"`)
236-
- **Comparison:** Uses `compare-versions` library with `>=` operator
240+
- **Format:** Semantic version string (e.g. `"7.60.0"`)
241+
- **Evaluation:** `validatedVersionGatedFeatureFlag(remoteFlag)` returns `true` only when `enabled` is true **and** the native app version is `>= minimumVersion`
237242
- **Use case:** Prevent feature activation on older app versions that lack required code
238243

244+
See [`docs/readme/version-gated-feature-flags.md`](../readme/version-gated-feature-flags.md) for the full pattern and non-standard flag shapes.
245+
239246
---
240247

241248
## Troubleshooting
@@ -249,8 +256,8 @@ The `minimumVersion` field ensures features only activate on compatible app vers
249256
### Version Gating Not Working
250257

251258
1. **Verify `minimumVersion` format:** Must be valid semver string
252-
2. **Check app version:** `getVersion()` from `react-native-device-info`
253-
3. **Check comparison:** Uses `>=` operator
259+
2. **Check native app version:** `getVersion()` from `react-native-device-info` (used inside `validatedVersionGatedFeatureFlag`) — not `package.json`
260+
3. **Confirm selector uses the helper:** `validatedVersionGatedFeatureFlag(remoteFlag) ?? localFallback` — not inline `compare-versions`
254261

255262
### Local Flag Not Overriding
256263

@@ -265,8 +272,9 @@ The `minimumVersion` field ensures features only activate on compatible app vers
265272
- **Selectors:** `app/components/UI/Perps/selectors/featureFlags/index.ts`
266273
- **Mocks:** `app/components/UI/Perps/mocks/remoteFeatureFlagMocks.ts`
267274
- **Tests:** `app/components/UI/Perps/selectors/featureFlags/index.test.ts`
268-
- **Version validation:** `app/util/remoteFeatureFlag/index.ts`
269-
- **Controller init:** `app/core/Engine/wallet-init/instance-options/remote-feature-flag-controller.ts` (RFFC instance options) and `app/core/Engine/wallet-init/initialization.ts` (constructed via `@metamask/wallet`)
275+
- **Version validation:** `app/util/remoteFeatureFlag/index.ts` (`validatedVersionGatedFeatureFlag`)
276+
- **Version-gated flags guide:** `docs/readme/version-gated-feature-flags.md`
277+
- **Controller init:** `app/core/Engine/controllers/remote-feature-flag-controller-init.ts`
270278
- **Configuration service:** `app/controllers/perps/services/FeatureFlagConfigurationService.ts`
271279

272280
---

docs/predict/live-nfl-architecture.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ The Live NFL feature introduces real-time sports betting functionality with WebS
88

99
## Feature Flag
1010

11-
The feature is gated behind `predictLiveNflEnabled`:
11+
The feature is gated behind `predictLiveNflEnabled` using `validatedVersionGatedFeatureFlag` (see [`docs/readme/version-gated-feature-flags.md`](../readme/version-gated-feature-flags.md)):
1212

1313
```typescript
1414
// app/components/UI/Predict/selectors/featureFlags/index.ts
15+
import {
16+
validatedVersionGatedFeatureFlag,
17+
type VersionGatedFeatureFlag,
18+
} from '../../../../util/remoteFeatureFlag';
19+
1520
export const selectPredictLiveNflEnabled = createSelector(
1621
selectRemoteFeatureFlags,
1722
(remoteFeatureFlags) => {

docs/predict/tasks/00-feature-flag-and-types.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ Set up the feature flag for Live NFL and define all necessary TypeScript types f
2727
Create/update `app/components/UI/Predict/selectors/featureFlags/index.ts`:
2828

2929
```typescript
30+
import {
31+
validatedVersionGatedFeatureFlag,
32+
type VersionGatedFeatureFlag,
33+
} from '../../../../util/remoteFeatureFlag';
34+
3035
/**
3136
* Selector for Predict Live NFL feature enablement
3237
*/
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Version-Gated Feature Flags
2+
3+
Canonical guide for evaluating remote feature flags that include a minimum app version. Applies to all teams (Ramp, Perps, Money, Predict, platform selectors, etc.).
4+
5+
## SSOT
6+
7+
| Concern | Location |
8+
| ------------------------------------ | -------------------------------------------------------------------------------------- |
9+
| Version comparison + flag validation | [`app/util/remoteFeatureFlag/index.ts`](../../app/util/remoteFeatureFlag/index.ts) |
10+
| Raw flag storage | `RemoteFeatureFlagController.remoteFeatureFlags` (no client version gating) |
11+
| Boolean for UI / hooks | Selectors under `app/selectors/featureFlagController/` or `**/selectors/featureFlags/` |
12+
13+
**Always use** `validatedVersionGatedFeatureFlag` in selectors. **Never** duplicate `hasMinimumRequiredVersion` or `validatedVersionGatedFeatureFlag` in feature modules.
14+
15+
`hasMinimumRequiredVersion` is a lower-level helper used inside `validatedVersionGatedFeatureFlag`. Import it from the util only when you need a standalone version check — do not reimplement it.
16+
17+
## Flag shapes
18+
19+
### Standard (preferred)
20+
21+
LaunchDarkly JSON:
22+
23+
```json
24+
{
25+
"enabled": true,
26+
"minimumVersion": "7.65.0"
27+
}
28+
```
29+
30+
Progressive-rollout wrappers (`{ name, value: { enabled, minimumVersion } }`) are normalized automatically.
31+
32+
### Non-standard configs
33+
34+
Some flags use different property names (e.g. `depositConfig` uses `active` instead of `enabled`). **Map** to the canonical shape before calling the helper:
35+
36+
```typescript
37+
validatedVersionGatedFeatureFlag({
38+
enabled: depositConfig.active ?? false,
39+
minimumVersion: depositConfig.minimumVersion ?? '',
40+
}) ?? false;
41+
```
42+
43+
### Multi-version flags (controller-processed)
44+
45+
`RemoteFeatureFlagController` resolves flags shaped as `{ versions: { "7.53.0": value, "7.60.0": value } }` at fetch time using `clientVersion`. Consumers read the **processed** value from state — no `validatedVersionGatedFeatureFlag` call needed for that shape.
46+
47+
## Implementation pattern
48+
49+
### 1. Selector (required)
50+
51+
```typescript
52+
import { createSelector } from 'reselect';
53+
import { selectRemoteFeatureFlags } from '../index'; // adjust path
54+
import {
55+
validatedVersionGatedFeatureFlag,
56+
type VersionGatedFeatureFlag,
57+
} from '../../../util/remoteFeatureFlag';
58+
59+
export const selectMyFeatureEnabled = createSelector(
60+
selectRemoteFeatureFlags,
61+
(remoteFeatureFlags) => {
62+
const localFlag = process.env.MM_MY_FEATURE_ENABLED === 'true';
63+
const remoteFlag =
64+
remoteFeatureFlags?.myFeature as unknown as VersionGatedFeatureFlag;
65+
66+
return validatedVersionGatedFeatureFlag(remoteFlag) ?? localFlag;
67+
},
68+
);
69+
```
70+
71+
- Remote flag wins when valid; `undefined` from the helper triggers env/local fallback.
72+
- When `OVERRIDE_REMOTE_FEATURE_FLAGS=true`, the helper returns `undefined` so local overrides apply.
73+
74+
### 2. Hook or component
75+
76+
```typescript
77+
const isEnabled = useSelector(selectMyFeatureEnabled);
78+
```
79+
80+
Do not call `getVersion()`, `compare-versions`, or local version helpers in hooks/components.
81+
82+
## Version source
83+
84+
`validatedVersionGatedFeatureFlag` compares against the **native binary version** from `react-native-device-info`'s `getVersion()` — not `package.json`. After a version bump, do a clean native rebuild if gating behaves unexpectedly.
85+
86+
## Forbidden
87+
88+
- Local `function hasMinimumRequiredVersion(...)` copies
89+
- Inline `compareVersions.compare(getVersion(), minimumVersion, '>=')` for feature flags
90+
- Duplicate util files under `app/components/UI/**/utils/` or `app/core/redux/slices/**/`
91+
92+
## Related
93+
94+
- [Perps feature flags](../perps/perps-feature-flags.md) — team-specific examples
95+
- [Compliance OFAC gating](../compliance.md#feature-flag) — real selector using this pattern
96+
- [`.cursor/rules/version-gated-feature-flags.mdc`](../../.cursor/rules/version-gated-feature-flags.mdc) — Cursor agent rule
97+
- External: [MetaMask remote feature flags](https://github.com/MetaMask/contributor-docs/blob/main/docs/remote-feature-flags.md)

0 commit comments

Comments
 (0)