MetaMask / MetaMask/metamask-mobile

[Bug]: Version-style remote flags resolve to undefined in validatedVersionGatedFeatureFlag unless value repeats minimumVersion (VersionGatedFeatureFlag shape)

Open
#27,765 2 comments 0 reactions 0 assignees View on GitHub
INVALID-ISSUE-TEMPLATE type-bug
Dominant language
TypeScript
Stars
3k
Forks
1.7k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

## TL;DR

**Any** remote flag whose **resolved** value comes from the **version-based** (and version + threshold) Client Config / LD shapes documented in [Remote Feature Flags](https://github.com/MetaMask/contributor-docs/blob/main/docs/remote-feature-flags.md) will often look like `{ name, value: { enabled: true } }` or `{ enabled: true }` **without** `minimumVersion`. `validatedVersionGatedFeatureFlag` only accepts a [`VersionGatedFeatureFlag`](https://github.com/MetaMask/metamask-mobile/blob/main/app/util/remoteFeatureFlag/index.ts) (`enabled` **and** `minimumVersion`). Anything else fails `unwrapVersionGatedFeatureFlag` → **`undefined`** → callers using `?? false` treat the feature as off. Version targeting is **already** applied by the `versions` map; requiring `minimumVersion` inside `value` duplicates that contract and breaks valid resolved payloads.

## Describe the bug

**Version-style** feature flags (see [“Object flag with version-based scope”](https://github.com/MetaMask/contributor-docs/blob/main/docs/remote-feature-flags.md) and [“Composing version-based scope with threshold scope”](https://github.com/MetaMask/contributor-docs/blob/main/docs/remote-feature-flags.md) in the contributor docs) resolve per app version (and optionally threshold) to a **winning variant**. The docs show inner `value` objects like `{ "enabled": true }` **without** a `minimumVersion` field—versioning is expressed by the **`versions`** structure, not by a field on `value`.

In the mobile app, `validatedVersionGatedFeatureFlag` assumes every acceptable payload unwraps to a **`VersionGatedFeatureFlag`**: `enabled` plus **`minimumVersion`** as a string. The type guard `isVersionGatedFeatureFlag` rejects `{ enabled: true }` if `minimumVersion` is missing, so `unwrapVersionGatedFeatureFlag` returns **`undefined`**, and **`validatedVersionGatedFeatureFlag` returns `undefined`**.

So for the **documented** version/threshold style, the resolved controller state is valid per product/docs, but **invalid** per this helper unless teams **repeat** a client-side minimum version inside `value` (redundant with version-based resolution).

This is **not specific to `homepageSectionsV1`**; that flag is one example where a selector uses `validatedVersionGatedFeatureFlag` and can incorrectly fall back to disabled.

## Expected behavior

If the controller stores a resolved variant that matches the documented version/threshold shapes (e.g. `value: { enabled: true }`), feature-flag consumers should be able to treat **`enabled`** as authoritative for “on/off” after resolution, **or** the client should document and implement a single normalization path so version-style flags are not rejected unless malformed.

## Actual behavior

`validatedVersionGatedFeatureFlag` returns **`undefined`** for many valid resolved values from version-style flags, so selectors that do `validatedVersionGatedFeatureFlag(remoteFlag) ?? false` behave as **always off** unless `value` also includes `minimumVersion` (i.e. effectively requires a **VersionGatedFeatureFlag** shape on top of version-based LD structure).

## Steps to reproduce (example)

1. Configure a flag using **version-based** (and optionally **threshold**) scope as in the [contributor docs](https://github.com/MetaMask/contributor-docs/blob/main/docs/remote-feature-flags.md), with `value: { "enabled": true }` and **no** `minimumVersion` in `value`.
2. Run an app version that resolves to the enabled variant; confirm controller state shows the wrapped shape with `value: { enabled: true }`.
3. Pass that state through `validatedVersionGatedFeatureFlag` and observe **`undefined`**.

## Reference: documented version + threshold example (from contributor docs)

The docs explicitly show composition of **versions** + **threshold** with `value: { "enabled": true }` only—no `minimumVersion` on `value`:

See: [Remote Feature Flags — “Composing version-based scope with threshold scope”](https://github.com/MetaMask/contributor-docs/blob/main/docs/remote-feature-flags.md).

### Example resolved controller state (not homepage-specific)

```json
{
"name": "Sections Enabled",
"value": {
"enabled": true
}
}
```

### Example LaunchDarkly / version map (illustrative)

```json
{
"versions": {
"0.0.0": [
{
"name": "Sections Disabled",
"scope": { "type": "threshold", "value": 1 },
"value": { "enabled": false }
}
],
"7.70.0": [
{
"name": "Sections Enabled",
"scope": { "type": "threshold", "value": 0.99 },
"value": { "enabled": true }
},
{
"name": "Sections Disabled",
"scope": { "type": "threshold", "value": 1 },
"value": { "enabled": false }
}
]
}
}
```

## Additional context

- **Docs:** [MetaMask contributor-docs — Remote Feature Flags](https://github.com/MetaMask/contributor-docs/blob/main/docs/remote-feature-flags.md)
- **Mobile helper:** `app/util/remoteFeatureFlag/index.ts` (`isVersionGatedFeatureFlag`, `unwrapVersionGatedFeatureFlag`, `validatedVersionGatedFeatureFlag`)
- **Example selector:** `app/selectors/featureFlagController/homepage/index.ts` (`selectHomepageSectionsV1Enabled`) — illustrative only; other selectors using the same helper may share the issue.

Relevant code (click to expand)

**Type guard** (requires `minimumVersion` on the unwrapped object): `app/util/remoteFeatureFlag/index.ts` — `isVersionGatedFeatureFlag` (lines 17–28)

**Unwrap** (fails when inner `value` is only `{ enabled: true }`): `app/util/remoteFeatureFlag/index.ts` — `unwrapVersionGatedFeatureFlag` (lines 46–66)

**Validation**:

`app/util/remoteFeatureFlag/index.ts` — `validatedVersionGatedFeatureFlag` (lines 68–90)

**Example selector** (illustrative): `app/selectors/featureFlagController/homepage/index.ts` — `selectHomepageSectionsV1Enabled` (lines 21–33)

Contributor guide

Open the contributing guide

Research direction

Start in app/util/remoteFeatureFlag/index.ts, reading isVersionGatedFeatureFlag, unwrapVersionGatedFeatureFlag, and validatedVersionGatedFeatureFlag, then inspect app/selectors/featureFlagController/homepage/index.ts and its selector usage. Reproduce the documented resolved value without minimumVersion and add coverage for the chosen normalization behavior; done means valid version-style flags are no longer returned as undefined while malformed values remain rejected.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
mobile-dev
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.