Framework for controlling Perps feature availability through LaunchDarkly with local fallback support. Supports version-gated rollouts and gradual feature releases.
Key Design Principles:
- LaunchDarkly is the single source of truth for feature enablement
- Version-gated flags use
validatedVersionGatedFeatureFlagfromapp/util/remoteFeatureFlag(see Version-gated feature flags) - Local environment variables provide development/testing fallback
- Graceful degradation when LaunchDarkly is unavailable
graph TD
LD[LaunchDarkly Remote] -->|JSON flag| RFC[RemoteFeatureFlagController]
RFC -->|stores in| Redux[Redux State]
Redux -->|read by| Selector[Feature Flag Selector]
Selector -->|boolean| Component[UI Component]
ENV[Environment Variable] -->|fallback| Selector
style LD fill:#e1f5ff
style RFC fill:#fff3e0
style Selector fill:#f3e5f5
style Component fill:#e8f5e9
Used for feature on/off toggles with version requirements.
Interface:
interface VersionGatedFeatureFlag {
enabled: boolean;
minimumVersion: string;
}Example LaunchDarkly JSON:
{
"enabled": true,
"minimumVersion": "7.60.0"
}Behavior:
enabled: true+ version >=minimumVersion= feature ONenabled: true+ version <minimumVersion= feature OFFenabled: false= feature OFF (regardless of version)- Invalid/missing flag = fallback to local environment variable
See Perps A/B Testing Framework for variant-based flags.
File: app/components/UI/Perps/selectors/featureFlags/index.ts
import {
validatedVersionGatedFeatureFlag,
type VersionGatedFeatureFlag,
} from '../../../../util/remoteFeatureFlag';
/**
* Selector for My Feature flag
* Controls visibility of My Feature in the UI
*
* @returns boolean - true if feature should be shown, false otherwise
*/
export const selectMyFeatureEnabledFlag = createSelector(
selectRemoteFeatureFlags,
(remoteFeatureFlags) => {
// Choose default behavior:
// - Use `=== 'true'` for disabled by default (must explicitly enable)
// - Use `!== 'false'` for enabled by default (must explicitly disable)
const localFlag = process.env.MM_PERPS_MY_FEATURE_ENABLED === 'true';
const remoteFlag =
remoteFeatureFlags?.perpsMyFeatureEnabled as unknown as VersionGatedFeatureFlag;
// Remote takes precedence, fallback to local
return validatedVersionGatedFeatureFlag(remoteFlag) ?? localFlag;
},
);Default Behavior Options:
| Pattern | Default | Use Case |
|---|---|---|
=== 'true' |
Disabled | New experimental features |
!== 'false' |
Enabled | Features that should be on unless explicitly disabled |
File: app/components/UI/Perps/mocks/remoteFeatureFlagMocks.ts
export const mockedPerpsFeatureFlagsEnabledState: Record<
string,
VersionGatedFeatureFlag
> = {
// ... existing flags ...
perpsMyFeatureEnabled: mockEnabledPerpsLDFlag,
};File: .js.env.example
export MM_PERPS_MY_FEATURE_ENABLED="true"import { useSelector } from 'react-redux';
import { selectMyFeatureEnabledFlag } from '../../selectors/featureFlags';
const MyComponent = () => {
const isMyFeatureEnabled = useSelector(selectMyFeatureEnabledFlag);
if (!isMyFeatureEnabled) {
return null; // or alternative UI
}
return <MyFeature />;
};File: app/components/UI/Perps/selectors/featureFlags/index.test.ts
Follow existing test patterns covering:
- Default behavior (when env var not set)
- Remote flag takes precedence over local
- Version gating validation
- Fallback to local when remote is invalid/unavailable
| Redux Property | LaunchDarkly Key | Env Variable | Default | Purpose |
|---|---|---|---|---|
perpsPerpTradingEnabled |
perps-perp-trading-enabled |
MM_PERPS_ENABLED |
false | Main Perps feature toggle |
perpsPerpTradingServiceInterruptionBannerEnabled |
perps-perp-trading-service-interruption-banner-enabled |
MM_PERPS_SERVICE_INTERRUPTION_BANNER_ENABLED |
false | Service disruption banner |
perpsPerpGtmOnboardingModalEnabled |
perps-perp-gtm-onboarding-modal-enabled |
MM_PERPS_GTM_MODAL_ENABLED |
false | GTM onboarding modal |
perpsOrderBookEnabled |
perps-order-book-enabled |
MM_PERPS_ORDER_BOOK_ENABLED |
false | Order Book feature |
perpsFeedbackEnabled |
perps-feedback-enabled |
MM_PERPS_FEEDBACK_ENABLED |
false | Feedback button on home |
perpsCompetitionBannerEnabled |
perps-competition-banner-enabled |
— | false | Competition promotion banner on perps home (remote only) |
perpsDefaultPayTokenWhenNoBalanceEnabled |
perps-default-pay-token-when-no-balance-enabled |
— | true | Default pay token when no perps balance + Add funds CTA on market details (remote only) |
vipProgramEnabled |
vip-program-enabled |
— | false | Gates VIP fee discount in perps (UI preview and order execution) |
| Redux Property | LaunchDarkly Key | Variants | Purpose |
|---|---|---|---|
perpsAbtestButtonColor |
perps-abtest-button-color |
control, monochrome |
Button color A/B test (TAT-1937) |
These flags are managed via FeatureFlagConfigurationService and control runtime configuration:
| Redux Property | LaunchDarkly Key | Env Variable | Purpose |
|---|---|---|---|
perpsHip3Enabled |
perps-hip3-enabled |
MM_PERPS_HIP3_ENABLED |
HIP-3 markets master switch |
perpsHip3AllowlistMarkets |
perps-hip3-allowlist-markets |
MM_PERPS_HIP3_ALLOWLIST_MARKETS |
HIP-3 market allowlist |
perpsHip3BlocklistMarkets |
perps-hip3-blocklist-markets |
MM_PERPS_HIP3_BLOCKLIST_MARKETS |
HIP-3 market blocklist |
perpsPayWithAnyTokenAllowlistAssets |
perps-pay-with-any-token-allowlist-assets |
MM_PERPS_PAY_WITH_ANY_TOKEN_ALLOWLIST_ASSETS |
Pay-with modal token allowlist (chainId.address, comma-separated; env overrides remote) |
perpsPerpTradingGeoBlockedCountriesV2 |
perps-perp-trading-geo-blocked-countries-v2 |
MM_PERPS_BLOCKED_REGIONS |
Geo-blocking regions list |
Note:
perpsPerpTradingGeoBlockedCountries(without V2) is deprecated. Use the V2 variant.
| Format | Example |
|---|---|
| LaunchDarkly (kebab-case) | perps-order-book-enabled |
| Redux state (camelCase) | perpsOrderBookEnabled |
{
"variations": [
{
"name": "Enabled",
"value": {
"enabled": true,
"minimumVersion": "7.60.0"
}
},
{
"name": "Disabled",
"value": {
"enabled": false,
"minimumVersion": "0.0.0"
}
}
],
"offVariation": 1,
"fallthrough": {
"variation": 0
}
}The minimumVersion field ensures features only activate on compatible app versions. Do not compare versions in selectors or components — use validatedVersionGatedFeatureFlag:
- Format: Semantic version string (e.g.
"7.60.0") - Evaluation:
validatedVersionGatedFeatureFlag(remoteFlag)returnstrueonly whenenabledis true and the native app version is>= minimumVersion - Use case: Prevent feature activation on older app versions that lack required code
See docs/readme/version-gated-feature-flags.md for the full pattern and non-standard flag shapes.
- Check Redux state: Verify flag exists in
RemoteFeatureFlagController.remoteFeatureFlags - Check version: Ensure app version meets
minimumVersionrequirement - Check selector: Verify selector is imported and used correctly
- Verify
minimumVersionformat: Must be valid semver string - Check native app version:
getVersion()fromreact-native-device-info(used insidevalidatedVersionGatedFeatureFlag) — notpackage.json - Confirm selector uses the helper:
validatedVersionGatedFeatureFlag(remoteFlag) ?? localFallback— not inlinecompare-versions
- Restart Metro bundler after changing
.js.env - Check override setting:
isRemoteFeatureFlagOverrideActivatedmust be true - Verify spelling: Environment variable names are case-sensitive
- Selectors:
app/components/UI/Perps/selectors/featureFlags/index.ts - Mocks:
app/components/UI/Perps/mocks/remoteFeatureFlagMocks.ts - Tests:
app/components/UI/Perps/selectors/featureFlags/index.test.ts - Version validation:
app/util/remoteFeatureFlag/index.ts(validatedVersionGatedFeatureFlag) - Version-gated flags guide:
docs/readme/version-gated-feature-flags.md - Controller init:
app/core/Engine/controllers/remote-feature-flag-controller-init.ts - Configuration service:
app/controllers/perps/services/FeatureFlagConfigurationService.ts