Skip to content

Commit c9895e7

Browse files
committed
feat: Add react-native-feature-flags plugin (SDK 54+)
New config plugin to allow override customisation of `ReactNativeFeatureFlags` on iOS and Android.
1 parent 639473d commit c9895e7

File tree

16 files changed

+680
-61
lines changed

16 files changed

+680
-61
lines changed

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ body:
3636
- '@config-plugins/react-native-branch'
3737
- '@config-plugins/react-native-callkeep'
3838
- '@config-plugins/react-native-dynamic-app-icon'
39+
- '@config-plugins/react-native-feature-flags'
3940
- '@config-plugins/react-native-pdf'
4041
- '@config-plugins/react-native-siri-shortcut'
4142
- '@config-plugins/react-native-webrtc'

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
ios-stickers,
3939
react-native-blob-util,
4040
react-native-branch,
41+
react-native-feature-flags,
4142
react-native-siri-shortcut,
4243
react-native-pdf,
4344
]

fixtures/AppDelegate.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public class AppDelegate: ExpoAppDelegate {
1919

2020
reactNativeDelegate = delegate
2121
reactNativeFactory = factory
22-
bindReactNativeFactory(factory)
2322

2423
#if os(iOS) || os(tvOS)
2524
window = UIWindow(frame: UIScreen.main.bounds)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// @generated by expo-module-scripts
2+
module.exports = require('expo-module-scripts/eslintrc.base.js');
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# @config-plugins/react-native-feature-flags
2+
3+
Expo Config Plugin to override native feature flags in React Native Core ([`ReactNativeFeatureFlags`](https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js)).
4+
5+
> **⚠️ Advanced users only**: `ReactNativeFeatureFlags` controls internal React Native behavior and experimental features that may affect the stability of your app in production. Only override flags if you understand their purpose and have tested the impact.
6+
7+
## Expo installation
8+
9+
> This package cannot be used in the Expo Go app because [it requires custom native code](https://docs.expo.io/workflow/customizing/).
10+
11+
```sh
12+
npx expo install @config-plugins/react-native-feature-flags
13+
```
14+
15+
After installing this package, add the [config plugin](https://docs.expo.io/guides/config-plugins/) to the [`plugins`](https://docs.expo.io/versions/latest/config/app/#plugins) array of your `app.json` or `app.config.js`:
16+
17+
```json
18+
{
19+
"plugins": [
20+
[
21+
"@config-plugins/react-native-feature-flags",
22+
{
23+
"nativeFlagOverrides": {
24+
"fuseboxFrameRecordingEnabled": true
25+
}
26+
}
27+
]
28+
]
29+
}
30+
```
31+
32+
## Configuration
33+
34+
| Option | Type | Default | Description |
35+
|---|---|---|---|
36+
| `nativeFlagOverrides` | `Record<string, boolean>` | *(required)* | One or more flag name/value pairs to override. Flag names must match method names on the base provider class (see below). |
37+
38+
The override runs before React Native initializes, so flags take effect from the very first render. If a flag name doesn't exist in the installed React Native version, the native build will fail with a compile error — remove the unrecognized flag to fix.
39+
40+
### Base classes
41+
42+
Your overrides extend from the following base classes, which provide the complete set of flag defaults for your app. This replaces any values that would otherwise be set by the `releaseLevel` passed to `ReactNativeFactory`:
43+
44+
- **iOS**: [`ReactNativeFeatureFlagsOverridesOSSStable`](https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSStable.h)
45+
- **Android**: [`ReactNativeNewArchitectureFeatureFlagsDefaults`](https://github.com/facebook/react-native/blob/main/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeNewArchitectureFeatureFlagsDefaults.kt)
46+
47+
Next, rebuild your app as described in the ["Adding custom native code"](https://docs.expo.io/workflow/customizing/) guide.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require("./build/withReactNativeFeatureFlags");
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('expo-module-scripts/jest-preset-plugin');
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "@config-plugins/react-native-feature-flags",
3+
"version": "1.0.0",
4+
"description": "Config plugin to override feature flags in React Native",
5+
"main": "build/withReactNativeFeatureFlags.js",
6+
"types": "build/withReactNativeFeatureFlags.d.ts",
7+
"sideEffects": false,
8+
"license": "MIT",
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/expo/config-plugins.git",
12+
"directory": "packages/react-native-feature-flags"
13+
},
14+
"scripts": {
15+
"build": "expo-module build",
16+
"clean": "expo-module clean",
17+
"lint": "expo-module lint",
18+
"test": "expo-module test",
19+
"prepare": "expo-module prepare",
20+
"prepublishOnly": "expo-module prepublishOnly",
21+
"expo-module": "expo-module"
22+
},
23+
"keywords": [
24+
"react",
25+
"expo",
26+
"config-plugins",
27+
"prebuild"
28+
],
29+
"dependencies": {
30+
"glob": "^10.4.2"
31+
},
32+
"devDependencies": {
33+
"@expo/config-plugins": "^54.0.1"
34+
},
35+
"peerDependencies": {
36+
"expo": "^54"
37+
},
38+
"upstreamPackage": "react-native"
39+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`withReactNativeFeatureFlagsAndroid adds imports and override to MainApplication.kt 1`] = `
4+
"package com.example.app
5+
6+
// @generated begin rn-feature-flags-import - expo prebuild (DO NOT MODIFY) sync-4385c6c195d7d2f7db2db0d13b4de8de5f4ace79
7+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
8+
import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlagsDefaults
9+
// @generated end rn-feature-flags-import
10+
import android.app.Application
11+
import com.facebook.react.ReactApplication
12+
13+
class MainApplication : Application(), ReactApplication {
14+
override fun onCreate() {
15+
super.onCreate()
16+
loadReactNative(this)
17+
// @generated begin rn-feature-flags-override - expo prebuild (DO NOT MODIFY) sync-9d15e9381c9936fc6560201bc44e595b09eac8fb
18+
ReactNativeFeatureFlags.dangerouslyForceOverride(object : ReactNativeNewArchitectureFeatureFlagsDefaults() {
19+
override fun fuseboxFrameRecordingEnabled(): Boolean = true
20+
override fun enableMicrotasks(): Boolean = false
21+
})
22+
// @generated end rn-feature-flags-override
23+
}
24+
}
25+
"
26+
`;
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`withReactNativeFeatureFlagsIOS generates RNFeatureFlagsOverride.h 1`] = `
4+
"// This file is generated by @config-plugins/react-native-feature-flags.
5+
// Do not edit manually.
6+
7+
#ifndef RNFeatureFlagsOverride_h
8+
#define RNFeatureFlagsOverride_h
9+
10+
#ifdef __cplusplus
11+
extern "C" {
12+
#endif
13+
14+
void RNFeatureFlagsOverride_apply(void);
15+
16+
#ifdef __cplusplus
17+
}
18+
#endif
19+
20+
#endif
21+
"
22+
`;
23+
24+
exports[`withReactNativeFeatureFlagsIOS generates RNFeatureFlagsOverride.mm 1`] = `
25+
"// This file is generated by @config-plugins/react-native-feature-flags.
26+
// Do not edit manually.
27+
28+
#import <react/featureflags/ReactNativeFeatureFlags.h>
29+
#import <react/featureflags/ReactNativeFeatureFlagsOverridesOSSStable.h>
30+
31+
class RNFeatureFlagsOverrideProvider : public facebook::react::ReactNativeFeatureFlagsOverridesOSSStable {
32+
public:
33+
bool fuseboxFrameRecordingEnabled() override { return true; }
34+
bool enableMicrotasks() override { return false; }
35+
};
36+
37+
extern "C" void RNFeatureFlagsOverride_apply() {
38+
facebook::react::ReactNativeFeatureFlags::dangerouslyForceOverride(
39+
std::make_unique<RNFeatureFlagsOverrideProvider>());
40+
}
41+
"
42+
`;
43+
44+
exports[`withReactNativeFeatureFlagsIOS inserts override call into Swift AppDelegate 1`] = `
45+
"import Expo
46+
import React
47+
import ReactAppDependencyProvider
48+
49+
@UIApplicationMain
50+
public class AppDelegate: ExpoAppDelegate {
51+
var window: UIWindow?
52+
53+
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
54+
var reactNativeFactory: RCTReactNativeFactory?
55+
56+
public override func application(
57+
_ application: UIApplication,
58+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
59+
) -> Bool {
60+
let delegate = ReactNativeDelegate()
61+
let factory = ExpoReactNativeFactory(delegate: delegate)
62+
delegate.dependencyProvider = RCTAppDependencyProvider()
63+
64+
reactNativeDelegate = delegate
65+
reactNativeFactory = factory
66+
// @generated begin rn-feature-flags-override - expo prebuild (DO NOT MODIFY) sync-8c8ed3ba25ac045452a092474dc8e34a460529c1
67+
RNFeatureFlagsOverride_apply()
68+
// @generated end rn-feature-flags-override
69+
70+
#if os(iOS) || os(tvOS)
71+
window = UIWindow(frame: UIScreen.main.bounds)
72+
factory.startReactNative(
73+
withModuleName: "main",
74+
in: window,
75+
launchOptions: launchOptions)
76+
#endif
77+
78+
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
79+
}
80+
81+
// Linking API
82+
public override func application(
83+
_ app: UIApplication,
84+
open url: URL,
85+
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
86+
) -> Bool {
87+
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
88+
}
89+
90+
// Universal Links
91+
public override func application(
92+
_ application: UIApplication,
93+
continue userActivity: NSUserActivity,
94+
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
95+
) -> Bool {
96+
let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
97+
return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result
98+
}
99+
}
100+
101+
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
102+
// Extension point for config-plugins
103+
104+
override func sourceURL(for bridge: RCTBridge) -> URL? {
105+
// needed to return the correct URL for expo-dev-client.
106+
bridge.bundleURL ?? bundleURL()
107+
}
108+
109+
override func bundleURL() -> URL? {
110+
#if DEBUG
111+
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
112+
#else
113+
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
114+
#endif
115+
}
116+
}
117+
"
118+
`;

0 commit comments

Comments
 (0)