Repository: microsoft/react-native-test-app
Pull request #785 (2022-02-24 22:41:42)

feat: add option to enable Fabric (experimental)
### Description

Add an option to enable Fabric in test app.

- [x] [Enable Fabric on Android](https://reactnative.dev/docs/next/new-architecture-app-renderer-android)
- [x] [Enable Fabric on iOS](https://reactnative.dev/docs/next/new-architecture-app-renderer-ios)
- [x] Updated docs ([Android](https://github.com/microsoft/react-native-test-app/wiki/Android-Specifics#fabric), [iOS](https://github.com/microsoft/react-native-test-app/wiki/iOS-and-macOS-Specifics#fabric))

### Platforms affected

- [x] Android
- [x] iOS
- [ ] macOS
- [ ] Windows

### Test plan

We need react-native 0.68 to enable Fabric: `yarn set-react-version 0.68`

For each platform below, verify that "Fabric" is displayed where the version number is, and that Metro outputs `"fabric":true`:

```
 LOG  Running "Example" with {"fabric":true,"initialProps":null,"rootTag":1}
```

#### Android

1. Enable Fabric in `gradle.properties`:

   ```diff
   diff --git a/example/android/gradle.properties b/example/android/gradle.properties
   index 0254fc1..62ca61f 100644
   --- a/example/android/gradle.properties
   +++ b/example/android/gradle.properties
   @@ -31,7 +31,7 @@ android.enableJetifier=true
    #FLIPPER_VERSION=0.125.0
   
    # Enable Fabric at runtime.
   -#USE_FABRIC=1
   +USE_FABRIC=1
   
    # Uncomment the line below if building react-native from source
    #ANDROID_NDK_VERSION=21.4.7075529
   ```

2. Build as usual:
   ```
   yarn android
   ```

#### iOS

1. Enable Fabric in `Podfile`:

   ```diff
   diff --git a/example/ios/Podfile b/example/ios/Podfile
   index 5052695..c9fd573 100644
   --- a/example/ios/Podfile
   +++ b/example/ios/Podfile
   @@ -4,7 +4,7 @@ workspace 'Example.xcworkspace'

    use_flipper! false if ENV['DISABLE_FLIPPER']

   -use_test_app! :fabric_enabled => false, :hermes_enabled => false do |target|
   +use_test_app! :fabric_enabled => true, :hermes_enabled => false do |target|
      target.tests do
        pod 'Example-Tests', :path => '..'
      end
   ```

2. Build as usual:
   ```
   pod install --project-directory=ios
   yarn ios
   ```

### Screenshots

| Platform | Home | App  |
| :------- | :--: | :--: |
| Android  | ![Screenshot_20220226_102329](https://user-images.githubusercontent.com/4123478/155837767-843bb6c3-0c9e-41d6-96de-ce608b8c638e.png) | ![Screenshot_20220225_132440](https://user-images.githubusercontent.com/4123478/155714868-0ad53c81-328f-4a32-966d-da6fff8660bf.png) |
| iOS      | ![image](https://user-images.githubusercontent.com/4123478/155837673-5a93062e-5174-44fc-9169-642dabe8d183.png) | ![image](https://user-images.githubusercontent.com/4123478/155619297-2df97a0b-6038-4399-82b1-48ceb8c77576.png) |


Patch:
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 99b7349..00042eb 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -52,6 +52,7 @@ applyTestAppModule(project)
 project.ext.react = [
     appName      : getAppName(),
     applicationId: getApplicationId(),
+    enableFabric : isFabricEnabled(rootDir),
     enableFlipper: getFlipperVersion(rootDir),
     enableHermes : true,
 ]
@@ -85,6 +86,8 @@ android {
         versionCode reactTestApp.versionCode
         versionName reactTestApp.versionName
 
+        buildConfigField "boolean", "ReactTestApp_useFabric", project.ext.react.enableFabric.toString()
+
         def recommendedFlipperVersion = getFlipperRecommendedVersion(rootDir)
         buildConfigField "String",
                          "ReactTestApp_recommendedFlipperVersion",
@@ -134,6 +137,13 @@ android {
             debug.java.srcDirs += "src/flipper/java"
         }
 
+        // TODO: Remove this block when we drop support for 0.63.
+        if (project.ext.react.enableFabric) {
+            main.java.srcDirs += "src/fabric/java"
+        } else {
+            main.java.srcDirs += "src/no-fabric/java"
+        }
+
         // TODO: Remove this block when we drop support for 0.67.
         // https://github.com/facebook/react-native/commit/ce74aa4ed335d4c36ce722d47937b582045e05c4
         if (getReactNativeVersionNumber(rootDir) < 6800) {
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 7279324..285f516 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -7,5 +7,5 @@
     <string name="remember_last_component">Remember last opened component</string>
     <string name="content_description_show_dev_options">Show dev options</string>
     <string name="show_dev_options">Show dev options</string>
-    <string name="runtime_info">React Native v%1$d.%2$d.%3$d\n(%4$s)</string>
+    <string name="runtime_info">React Native v%1$d.%2$d.%3$d\n(%4$s%5$s)</string>
 </resources>
diff --git a/android/dependencies.gradle b/android/dependencies.gradle
index 31ea63c..dfe7cd2 100644
--- a/android/dependencies.gradle
+++ b/android/dependencies.gradle
@@ -41,6 +41,6 @@ ext {
         moshiKotlinCodegen          : "com.squareup.moshi:moshi-kotlin-codegen:1.12.0",
     ]
 
-    androidPluginVersion = "7.1.1"
+    androidPluginVersion = "7.1.2"
     kotlinVersion = versionOf(libraries.kotlinStdlibJdk7)
 }
diff --git a/example/App.js b/example/App.js
index 59c7f62..e57ccd8 100644
--- a/example/App.js
+++ b/example/App.js
@@ -18,7 +18,7 @@ import {
 } from "react-native/Libraries/NewAppScreen";
 
 const Section = ({ children, title }) => {
-  const isDarkMode = useColorScheme && useColorScheme() === "dark";
+  const isDarkMode = useColorScheme() === "dark";
   return (
     <View style={styles.sectionContainer}>
       <Text
@@ -46,7 +46,7 @@ const Section = ({ children, title }) => {
 };
 
 const App = () => {
-  const isDarkMode = useColorScheme && useColorScheme() === "dark";
+  const isDarkMode = useColorScheme() === "dark";
 
   const backgroundStyle = {
     backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
diff --git a/example/android/gradle.properties b/example/android/gradle.properties
index 771053d..7a3711f 100644
--- a/example/android/gradle.properties
+++ b/example/android/gradle.properties
@@ -3,6 +3,7 @@
 # IDE (e.g. Android Studio) users:
 # Gradle settings configured through the IDE *will override*
 # any settings specified in this file.
+
 # For more details on how to configure your build environment visit
 # http://www.gradle.org/docs/current/userguide/build_environment.html
 
@@ -30,5 +31,8 @@ android.enableJetifier=true
 # Native defaults to. To disable Flipper, set it to `false`.
 #FLIPPER_VERSION=0.125.0
 
+# Enable Fabric at runtime.
+#USE_FABRIC=1
+
 # Uncomment the line below if building react-native from source
 #ANDROID_NDK_VERSION=21.4.7075529
diff --git a/example/ios/Podfile b/example/ios/Podfile
index b6eae97..5052695 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -4,7 +4,7 @@ workspace 'Example.xcworkspace'
 
 use_flipper! false if ENV['DISABLE_FLIPPER']
 
-use_test_app! :hermes_enabled => false do |target|
+use_test_app! :fabric_enabled => false, :hermes_enabled => false do |target|
   target.tests do
     pod 'Example-Tests', :path => '..'
   end
diff --git a/example/react-native.config.js b/example/react-native.config.js
index e9f5be2..262d4b8 100644
--- a/example/react-native.config.js
+++ b/example/react-native.config.js
@@ -1,61 +1,24 @@
 const fs = require("fs");
 const path = require("path");
-
-const windowsProjectFile = path.join(
-  "node_modules",
-  ".generated",
-  "windows",
-  "ReactTestApp",
-  "ReactTestApp.vcxproj"
-);
+const {
+  androidManifestPath,
+  iosProjectPath,
+  windowsProjectPath,
+} = require("react-native-test-app");
 
 module.exports = {
   project: {
     android: {
       sourceDir: "android",
-      manifestPath: path.relative(
-        path.join(__dirname, "android"),
-        path.join(
-          path.dirname(require.resolve("react-native-test-app/package.json")),
-          "android",
-          "app",
-          "src",
-          "main",
-          "AndroidManifest.xml"
-        )
-      ),
+      manifestPath: androidManifestPath(path.join(__dirname, "android")),
     },
     ios: {
-      project: (() => {
-        const {
-          packageSatisfiesVersionRange,
-        } = require("react-native-test-app/scripts/configure");
-        if (
-          packageSatisfiesVersionRange(
-            "@react-native-community/cli-platform-ios",
-            "<5.0.2"
-          )
-        ) {
-          // Prior to @react-native-community/cli-platform-ios v5.0.0,
-          // `project` was only used to infer `sourceDir` and `podfile`.
-          return "ios/ReactTestApp-Dummy.xcodeproj";
-        }
-
-        // `sourceDir` and `podfile` detection was fixed in
-        // @react-native-community/cli-platform-ios v5.0.2 (see
-        // https://github.com/react-native-community/cli/pull/1444).
-        return "node_modules/.generated/ios/ReactTestApp.xcodeproj";
-      })(),
+      project: iosProjectPath("ios"),
     },
-    windows: fs.existsSync(windowsProjectFile) && {
+    windows: fs.existsSync("windows/Example.sln") && {
       sourceDir: "windows",
       solutionFile: "Example.sln",
-      project: {
-        projectFile: path.relative(
-          path.join(__dirname, "windows"),
-          windowsProjectFile
-        ),
-      },
+      project: windowsProjectPath(path.join(__dirname, "windows")),
     },
   },
 };
diff --git a/ios/use_react_native-0.68.rb b/ios/use_react_native-0.68.rb
new file mode 100644
index 0000000..561d41c
--- /dev/null
+++ b/ios/use_react_native-0.68.rb
@@ -0,0 +1,37 @@
+require 'open3'
+
+require_relative('pod_helpers')
+
+def include_react_native!(options)
+  fabric_enabled = options[:fabric_enabled]
+  react_native = options[:path]
+  flipper_versions = options[:rta_flipper_versions]
+  project_root = options[:rta_project_root]
+  target_platform = options[:rta_target_platform]
+
+  require_relative(File.join(project_root, react_native, 'scripts', 'react_native_pods'))
+
+  if fabric_enabled
+    Pod::UI.warn(
+      'As of writing, Fabric is still experimental and subject to change. ' \
+      'For more information, please see ' \
+      'https://reactnative.dev/docs/next/new-architecture-app-renderer-ios.'
+    )
+    ENV['RCT_NEW_ARCH_ENABLED'] = '1'
+  end
+
+  use_flipper!(flipper_versions) if target_platform == :ios && flipper_versions
+  use_react_native!(options)
+
+  # If we're using react-native@main, we'll also need to prepare
+  # `react-native-codegen`.
+  codegen = File.join(project_root, react_native, 'packages', 'react-native-codegen')
+  Open3.popen3('yarn', :chdir => codegen) if File.directory?(codegen)
+
+  lambda { |installer|
+    react_native_post_install(installer)
+    if defined?(__apply_Xcode_12_5_M1_post_install_workaround)
+      __apply_Xcode_12_5_M1_post_install_workaround(installer)
+    end
+  }
+end
diff --git a/package.json b/package.json
index 550f623..4ff45b4 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
     "/android/**/*.gradle",
     "/android/app/lint.xml",
     "/android/app/src/**/compat",
-    "/android/app/src/{flipper,main}",
+    "/android/app/src/{fabric,flipper,main,no-fabric}",
     "/android/support/src",
     "/common",
     "/example/.gitignore",
@@ -39,6 +39,7 @@
     "/windows/*.{js,props,sln}",
     "/windows/ReactTestApp"
   ],
+  "main": "scripts/configure.js",
   "bin": {
     "configure-test-app": "scripts/configure.js",
     "init-test-app": "scripts/init.js",
diff --git a/scripts/configure.js b/scripts/configure.js
index e44de04..bc55c16 100755
--- a/scripts/configure.js
+++ b/scripts/configure.js
@@ -213,6 +213,64 @@ function getPlatformPackage(packageName, targetVersion) {
   return { [packageName]: `^${v.major}.${v.minor}.0` };
 }
 
+/**
+ * @param {string} sourceDir
+ * @returns {string}
+ */
+function androidManifestPath(sourceDir) {
+  return path.relative(
+    sourceDir,
+    path.join(
+      path.dirname(require.resolve("../package.json")),
+      "android",
+      "app",
+      "src",
+      "main",
+      "AndroidManifest.xml"
+    )
+  );
+}
+
+/**
+ * @param {string} sourceDir
+ * @returns {string}
+ */
+function iosProjectPath(sourceDir) {
+  const useOldWorkaround = packageSatisfiesVersionRange(
+    "@react-native-community/cli-platform-ios",
+    "<5.0.2"
+  );
+  if (useOldWorkaround) {
+    // Prior to @react-native-community/cli-platform-ios v5.0.0, `project` was
+    // only used to infer `sourceDir` and `podfile`.
+    return path.join(sourceDir, "ReactTestApp-Dummy.xcodeproj");
+  }
+
+  // `sourceDir` and `podfile` detection was fixed in
+  // @react-native-community/cli-platform-ios v5.0.2 (see
+  // https://github.com/react-native-community/cli/pull/1444).
+  return "node_modules/.generated/ios/ReactTestApp.xcodeproj";
+}
+
+/**
+ * @param {string} sourceDir
+ * @returns {{ projectFile: string }}
+ */
+function windowsProjectPath(sourceDir) {
+  return {
+    projectFile: path.relative(
+      sourceDir,
+      path.join(
+        "node_modules",
+        ".generated",
+        "windows",
+        "ReactTestApp",
+        "ReactTestApp.vcxproj"
+      )
+    ),
+  };
+}
+
 /**
  * Returns the appropriate `react-native.config.js` for specified parameters.
  * @param {ConfigureParams} params
@@ -224,22 +282,12 @@ function reactNativeConfig({ name, testAppPath, platforms, flatten }) {
     switch (platforms[0]) {
       case "android":
         return join(
-          'const path = require("path");',
+          'const { androidManifestPath } = require("react-native-test-app");',
           "module.exports = {",
           "  project: {",
           "    android: {",
           '      sourceDir: ".",',
-          "      manifestPath: path.relative(",
-          "        __dirname,",
-          "        path.join(",
-          '          path.dirname(require.resolve("react-native-test-app/package.json")),',
-          '          "android",',
-          '          "app",',
-          '          "src",',
-          '          "main",',
-          '          "AndroidManifest.xml"',
-          "        )",
-          "      ),",
+          "      manifestPath: androidManifestPath(__dirname),",
           "    },",
           "  },",
           "};",
@@ -249,18 +297,11 @@ function reactNativeConfig({ name, testAppPath, platforms, flatten }) {
       case "macos":
       case "ios":
         return join(
-          "const {",
-          "  packageSatisfiesVersionRange,",
-          '} = require("react-native-test-app/scripts/configure");',
+          'const { iosProjectPath } = require("react-native-test-app");',
           "module.exports = {",
           "  project: {",
           "    ios: {",
-          "      project: packageSatisfiesVersionRange(",
-          '        "@react-native-community/cli-platform-ios",',
-          '        "<5.0.2"',
-          "      )",
-          '        ? "ReactTestApp-Dummy.xcodeproj"',
-          '        : "node_modules/.generated/ios/ReactTestApp.xcodeproj",',
+          '      project: iosProjectPath("."),',
           "    }",
           "  }",
           "};",
@@ -270,6 +311,7 @@ function reactNativeConfig({ name, testAppPath, platforms, flatten }) {
       case "windows":
         return join(
           'const path = require("path");',
+          'const { windowsProjectPath } = require("react-native-test-app");',
           "",
           'const sourceDir = "windows";',
           "module.exports = {",
@@ -277,18 +319,7 @@ function reactNativeConfig({ name, testAppPath, platforms, flatten }) {
           "    windows: {",
           "      sourceDir,",
           `      solutionFile: "${name}.sln",`,
-          "      project: {",
-          "        projectFile: path.relative(",
-          "          path.join(__dirname, sourceDir),",
-          "          path.join(",
-          '            "node_modules",',
-          '            ".generated",',
-          '            "windows",',
-          '            "ReactTestApp",',
-          '            "ReactTestApp.vcxproj"',
-          "          )",
-          "        ),",
-          "      },",
+          "      project: windowsProjectPath(path.join(__dirname, sourceDir)),",
           "    },",
           "  },",
           '  reactNativePath: "node_modules/react-native-windows",',
@@ -902,7 +933,7 @@ if (require.main === module) {
       const result = configure({
         name: typeof name === "string" && name ? name : getAppName(packagePath),
         packagePath,
-        testAppPath: path.resolve(__dirname, ".."),
+        testAppPath: path.dirname(require.resolve("../package.json")),
         targetVersion,
         platforms,
         flatten,
@@ -916,12 +947,14 @@ if (require.main === module) {
   ).argv;
 }
 
+exports.androidManifestPath = androidManifestPath;
 exports.configure = configure;
 exports.error = error;
 exports.gatherConfig = gatherConfig;
 exports.getAppName = getAppName;
 exports.getConfig = getConfig;
 exports.getPlatformPackage = getPlatformPackage;
+exports.iosProjectPath = iosProjectPath;
 exports.isDestructive = isDestructive;
 exports.isInstalled = isInstalled;
 exports.join = join;
@@ -934,4 +967,5 @@ exports.removeAllFiles = removeAllFiles;
 exports.sortByKeys = sortByKeys;
 exports.updatePackageManifest = updatePackageManifest;
 exports.warn = warn;
+exports.windowsProjectPath = windowsProjectPath;
 exports.writeAllFiles = writeAllFiles;
diff --git a/yarn.lock b/yarn.lock
index 45357b7..10418a9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2215,16 +2215,17 @@ __metadata:
   linkType: hard
 
 "@rnx-kit/eslint-plugin@npm:^0.2.0":
-  version: 0.2.10
-  resolution: "@rnx-kit/eslint-plugin@npm:0.2.10"
+  version: 0.2.11
+  resolution: "@rnx-kit/eslint-plugin@npm:0.2.11"
   dependencies:
     "@typescript-eslint/eslint-plugin": ^5.0.0
     "@typescript-eslint/parser": ^5.0.0
     enhanced-resolve: ^5.8.3
     eslint-plugin-react: ^7.26.0
+    eslint-plugin-react-hooks: ^4.3.0
   peerDependencies:
     eslint: ">=6.0.0"
-  checksum: 985bb70be1cdcc8d6a6347a299e118601cb6a4c69ec305dae196e5ccc85cbd72f34f38619162a0d065f9e910ee2eef43932c4b194c00efb6bf9ceffb02b7fceb
+  checksum: 7548bb3d4116a3a784ec973e3805b10813f537c9f68edd55da89ac7ee8d01a4a58e04cbcd9d92482396b701cfa5cb599b0c0c59ac51ade85c4e6504a6b341766
   languageName: node
   linkType: hard
 
@@ -2490,12 +2491,12 @@ __metadata:
   linkType: hard
 
 "@types/jest@npm:^27.0.0":
-  version: 27.4.0
-  resolution: "@types/jest@npm:27.4.0"
+  version: 27.4.1
+  resolution: "@types/jest@npm:27.4.1"
   dependencies:
-    jest-diff: ^27.0.0
+    jest-matcher-utils: ^27.0.0
     pretty-format: ^27.0.0
-  checksum: d2350267f954f9a2e4a15e5f02fbf19a77abfb9fd9e57a954de1fb0e9a0d3d5f8d3646ac7d9c42aeb4b4d828d2e70624ec149c85bb50a48634a54eed8429e1f8
+  checksum: 5184f3eef4832d01ee8f59bed15eec45ccc8e29c724a5e6ce37bf74396b37bdf04f557000f45ba4fc38ae6075cf9cfcce3d7a75abc981023c61ceb27230a93e4
   languageName: node
   linkType: hard
 
@@ -2538,9 +2539,9 @@ __metadata:
   linkType: hard
 
 "@types/node@npm:*, @types/node@npm:>=12, @types/node@npm:^16.0.0":
-  version: 16.11.25
-  resolution: "@types/node@npm:16.11.25"
-  checksum: 0b6e25a81364be89256ad1a36341e27b387e646d3186e270108a8bb7b6ecdfdf5ae037aa1c75a5117b8a7509c80093b75431cd5cfcfbc4d553b52e7db2ca272e
+  version: 16.11.26
+  resolution: "@types/node@npm:16.11.26"
+  checksum: 57757caaba3f0d95de82198cb276a1002c49b710108c932a1d02d7c91ff2fa57cfe2dd19fde60853b6dd90b0964b3cf35557981d2628e20aed6a909057aedfe6
   languageName: node
   linkType: hard
 
@@ -5176,6 +5177,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"eslint-plugin-react-hooks@npm:^4.3.0":
+  version: 4.3.0
+  resolution: "eslint-plugin-react-hooks@npm:4.3.0"
+  peerDependencies:
+    eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
+  checksum: 0ba1566ba0780bbc75a5921f49188edf232db2085ab32c8d3889592f0db9d6fadc97fcf639775e0101dec6b5409ca3c803ec44213b90c8bacaf0bdf921871c2e
+  languageName: node
+  linkType: hard
+
 "eslint-plugin-react@npm:^7.26.0":
   version: 7.28.0
   resolution: "eslint-plugin-react@npm:7.28.0"
@@ -7245,7 +7255,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"jest-diff@npm:^27.0.0, jest-diff@npm:^27.5.1":
+"jest-diff@npm:^27.5.1":
   version: 27.5.1
   resolution: "jest-diff@npm:27.5.1"
   dependencies:
@@ -7406,7 +7416,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"jest-matcher-utils@npm:^27.5.1":
+"jest-matcher-utils@npm:^27.0.0, jest-matcher-utils@npm:^27.5.1":
   version: 27.5.1
   resolution: "jest-matcher-utils@npm:27.5.1"
   dependencies:


Test patch:
diff --git a/android/app/src/fabric/java/com/microsoft/reacttestapp/fabric/FabricJSIModulePackage.kt b/android/app/src/fabric/java/com/microsoft/reacttestapp/fabric/FabricJSIModulePackage.kt
new file mode 100644
index 0000000..1c8139c
--- /dev/null
+++ b/android/app/src/fabric/java/com/microsoft/reacttestapp/fabric/FabricJSIModulePackage.kt
@@ -0,0 +1,50 @@
+package com.microsoft.reacttestapp.fabric
+
+import com.facebook.react.ReactInstanceManager
+import com.facebook.react.ReactNativeHost
+import com.facebook.react.bridge.JSIModulePackage
+import com.facebook.react.bridge.JSIModuleProvider
+import com.facebook.react.bridge.JSIModuleSpec
+import com.facebook.react.bridge.JSIModuleType
+import com.facebook.react.bridge.JavaScriptContextHolder
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.UIManager
+import com.facebook.react.fabric.ComponentFactory
+import com.facebook.react.fabric.CoreComponentsRegistry
+import com.facebook.react.fabric.EmptyReactNativeConfig
+import com.facebook.react.fabric.FabricJSIModuleProvider
+import com.facebook.react.uimanager.ViewManagerRegistry
+import java.lang.ref.WeakReference
+
+class FabricJSIModulePackage(reactNativeHost: ReactNativeHost) : JSIModulePackage {
+
+    private val reactInstanceManager: ReactInstanceManager?
+        get() = reactNativeHost.get()?.reactInstanceManager
+
+    private val reactNativeHost: WeakReference<ReactNativeHost> = WeakReference(reactNativeHost)
+
+    override fun getJSIModules(
+        reactApplicationContext: ReactApplicationContext,
+        jsContext: JavaScriptContextHolder?
+    ): ArrayList<JSIModuleSpec<*>> {
+        return arrayListOf(object : JSIModuleSpec<UIManager?> {
+            override fun getJSIModuleType(): JSIModuleType = JSIModuleType.UIManager
+
+            override fun getJSIModuleProvider(): JSIModuleProvider<UIManager?> {
+                val componentFactory = ComponentFactory()
+                CoreComponentsRegistry.register(componentFactory)
+
+                return FabricJSIModuleProvider(
+                    reactApplicationContext,
+                    componentFactory,
+                    EmptyReactNativeConfig(),
+                    ViewManagerRegistry(
+                        reactInstanceManager?.getOrCreateViewManagers(
+                            reactApplicationContext
+                        )
+                    )
+                )
+            }
+        })
+    }
+}
diff --git a/android/app/src/main/java/com/microsoft/reacttestapp/MainActivity.kt b/android/app/src/main/java/com/microsoft/reacttestapp/MainActivity.kt
index 90813ae..ad771a2 100644
--- a/android/app/src/main/java/com/microsoft/reacttestapp/MainActivity.kt
+++ b/android/app/src/main/java/com/microsoft/reacttestapp/MainActivity.kt
@@ -120,7 +120,8 @@ class MainActivity : ReactActivity() {
                 ReactNativeVersion.VERSION["major"] as Int,
                 ReactNativeVersion.VERSION["minor"] as Int,
                 ReactNativeVersion.VERSION["patch"] as Int,
-                reactInstanceManager.jsExecutorName
+                reactInstanceManager.jsExecutorName,
+                if (BuildConfig.ReactTestApp_useFabric) "+Fabric" else ""
             )
         }
     }
diff --git a/android/app/src/main/java/com/microsoft/reacttestapp/component/ComponentActivity.kt b/android/app/src/main/java/com/microsoft/reacttestapp/component/ComponentActivity.kt
index 235e9f8..604605a 100644
--- a/android/app/src/main/java/com/microsoft/reacttestapp/component/ComponentActivity.kt
+++ b/android/app/src/main/java/com/microsoft/reacttestapp/component/ComponentActivity.kt
@@ -7,6 +7,8 @@ import android.view.MenuItem
 import androidx.fragment.app.Fragment
 import com.facebook.react.ReactActivity
 import com.facebook.react.ReactActivityDelegate
+import com.facebook.react.ReactRootView
+import com.microsoft.reacttestapp.BuildConfig
 
 class ComponentActivity : ReactActivity() {
 
@@ -17,6 +19,12 @@ class ComponentActivity : ReactActivity() {
         override fun getLaunchOptions(): Bundle? {
             return intent.extras?.getBundle(COMPONENT_INITIAL_PROPERTIES)
         }
+
+        override fun createRootView(): ReactRootView {
+            val rootView = super.createRootView()
+            rootView.setIsFabric(BuildConfig.ReactTestApp_useFabric)
+            return rootView
+        }
     }
 
     companion object {
diff --git a/android/app/src/main/java/com/microsoft/reacttestapp/component/ComponentBottomSheetDialogFragment.kt b/android/app/src/main/java/com/microsoft/reacttestapp/component/ComponentBottomSheetDialogFragment.kt
index 215f385..3d19366 100644
--- a/android/app/src/main/java/com/microsoft/reacttestapp/component/ComponentBottomSheetDialogFragment.kt
+++ b/android/app/src/main/java/com/microsoft/reacttestapp/component/ComponentBottomSheetDialogFragment.kt
@@ -7,6 +7,7 @@ import android.view.ViewGroup
 import com.facebook.react.ReactApplication
 import com.facebook.react.ReactRootView
 import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.microsoft.reacttestapp.BuildConfig
 
 class ComponentBottomSheetDialogFragment : BottomSheetDialogFragment() {
 
@@ -36,6 +37,7 @@ class ComponentBottomSheetDialogFragment : BottomSheetDialogFragment() {
     ): View {
         val reactApplication = requireActivity().application as ReactApplication
         return ReactRootView(context).apply {
+            setIsFabric(BuildConfig.ReactTestApp_useFabric)
             startReactApplication(
                 reactApplication.reactNativeHost.reactInstanceManager,
                 requireArguments().getString(NAME),
diff --git a/android/app/src/main/java/com/microsoft/reacttestapp/react/TestAppReactNativeHost.kt b/android/app/src/main/java/com/microsoft/reacttestapp/react/TestAppReactNativeHost.kt
index 3858643..d1e15d3 100644
--- a/android/app/src/main/java/com/microsoft/reacttestapp/react/TestAppReactNativeHost.kt
+++ b/android/app/src/main/java/com/microsoft/reacttestapp/react/TestAppReactNativeHost.kt
@@ -8,6 +8,7 @@ import com.facebook.react.PackageList
 import com.facebook.react.ReactInstanceManager
 import com.facebook.react.ReactNativeHost
 import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.JSIModulePackage
 import com.facebook.react.bridge.ReactContext
 import com.facebook.react.bridge.ReactMarker
 import com.facebook.react.bridge.ReactMarkerConstants
@@ -21,6 +22,7 @@ import com.facebook.soloader.SoLoader
 import com.microsoft.reacttestapp.BuildConfig
 import com.microsoft.reacttestapp.R
 import com.microsoft.reacttestapp.compat.ReactInstanceEventListener
+import com.microsoft.reacttestapp.fabric.FabricJSIModulePackage
 import java.util.concurrent.CountDownLatch
 
 sealed class BundleSource {
@@ -149,6 +151,14 @@ class TestAppReactNativeHost(
         return reactInstanceManager
     }
 
+    override fun getJSIModulePackage(): JSIModulePackage? {
+        return if (BuildConfig.ReactTestApp_useFabric) {
+            FabricJSIModulePackage(this)
+        } else {
+            null
+        }
+    }
+
     override fun getJSMainModuleName() = "index"
 
     override fun getBundleAssetName() =
diff --git a/android/app/src/no-fabric/java/com/microsoft/reacttestapp/fabric/FabricJSIModulePackage.kt b/android/app/src/no-fabric/java/com/microsoft/reacttestapp/fabric/FabricJSIModulePackage.kt
new file mode 100644
index 0000000..f7ae2a5
--- /dev/null
+++ b/android/app/src/no-fabric/java/com/microsoft/reacttestapp/fabric/FabricJSIModulePackage.kt
@@ -0,0 +1,18 @@
+package com.microsoft.reacttestapp.fabric
+
+import com.facebook.react.ReactNativeHost
+import com.facebook.react.bridge.JSIModule
+import com.facebook.react.bridge.JSIModulePackage
+import com.facebook.react.bridge.JSIModuleSpec
+import com.facebook.react.bridge.JavaScriptContextHolder
+import com.facebook.react.bridge.ReactApplicationContext
+
+@Suppress("UNUSED_PARAMETER")
+class FabricJSIModulePackage(reactNativeHost: ReactNativeHost) : JSIModulePackage {
+    override fun getJSIModules(
+        reactApplicationContext: ReactApplicationContext,
+        jsContext: JavaScriptContextHolder?
+    ): ArrayList<JSIModuleSpec<JSIModule>> {
+        return arrayListOf()
+    }
+}
diff --git a/android/test-app-util.gradle b/android/test-app-util.gradle
index c4050fa..bd2b1af 100644
--- a/android/test-app-util.gradle
+++ b/android/test-app-util.gradle
@@ -3,7 +3,7 @@ import groovy.json.JsonSlurper
 import java.nio.file.Paths
 
 ext.buildReactNativeFromSource = { baseDir ->
-    def reactNativePath = findNodeModulesPath(baseDir, 'react-native')
+    def reactNativePath = findNodeModulesPath(baseDir, "react-native")
     return !file("${reactNativePath}/android").exists()
 }
 
@@ -51,7 +51,7 @@ ext.findNodeModulesPath = { baseDir, packageName ->
     // Node's module resolution algorithm searches up to the root directory,
     // after which the base path will be null
     while (basePath) {
-        def candidatePath = Paths.get(basePath.toString(), 'node_modules', packageName)
+        def candidatePath = Paths.get(basePath.toString(), "node_modules", packageName)
         if (candidatePath.toFile().exists()) {
             return candidatePath.toString()
         }
@@ -63,16 +63,16 @@ ext.findNodeModulesPath = { baseDir, packageName ->
 }
 
 ext.getAppName = { baseDir ->
-    def manifestFile = findFile('app.json')
+    def manifestFile = findFile("app.json")
     if (manifestFile != null) {
         def manifest = new JsonSlurper().parseText(manifestFile.text)
 
-        def displayName = manifest['displayName']
+        def displayName = manifest["displayName"]
         if (displayName instanceof String) {
             return displayName
         }
 
-        def name = manifest['name']
+        def name = manifest["name"]
         if (name instanceof String) {
             return name
         }
@@ -82,26 +82,26 @@ ext.getAppName = { baseDir ->
 }
 
 ext.getApplicationId = { baseDir ->
-    def manifestFile = findFile('app.json')
+    def manifestFile = findFile("app.json")
     if (manifestFile != null) {
         def manifest = new JsonSlurper().parseText(manifestFile.text)
-        def config = manifest['android']
-        if (config instanceof Object && config.containsKey('package')) {
-            return config['package']
+        def config = manifest["android"]
+        if (config instanceof Object && config.containsKey("package")) {
+            return config["package"]
         }
     }
 
-    return 'com.microsoft.reacttestapp'
+    return "com.microsoft.reacttestapp"
 }
 
 ext.getFlipperRecommendedVersion = { baseDir ->
-    def reactNativePath = findNodeModulesPath(baseDir, 'react-native')
+    def reactNativePath = findNodeModulesPath(baseDir, "react-native")
     def props = new Properties()
     file("${reactNativePath}/template/android/gradle.properties").withInputStream {
         props.load(it)
     }
 
-    return props.getProperty('FLIPPER_VERSION')
+    return props.getProperty("FLIPPER_VERSION")
 }
 
 ext.getFlipperVersion = { baseDir ->
@@ -112,9 +112,9 @@ ext.getFlipperVersion = { baseDir ->
     }
 
     // Prefer user specified Flipper version
-    if (project.hasProperty('FLIPPER_VERSION')) {
-        def flipperVersion = project.getProperty('FLIPPER_VERSION')
-        return flipperVersion == 'false' ? null : flipperVersion
+    if (project.hasProperty("FLIPPER_VERSION")) {
+        def flipperVersion = project.getProperty("FLIPPER_VERSION")
+        return flipperVersion == "false" ? null : flipperVersion
     }
 
     // Use the recommended Flipper version
@@ -167,3 +167,9 @@ ext.getSigningConfigs = { baseDir ->
 
     return definedConfigs
 }
+
+ext.isFabricEnabled = { baseDir ->
+    return project.hasProperty("USE_FABRIC") &&
+        project.getProperty("USE_FABRIC") == "1" &&
+        getReactNativeVersionNumber(baseDir) >= 6800
+}
diff --git a/ios/ReactTestApp.xcodeproj/project.pbxproj b/ios/ReactTestApp.xcodeproj/project.pbxproj
index ca66ffc..5bc7425 100644
--- a/ios/ReactTestApp.xcodeproj/project.pbxproj
+++ b/ios/ReactTestApp.xcodeproj/project.pbxproj
@@ -9,6 +9,7 @@
 /* Begin PBXBuildFile section */
 		1914199A234B2DD800D856AE /* RCTDevSupport+UIScene.m in Sources */ = {isa = PBXBuildFile; fileRef = 19141999234B2DD800D856AE /* RCTDevSupport+UIScene.m */; };
 		192DD201240FCAF5004E9CEB /* Manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192DD200240FCAF5004E9CEB /* Manifest.swift */; };
+		1963A06227C82E730013D276 /* React+Fabric.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1963A06127C82E730013D276 /* React+Fabric.mm */; };
 		196C22622490CB7600449D3C /* React+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 196C22602490CB7600449D3C /* React+Compatibility.m */; };
 		196C7215232F1788006556ED /* ReactInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196C7214232F1788006556ED /* ReactInstance.swift */; };
 		196C724123319A85006556ED /* QRCodeScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196C724023319A85006556ED /* QRCodeScannerViewController.swift */; };
@@ -47,6 +48,8 @@
 		192F052624AD3CC500A48456 /* ReactTestApp.release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.release.xcconfig; sourceTree = "<group>"; };
 		192F052724AD3CC500A48456 /* ReactTestApp.common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.common.xcconfig; sourceTree = "<group>"; };
 		192F052824AD3CC500A48456 /* ReactTestApp.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.debug.xcconfig; sourceTree = "<group>"; };
+		1963A06027C82E730013D276 /* React+Fabric.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "React+Fabric.h"; sourceTree = "<group>"; };
+		1963A06127C82E730013D276 /* React+Fabric.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "React+Fabric.mm"; sourceTree = "<group>"; };
 		196C22602490CB7600449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "React+Compatibility.m"; sourceTree = "<group>"; };
 		196C22612490CB7600449D3C /* React+Compatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "React+Compatibility.h"; sourceTree = "<group>"; };
 		196C7207232EF5DC006556ED /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
@@ -132,6 +135,8 @@
 				19141999234B2DD800D856AE /* RCTDevSupport+UIScene.m */,
 				196C22612490CB7600449D3C /* React+Compatibility.h */,
 				196C22602490CB7600449D3C /* React+Compatibility.m */,
+				1963A06027C82E730013D276 /* React+Fabric.h */,
+				1963A06127C82E730013D276 /* React+Fabric.mm */,
 				1988282224105BCC005057FF /* UIViewController+ReactTestApp.h */,
 				1988284424105BEC005057FF /* UIViewController+ReactTestApp.m */,
 				19ECD0DB232ED427003D8557 /* Assets.xcassets */,
@@ -343,6 +348,7 @@
 				196C724123319A85006556ED /* QRCodeScannerViewController.swift in Sources */,
 				1914199A234B2DD800D856AE /* RCTDevSupport+UIScene.m in Sources */,
 				196C22622490CB7600449D3C /* React+Compatibility.m in Sources */,
+				1963A06227C82E730013D276 /* React+Fabric.mm in Sources */,
 				196C7215232F1788006556ED /* ReactInstance.swift in Sources */,
 				19ECD0D8232ED425003D8557 /* SceneDelegate.swift in Sources */,
 				19A624A4258C95F000032776 /* Session.swift in Sources */,
diff --git a/ios/ReactTestApp/ContentView.swift b/ios/ReactTestApp/ContentView.swift
index b3e99ec..8ae4ce8 100644
--- a/ios/ReactTestApp/ContentView.swift
+++ b/ios/ReactTestApp/ContentView.swift
@@ -183,10 +183,10 @@ final class ContentViewController: UITableViewController {
             }
 
             let viewController = UIViewController(nibName: nil, bundle: nil)
-            viewController.view = RCTRootView(
-                bridge: bridge,
-                moduleName: component.appKey,
-                initialProperties: component.initialProperties
+            viewController.view = RTACreateReactRootView(
+                bridge,
+                component.appKey,
+                component.initialProperties
             )
             viewController.view.backgroundColor = UIColor.systemBackground
             return viewController
@@ -244,7 +244,12 @@ final class ContentViewController: UITableViewController {
             let patch = version[RCTVersionPatch] ?? "0"
             return "\(major).\(minor).\(patch)"
         }()
-        return "React Native version: \(version)"
+        #if USE_FABRIC
+            let fabric = " (Fabric)"
+        #else
+            let fabric = ""
+        #endif
+        return "React Native version: \(version)\(fabric)"
     }
 }
 
diff --git a/ios/ReactTestApp/React+Compatibility.h b/ios/ReactTestApp/React+Compatibility.h
index ce15068..e264394 100644
--- a/ios/ReactTestApp/React+Compatibility.h
+++ b/ios/ReactTestApp/React+Compatibility.h
@@ -1,7 +1,3 @@
 #import <Foundation/Foundation.h>
 
-@class RCTBridge;
-
 IMP RTASwizzleSelector(Class class, SEL originalSelector, SEL swizzledSelector);
-
-void RTATriggerReloadCommand(RCTBridge *, NSString *reason);
diff --git a/ios/ReactTestApp/React+Compatibility.m b/ios/ReactTestApp/React+Compatibility.m
index 175c136..6639d28 100644
--- a/ios/ReactTestApp/React+Compatibility.m
+++ b/ios/ReactTestApp/React+Compatibility.m
@@ -27,23 +27,6 @@ IMP RTASwizzleSelector(Class class, SEL originalSelector, SEL swizzledSelector)
     return originalImpl;
 }
 
-// MARK: - [0.62] RCTTriggerReloadCommandListeners replaces -[RCTBridge reload]
-
-// `RCTReloadCommand.h` is excluded from `react-native-macos`
-// See https://github.com/microsoft/react-native-macos/blob/v0.61.39/React-Core.podspec#L66
-#if REACT_NATIVE_VERSION == 0 || REACT_NATIVE_VERSION >= 6200
-#import <React/RCTReloadCommand.h>
-#endif
-
-void RTATriggerReloadCommand(RCTBridge *bridge, NSString *reason)
-{
-#if REACT_NATIVE_VERSION > 0 && REACT_NATIVE_VERSION < 6200
-    [bridge reload];
-#else
-    RCTTriggerReloadCommandListeners(reason);
-#endif
-}
-
 // MARK: - [0.63.2] Images do not render on iOS 14
 // See https://github.com/facebook/react-native/pull/29420
 
@@ -106,4 +89,4 @@ static void (*orig_displayLayer)(id, SEL, CALayer *);
 
 @end
 
-#endif
+#endif  // !TARGET_OS_OSX && REACT_NATIVE_VERSION < 6302
diff --git a/ios/ReactTestApp/React+Fabric.h b/ios/ReactTestApp/React+Fabric.h
new file mode 100644
index 0000000..f611f71
--- /dev/null
+++ b/ios/ReactTestApp/React+Fabric.h
@@ -0,0 +1,17 @@
+#import <Foundation/Foundation.h>
+
+#if TARGET_OS_IOS
+#define RTAView UIView
+@class UIView;
+#else
+#define RTAView NSView
+@class NSView;
+#endif
+
+@class RCTBridge;
+
+FOUNDATION_EXTERN RTAView *RTACreateReactRootView(RCTBridge *,
+                                                  NSString *moduleName,
+                                                  NSDictionary *initialProperties);
+
+FOUNDATION_EXTERN NSObject *RTACreateSurfacePresenterBridgeAdapter(RCTBridge *);
diff --git a/ios/ReactTestApp/React+Fabric.mm b/ios/ReactTestApp/React+Fabric.mm
new file mode 100644
index 0000000..67fee2a
--- /dev/null
+++ b/ios/ReactTestApp/React+Fabric.mm
@@ -0,0 +1,41 @@
+#import "React+Fabric.h"
+
+#ifdef USE_FABRIC
+#import <React/RCTFabricSurfaceHostingProxyRootView.h>
+#import <React/RCTSurfacePresenter.h>
+#import <React/RCTSurfacePresenterBridgeAdapter.h>
+#import <react/config/ReactNativeConfig.h>
+#else
+#import <React/RCTRootView.h>
+#endif  // USE_FABRIC
+
+RTAView *RTACreateReactRootView(RCTBridge *bridge,
+                                NSString *moduleName,
+                                NSDictionary *initialProperties)
+{
+#ifdef USE_FABRIC
+    return [[RCTFabricSurfaceHostingProxyRootView alloc] initWithBridge:bridge
+                                                             moduleName:moduleName
+                                                      initialProperties:initialProperties];
+#else
+    return [[RCTRootView alloc] initWithBridge:bridge
+                                    moduleName:moduleName
+                             initialProperties:initialProperties];
+#endif  // USE_FABRIC
+}
+
+NSObject *RTACreateSurfacePresenterBridgeAdapter(RCTBridge *bridge)
+{
+#ifdef USE_FABRIC
+    auto contextContainer = std::make_shared<facebook::react::ContextContainer const>();
+    auto reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
+    contextContainer->insert("ReactNativeConfig", reactNativeConfig);
+
+    auto bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge
+                                                                 contextContainer:contextContainer];
+    bridge.surfacePresenter = bridgeAdapter.surfacePresenter;
+    return bridgeAdapter;
+#else
+    return nil;
+#endif  // USE_FABRIC
+}
diff --git a/ios/ReactTestApp/ReactInstance.swift b/ios/ReactTestApp/ReactInstance.swift
index 335ab4d..277d38c 100644
--- a/ios/ReactTestApp/ReactInstance.swift
+++ b/ios/ReactTestApp/ReactInstance.swift
@@ -14,6 +14,7 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
         }
     }
 
+    private var surfacePresenterBridgeAdapter: NSObject?
     private(set) var bridge: RCTBridge?
     private var bundleRoot: String?
 
@@ -67,6 +68,13 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
                 name: .didReceiveRemoteBundleURL,
                 object: nil
             )
+        #else
+            NSAppleEventManager.shared().setEventHandler(
+                RCTLinkingManager.self,
+                andSelector: #selector(RCTLinkingManager.getUrlEventHandler(_:withReplyEvent:)),
+                forEventClass: AEEventClass(kInternetEventClass),
+                andEventID: AEEventID(kAEGetURL)
+            )
         #endif
 
         #if USE_FLIPPER
@@ -88,14 +96,14 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
     }
 
     func initReact(bundleRoot: String?, onDidInitialize: @escaping () -> Void) {
-        if let bridge = bridge {
+        if bridge != nil {
             if remoteBundleURL == nil {
                 // When loading the embedded bundle, we must disable remote
                 // debugging to prevent the bridge from getting stuck in
                 // -[RCTWebSocketExecutor executeApplicationScript:sourceURL:onComplete:]
                 RCTDevSettings().isDebuggingRemotely = false
             }
-            RTATriggerReloadCommand(bridge, "ReactTestApp")
+            RCTTriggerReloadCommandListeners("ReactTestApp")
             return
         }
 
@@ -111,6 +119,7 @@ final class ReactInstance: NSObject, RCTBridgeDelegate {
             return
         }
 
+        surfacePresenterBridgeAdapter = RTACreateSurfacePresenterBridgeAdapter(bridge)
         self.bridge = bridge
 
         NotificationCenter.default.post(
diff --git a/ios/ReactTestApp/ReactTestApp-Bridging-Header.h b/ios/ReactTestApp/ReactTestApp-Bridging-Header.h
index b3a5ddd..57b484e 100644
--- a/ios/ReactTestApp/ReactTestApp-Bridging-Header.h
+++ b/ios/ReactTestApp/ReactTestApp-Bridging-Header.h
@@ -6,7 +6,9 @@
 #import <React/RCTDevMenu.h>
 #import <React/RCTDevSettings.h>
 #import <React/RCTEventEmitter.h>
+#import <React/RCTLinkingManager.h>
 #import <React/RCTLog.h>
+#import <React/RCTReloadCommand.h>
 #import <React/RCTRootView.h>
 #import <React/RCTUtils.h>
 #import <React/RCTVersion.h>
@@ -21,7 +23,8 @@
 #import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
 #import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
 #import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
-#endif
+#endif  // USE_FLIPPER
 
 #import "React+Compatibility.h"
+#import "React+Fabric.h"
 #import "UIViewController+ReactTestApp.h"
diff --git a/ios/ReactTestApp/SceneDelegate.swift b/ios/ReactTestApp/SceneDelegate.swift
index eab05fd..3ba5461 100644
--- a/ios/ReactTestApp/SceneDelegate.swift
+++ b/ios/ReactTestApp/SceneDelegate.swift
@@ -61,6 +61,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
     }
 
     func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
+        for context in URLContexts {
+            RCTLinkingManager.application(
+                UIApplication.shared,
+                open: context.url,
+                options: context.options.dictionary()
+            )
+        }
+
         NotificationCenter.default.post(
             name: .ReactTestAppSceneDidOpenURL,
             object: [
@@ -70,3 +78,27 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
         )
     }
 }
+
+extension UIScene.OpenURLOptions {
+    func dictionary() -> [UIApplication.OpenURLOptionsKey: Any] {
+        var options: [UIApplication.OpenURLOptionsKey: Any] = [:]
+
+        if let sourceApplication = sourceApplication {
+            options[.sourceApplication] = sourceApplication
+        }
+
+        if let annotation = annotation {
+            options[.annotation] = annotation
+        }
+
+        options[.openInPlace] = openInPlace
+
+        if #available(iOS 14.5, *) {
+            if let eventAttribution = eventAttribution {
+                options[.eventAttribution] = eventAttribution
+            }
+        }
+
+        return options
+    }
+}
diff --git a/ios/test_app.rb b/ios/test_app.rb
index 8b99172..99d7b88 100644
--- a/ios/test_app.rb
+++ b/ios/test_app.rb
@@ -76,7 +76,9 @@ end
 
 def react_native_pods(version)
   v = version.release
-  if v == Gem::Version.new('0.0.0') || v >= Gem::Version.new('0.64')
+  if v == Gem::Version.new('0.0.0') || v >= Gem::Version.new('0.68')
+    'use_react_native-0.68'
+  elsif v >= Gem::Version.new('0.64')
     'use_react_native-0.64'
   elsif v >= Gem::Version.new('0.63')
     'use_react_native-0.63'
@@ -146,13 +148,14 @@ def use_react_native!(project_root, target_platform, options)
   require_relative(react_native_pods(version))
 
   include_react_native!(**options,
+                        app_path: find_file('package.json', project_root).parent,
                         path: react_native.relative_path_from(project_root).to_s,
                         rta_flipper_versions: flipper_versions,
                         rta_project_root: project_root,
                         rta_target_platform: target_platform)
 end
 
-def make_project!(xcodeproj, project_root, target_platform)
+def make_project!(xcodeproj, project_root, target_platform, options)
   src_xcodeproj = project_path(xcodeproj, target_platform)
   destination = File.join(nearest_node_modules(project_root), '.generated', target_platform.to_s)
   dst_xcodeproj = File.join(destination, xcodeproj)
@@ -212,6 +215,7 @@ def make_project!(xcodeproj, project_root, target_platform)
                                            end
 
   supports_flipper = target_platform == :ios && flipper_enabled?
+  use_fabric = options[:fabric_enabled] && version >= 6800
 
   app_project = Xcodeproj::Project.open(dst_xcodeproj)
   app_project.native_targets.each do |target|
@@ -220,24 +224,24 @@ def make_project!(xcodeproj, project_root, target_platform)
     target.build_configurations.each do |config|
       use_flipper = config.name == 'Debug' && supports_flipper
 
-      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
-        '$(inherited)',
-        version_macro,
-        "FB_SONARKIT_ENABLED=#{use_flipper ? 1 : 0}",
-        "USE_FLIPPER=#{use_flipper ? 1 : 0}",
-      ]
+      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
+      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << version_macro
+      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'USE_FABRIC=1' if use_fabric
+      if use_flipper
+        config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'FB_SONARKIT_ENABLED=1'
+        config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'USE_FLIPPER=1'
+      end
 
       build_settings.each do |setting, value|
         config.build_settings[setting] = value
       end
 
-      next unless use_flipper
-
-      config.build_settings['OTHER_SWIFT_FLAGS'] ||= [
-        '$(inherited)',
-        '-DFB_SONARKIT_ENABLED',
-        '-DUSE_FLIPPER',
-      ]
+      config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)']
+      config.build_settings['OTHER_SWIFT_FLAGS'] << '-DUSE_FABRIC' if use_fabric
+      if use_flipper
+        config.build_settings['OTHER_SWIFT_FLAGS'] << '-DFB_SONARKIT_ENABLED'
+        config.build_settings['OTHER_SWIFT_FLAGS'] << '-DUSE_FLIPPER'
+      end
     end
   end
   app_project.save
@@ -257,9 +261,8 @@ def use_test_app_internal!(target_platform, options)
 
   xcodeproj = 'ReactTestApp.xcodeproj'
   project_root = Pod::Config.instance.installation_root
-  dst_xcodeproj, platforms = make_project!(xcodeproj, project_root, target_platform).values_at(
-    :xcodeproj_path, :platforms
-  )
+  project_target = make_project!(xcodeproj, project_root, target_platform, options)
+  dst_xcodeproj, platforms = project_target.values_at(:xcodeproj_path, :platforms)
 
   require_relative(autolink_script_path)
 
@@ -312,13 +315,14 @@ def use_test_app_internal!(target_platform, options)
       when /\AFlipper/, 'libevent'
         target.build_configurations.each do |config|
           # Flipper and its dependencies log too many warnings
-          config.build_settings['WARNING_CFLAGS'] ||= ['"-w"']
+          config.build_settings['WARNING_CFLAGS'] = ['-w']
         end
       when /\AReact/
         target.build_configurations.each do |config|
           # Xcode 10.2 requires suppression of nullability for React
           # https://stackoverflow.com/questions/37691049/xcode-compile-flag-to-suppress-nullability-warnings-not-working
-          config.build_settings['WARNING_CFLAGS'] ||= ['"-Wno-nullability-completeness"']
+          config.build_settings['WARNING_CFLAGS'] ||= ['$(inherited)']
+          config.build_settings['WARNING_CFLAGS'] << '-Wno-nullability-completeness'
         end
       else
         # Ensure `ENABLE_TESTING_SEARCH_PATHS` is always set otherwise Xcode may
diff --git a/macos/ReactTestApp.xcodeproj/project.pbxproj b/macos/ReactTestApp.xcodeproj/project.pbxproj
index 3ce2e53..b77fde0 100644
--- a/macos/ReactTestApp.xcodeproj/project.pbxproj
+++ b/macos/ReactTestApp.xcodeproj/project.pbxproj
@@ -15,6 +15,7 @@
 		193EF093247A830200BE8C79 /* UIViewController+ReactTestApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 193EF091247A830200BE8C79 /* UIViewController+ReactTestApp.m */; };
 		193EF098247B130700BE8C79 /* ReactInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193EF097247B130700BE8C79 /* ReactInstance.swift */; };
 		1960F339258C97C400AEC7A2 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1960F338258C97C400AEC7A2 /* Session.swift */; };
+		1963A06527C82F7E0013D276 /* React+Fabric.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1963A06427C82F7E0013D276 /* React+Fabric.mm */; };
 		196C22652490CBAB00449D3C /* React+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 196C22632490CBAB00449D3C /* React+Compatibility.m */; };
 		19C4C89327710D8500157870 /* Manifest+Decoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C4C89227710D8500157870 /* Manifest+Decoder.swift */; };
 		19E791C024B08E1400FA6468 /* ReactTestAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19E791BF24B08E1400FA6468 /* ReactTestAppTests.swift */; };
@@ -56,6 +57,8 @@
 		193EF094247A84DA00BE8C79 /* ReactTestApp-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ReactTestApp-Bridging-Header.h"; path = "../ReactTestAppShared/ReactTestApp-Bridging-Header.h"; sourceTree = "<group>"; };
 		193EF097247B130700BE8C79 /* ReactInstance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReactInstance.swift; path = ../ReactTestAppShared/ReactInstance.swift; sourceTree = "<group>"; };
 		1960F338258C97C400AEC7A2 /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Session.swift; path = ../ReactTestAppShared/Session.swift; sourceTree = "<group>"; };
+		1963A06327C82F7E0013D276 /* React+Fabric.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "React+Fabric.h"; path = "../ReactTestAppShared/React+Fabric.h"; sourceTree = "<group>"; };
+		1963A06427C82F7E0013D276 /* React+Fabric.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = "React+Fabric.mm"; path = "../ReactTestAppShared/React+Fabric.mm"; sourceTree = "<group>"; };
 		196C22632490CBAB00449D3C /* React+Compatibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "React+Compatibility.m"; path = "../ReactTestAppShared/React+Compatibility.m"; sourceTree = "<group>"; };
 		196C22642490CBAB00449D3C /* React+Compatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "React+Compatibility.h"; path = "../ReactTestAppShared/React+Compatibility.h"; sourceTree = "<group>"; };
 		19B368BC24B12C24002CCEFF /* ReactTestApp.common.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ReactTestApp.common.xcconfig; sourceTree = "<group>"; };
@@ -123,6 +126,8 @@
 				193EF094247A84DA00BE8C79 /* ReactTestApp-Bridging-Header.h */,
 				196C22642490CBAB00449D3C /* React+Compatibility.h */,
 				196C22632490CBAB00449D3C /* React+Compatibility.m */,
+				1963A06327C82F7E0013D276 /* React+Fabric.h */,
+				1963A06427C82F7E0013D276 /* React+Fabric.mm */,
 				193EF092247A830200BE8C79 /* UIViewController+ReactTestApp.h */,
 				193EF091247A830200BE8C79 /* UIViewController+ReactTestApp.m */,
 				193EF066247A736300BE8C79 /* Assets.xcassets */,
@@ -332,6 +337,7 @@
 				193EF08F247A799D00BE8C79 /* Manifest.swift in Sources */,
 				19C4C89327710D8500157870 /* Manifest+Decoder.swift in Sources */,
 				196C22652490CBAB00449D3C /* React+Compatibility.m in Sources */,
+				1963A06527C82F7E0013D276 /* React+Fabric.mm in Sources */,
 				193EF098247B130700BE8C79 /* ReactInstance.swift in Sources */,
 				1960F339258C97C400AEC7A2 /* Session.swift in Sources */,
 				193EF065247A736200BE8C79 /* ViewController.swift in Sources */,
diff --git a/macos/ReactTestApp/AppDelegate.swift b/macos/ReactTestApp/AppDelegate.swift
index ec1cb27..bd22e56 100644
--- a/macos/ReactTestApp/AppDelegate.swift
+++ b/macos/ReactTestApp/AppDelegate.swift
@@ -171,31 +171,37 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
 
             let viewController = NSViewController(nibName: nil, bundle: nil)
             viewController.title = title
-            viewController.view = RCTRootView(
-                bridge: bridge,
-                moduleName: component.appKey,
-                initialProperties: component.initialProperties
+            viewController.view = RTACreateReactRootView(
+                bridge,
+                component.appKey,
+                component.initialProperties
             )
             return viewController
         }()
 
         switch component.presentationStyle {
         case "modal":
+            let rootView = viewController.view
             let modalFrame = NSRect(size: WindowSize.modalSize)
-            viewController.view.frame = modalFrame
-            if let rootView = viewController.view as? RCTRootView {
-                var token: NSObjectProtocol?
-                token = NotificationCenter.default.addObserver(
-                    forName: .RCTContentDidAppear,
-                    object: rootView,
-                    queue: nil,
-                    using: { _ in
-                        rootView.contentView.frame = modalFrame
-                        NotificationCenter.default.removeObserver(token!)
-                    }
-                )
-            }
+            rootView.frame = modalFrame
+
+            var token: NSObjectProtocol?
+            token = NotificationCenter.default.addObserver(
+                forName: .RCTContentDidAppear,
+                object: rootView,
+                queue: nil,
+                using: { _ in
+                    #if USE_FABRIC
+                        rootView.frame = modalFrame
+                    #else
+                        (rootView as? RCTRootView)?.contentView.frame = modalFrame
+                    #endif
+                    NotificationCenter.default.removeObserver(token!)
+                }
+            )
+
             window.contentViewController?.presentAsModalWindow(viewController)
+
         default:
             window.title = title
             let frame = window.contentViewController?.view.frame
diff --git a/test/configure/__snapshots__/gatherConfig.test.js.snap b/test/configure/__snapshots__/gatherConfig.test.js.snap
index 0070e83..0eeeece 100644
--- a/test/configure/__snapshots__/gatherConfig.test.js.snap
+++ b/test/configure/__snapshots__/gatherConfig.test.js.snap
@@ -24,18 +24,11 @@ use_test_app!
     "metro.config.js": Object {
       "source": "example/metro.config.js",
     },
-    "react-native.config.js": "const {
-  packageSatisfiesVersionRange,
-} = require(\\"react-native-test-app/scripts/configure\\");
+    "react-native.config.js": "const { iosProjectPath } = require(\\"react-native-test-app\\");
 module.exports = {
   project: {
     ios: {
-      project: packageSatisfiesVersionRange(
-        \\"@react-native-community/cli-platform-ios\\",
-        \\"<5.0.2\\"
-      )
-        ? \\"ReactTestApp-Dummy.xcodeproj\\"
-        : \\"node_modules/.generated/ios/ReactTestApp.xcodeproj\\",
+      project: iosProjectPath(\\".\\"),
     }
   }
 };
@@ -135,62 +128,25 @@ use_test_app!
     },
     "react-native.config.js": "const fs = require(\\"fs\\");
 const path = require(\\"path\\");
-
-const windowsProjectFile = path.join(
-  \\"node_modules\\",
-  \\".generated\\",
-  \\"windows\\",
-  \\"ReactTestApp\\",
-  \\"ReactTestApp.vcxproj\\"
-);
+const {
+  androidManifestPath,
+  iosProjectPath,
+  windowsProjectPath,
+} = require(\\"react-native-test-app\\");
 
 module.exports = {
   project: {
     android: {
       sourceDir: \\"android\\",
-      manifestPath: path.relative(
-        path.join(__dirname, \\"android\\"),
-        path.join(
-          path.dirname(require.resolve(\\"react-native-test-app/package.json\\")),
-          \\"android\\",
-          \\"app\\",
-          \\"src\\",
-          \\"main\\",
-          \\"AndroidManifest.xml\\"
-        )
-      ),
+      manifestPath: androidManifestPath(path.join(__dirname, \\"android\\")),
     },
     ios: {
-      project: (() => {
-        const {
-          packageSatisfiesVersionRange,
-        } = require(\\"react-native-test-app/scripts/configure\\");
-        if (
-          packageSatisfiesVersionRange(
-            \\"@react-native-community/cli-platform-ios\\",
-            \\"<5.0.2\\"
-          )
-        ) {
-          // Prior to @react-native-community/cli-platform-ios v5.0.0,
-          // \`project\` was only used to infer \`sourceDir\` and \`podfile\`.
-          return \\"ios/ReactTestApp-Dummy.xcodeproj\\";
-        }
-
-        // \`sourceDir\` and \`podfile\` detection was fixed in
-        // @react-native-community/cli-platform-ios v5.0.2 (see
-        // https://github.com/react-native-community/cli/pull/1444).
-        return \\"node_modules/.generated/ios/ReactTestApp.xcodeproj\\";
-      })(),
+      project: iosProjectPath(\\"ios\\"),
     },
-    windows: fs.existsSync(windowsProjectFile) && {
+    windows: fs.existsSync(\\"windows/Test.sln\\") && {
       sourceDir: \\"windows\\",
       solutionFile: \\"Test.sln\\",
-      project: {
-        projectFile: path.relative(
-          path.join(__dirname, \\"windows\\"),
-          windowsProjectFile
-        ),
-      },
+      project: windowsProjectPath(path.join(__dirname, \\"windows\\")),
     },
   },
 };
@@ -239,62 +195,25 @@ Object {
     },
     "common/react-native.config.js": "const fs = require(\\"fs\\");
 const path = require(\\"path\\");
-
-const windowsProjectFile = path.join(
-  \\"node_modules\\",
-  \\".generated\\",
-  \\"windows\\",
-  \\"ReactTestApp\\",
-  \\"ReactTestApp.vcxproj\\"
-);
+const {
+  androidManifestPath,
+  iosProjectPath,
+  windowsProjectPath,
+} = require(\\"react-native-test-app\\");
 
 module.exports = {
   project: {
     android: {
       sourceDir: \\"android\\",
-      manifestPath: path.relative(
-        path.join(__dirname, \\"android\\"),
-        path.join(
-          path.dirname(require.resolve(\\"react-native-test-app/package.json\\")),
-          \\"android\\",
-          \\"app\\",
-          \\"src\\",
-          \\"main\\",
-          \\"AndroidManifest.xml\\"
-        )
-      ),
+      manifestPath: androidManifestPath(path.join(__dirname, \\"android\\")),
     },
     ios: {
-      project: (() => {
-        const {
-          packageSatisfiesVersionRange,
-        } = require(\\"react-native-test-app/scripts/configure\\");
-        if (
-          packageSatisfiesVersionRange(
-            \\"@react-native-community/cli-platform-ios\\",
-            \\"<5.0.2\\"
-          )
-        ) {
-          // Prior to @react-native-community/cli-platform-ios v5.0.0,
-          // \`project\` was only used to infer \`sourceDir\` and \`podfile\`.
-          return \\"ios/ReactTestApp-Dummy.xcodeproj\\";
-        }
-
-        // \`sourceDir\` and \`podfile\` detection was fixed in
-        // @react-native-community/cli-platform-ios v5.0.2 (see
-        // https://github.com/react-native-community/cli/pull/1444).
-        return \\"node_modules/.generated/ios/ReactTestApp.xcodeproj\\";
-      })(),
+      project: iosProjectPath(\\"ios\\"),
     },
-    windows: fs.existsSync(windowsProjectFile) && {
+    windows: fs.existsSync(\\"windows/Test.sln\\") && {
       sourceDir: \\"windows\\",
       solutionFile: \\"Test.sln\\",
-      project: {
-        projectFile: path.relative(
-          path.join(__dirname, \\"windows\\"),
-          windowsProjectFile
-        ),
-      },
+      project: windowsProjectPath(path.join(__dirname, \\"windows\\")),
     },
   },
 };
@@ -304,62 +223,25 @@ module.exports = {
     },
     "react-native.config.js": "const fs = require(\\"fs\\");
 const path = require(\\"path\\");
-
-const windowsProjectFile = path.join(
-  \\"node_modules\\",
-  \\".generated\\",
-  \\"windows\\",
-  \\"ReactTestApp\\",
-  \\"ReactTestApp.vcxproj\\"
-);
+const {
+  androidManifestPath,
+  iosProjectPath,
+  windowsProjectPath,
+} = require(\\"react-native-test-app\\");
 
 module.exports = {
   project: {
     android: {
       sourceDir: \\"android\\",
-      manifestPath: path.relative(
-        path.join(__dirname, \\"android\\"),
-        path.join(
-          path.dirname(require.resolve(\\"react-native-test-app/package.json\\")),
-          \\"android\\",
-          \\"app\\",
-          \\"src\\",
-          \\"main\\",
-          \\"AndroidManifest.xml\\"
-        )
-      ),
+      manifestPath: androidManifestPath(path.join(__dirname, \\"android\\")),
     },
     ios: {
-      project: (() => {
-        const {
-          packageSatisfiesVersionRange,
-        } = require(\\"react-native-test-app/scripts/configure\\");
-        if (
-          packageSatisfiesVersionRange(
-            \\"@react-native-community/cli-platform-ios\\",
-            \\"<5.0.2\\"
-          )
-        ) {
-          // Prior to @react-native-community/cli-platform-ios v5.0.0,
-          // \`project\` was only used to infer \`sourceDir\` and \`podfile\`.
-          return \\"ios/ReactTestApp-Dummy.xcodeproj\\";
-        }
-
-        // \`sourceDir\` and \`podfile\` detection was fixed in
-        // @react-native-community/cli-platform-ios v5.0.2 (see
-        // https://github.com/react-native-community/cli/pull/1444).
-        return \\"node_modules/.generated/ios/ReactTestApp.xcodeproj\\";
-      })(),
+      project: iosProjectPath(\\"ios\\"),
     },
-    windows: fs.existsSync(windowsProjectFile) && {
+    windows: fs.existsSync(\\"windows/Test.sln\\") && {
       sourceDir: \\"windows\\",
       solutionFile: \\"Test.sln\\",
-      project: {
-        projectFile: path.relative(
-          path.join(__dirname, \\"windows\\"),
-          windowsProjectFile
-        ),
-      },
+      project: windowsProjectPath(path.join(__dirname, \\"windows\\")),
     },
   },
 };
@@ -398,62 +280,25 @@ use_test_app!
     },
     "react-native.config.js": "const fs = require(\\"fs\\");
 const path = require(\\"path\\");
-
-const windowsProjectFile = path.join(
-  \\"node_modules\\",
-  \\".generated\\",
-  \\"windows\\",
-  \\"ReactTestApp\\",
-  \\"ReactTestApp.vcxproj\\"
-);
+const {
+  androidManifestPath,
+  iosProjectPath,
+  windowsProjectPath,
+} = require(\\"react-native-test-app\\");
 
 module.exports = {
   project: {
     android: {
       sourceDir: \\"android\\",
-      manifestPath: path.relative(
-        path.join(__dirname, \\"android\\"),
-        path.join(
-          path.dirname(require.resolve(\\"react-native-test-app/package.json\\")),
-          \\"android\\",
-          \\"app\\",
-          \\"src\\",
-          \\"main\\",
-          \\"AndroidManifest.xml\\"
-        )
-      ),
+      manifestPath: androidManifestPath(path.join(__dirname, \\"android\\")),
     },
     ios: {
-      project: (() => {
-        const {
-          packageSatisfiesVersionRange,
-        } = require(\\"react-native-test-app/scripts/configure\\");
-        if (
-          packageSatisfiesVersionRange(
-            \\"@react-native-community/cli-platform-ios\\",
-            \\"<5.0.2\\"
-          )
-        ) {
-          // Prior to @react-native-community/cli-platform-ios v5.0.0,
-          // \`project\` was only used to infer \`sourceDir\` and \`podfile\`.
-          return \\"ios/ReactTestApp-Dummy.xcodeproj\\";
-        }
-
-        // \`sourceDir\` and \`podfile\` detection was fixed in
-        // @react-native-community/cli-platform-ios v5.0.2 (see
-        // https://github.com/react-native-community/cli/pull/1444).
-        return \\"node_modules/.generated/ios/ReactTestApp.xcodeproj\\";
-      })(),
+      project: iosProjectPath(\\"ios\\"),
     },
-    windows: fs.existsSync(windowsProjectFile) && {
+    windows: fs.existsSync(\\"windows/Test.sln\\") && {
       sourceDir: \\"windows\\",
       solutionFile: \\"Test.sln\\",
-      project: {
-        projectFile: path.relative(
-          path.join(__dirname, \\"windows\\"),
-          windowsProjectFile
-        ),
-      },
+      project: windowsProjectPath(path.join(__dirname, \\"windows\\")),
     },
   },
 };
@@ -562,62 +407,25 @@ use_test_app!
     },
     "react-native.config.js": "const fs = require(\\"fs\\");
 const path = require(\\"path\\");
-
-const windowsProjectFile = path.join(
-  \\"node_modules\\",
-  \\".generated\\",
-  \\"windows\\",
-  \\"ReactTestApp\\",
-  \\"ReactTestApp.vcxproj\\"
-);
+const {
+  androidManifestPath,
+  iosProjectPath,
+  windowsProjectPath,
+} = require(\\"react-native-test-app\\");
 
 module.exports = {
   project: {
     android: {
       sourceDir: \\"android\\",
-      manifestPath: path.relative(
-        path.join(__dirname, \\"android\\"),
-        path.join(
-          path.dirname(require.resolve(\\"react-native-test-app/package.json\\")),
-          \\"android\\",
-          \\"app\\",
-          \\"src\\",
-          \\"main\\",
-          \\"AndroidManifest.xml\\"
-        )
-      ),
+      manifestPath: androidManifestPath(path.join(__dirname, \\"android\\")),
     },
     ios: {
-      project: (() => {
-        const {
-          packageSatisfiesVersionRange,
-        } = require(\\"react-native-test-app/scripts/configure\\");
-        if (
-          packageSatisfiesVersionRange(
-            \\"@react-native-community/cli-platform-ios\\",
-            \\"<5.0.2\\"
-          )
-        ) {
-          // Prior to @react-native-community/cli-platform-ios v5.0.0,
-          // \`project\` was only used to infer \`sourceDir\` and \`podfile\`.
-          return \\"ios/ReactTestApp-Dummy.xcodeproj\\";
-        }
-
-        // \`sourceDir\` and \`podfile\` detection was fixed in
-        // @react-native-community/cli-platform-ios v5.0.2 (see
-        // https://github.com/react-native-community/cli/pull/1444).
-        return \\"node_modules/.generated/ios/ReactTestApp.xcodeproj\\";
-      })(),
+      project: iosProjectPath(\\"ios\\"),
     },
-    windows: fs.existsSync(windowsProjectFile) && {
+    windows: fs.existsSync(\\"windows/Test.sln\\") && {
       sourceDir: \\"windows\\",
       solutionFile: \\"Test.sln\\",
-      project: {
-        projectFile: path.relative(
-          path.join(__dirname, \\"windows\\"),
-          windowsProjectFile
-        ),
-      },
+      project: windowsProjectPath(path.join(__dirname, \\"windows\\")),
     },
   },
 };
@@ -733,62 +541,25 @@ use_test_app!
     },
     "react-native.config.js": "const fs = require(\\"fs\\");
 const path = require(\\"path\\");
-
-const windowsProjectFile = path.join(
-  \\"node_modules\\",
-  \\".generated\\",
-  \\"windows\\",
-  \\"ReactTestApp\\",
-  \\"ReactTestApp.vcxproj\\"
-);
+const {
+  androidManifestPath,
+  iosProjectPath,
+  windowsProjectPath,
+} = require(\\"react-native-test-app\\");
 
 module.exports = {
   project: {
     android: {
       sourceDir: \\"android\\",
-      manifestPath: path.relative(
-        path.join(__dirname, \\"android\\"),
-        path.join(
-          path.dirname(require.resolve(\\"react-native-test-app/package.json\\")),
-          \\"android\\",
-          \\"app\\",
-          \\"src\\",
-          \\"main\\",
-          \\"AndroidManifest.xml\\"
-        )
-      ),
+      manifestPath: androidManifestPath(path.join(__dirname, \\"android\\")),
     },
     ios: {
-      project: (() => {
-        const {
-          packageSatisfiesVersionRange,
-        } = require(\\"react-native-test-app/scripts/configure\\");
-        if (
-          packageSatisfiesVersionRange(
-            \\"@react-native-community/cli-platform-ios\\",
-            \\"<5.0.2\\"
-          )
-        ) {
-          // Prior to @react-native-community/cli-platform-ios v5.0.0,
-          // \`project\` was only used to infer \`sourceDir\` and \`podfile\`.
-          return \\"ios/ReactTestApp-Dummy.xcodeproj\\";
-        }
-
-        // \`sourceDir\` and \`podfile\` detection was fixed in
-        // @react-native-community/cli-platform-ios v5.0.2 (see
-        // https://github.com/react-native-community/cli/pull/1444).
-        return \\"node_modules/.generated/ios/ReactTestApp.xcodeproj\\";
-      })(),
+      project: iosProjectPath(\\"ios\\"),
     },
-    windows: fs.existsSync(windowsProjectFile) && {
+    windows: fs.existsSync(\\"windows/Test.sln\\") && {
       sourceDir: \\"windows\\",
       solutionFile: \\"Test.sln\\",
-      project: {
-        projectFile: path.relative(
-          path.join(__dirname, \\"windows\\"),
-          windowsProjectFile
-        ),
-      },
+      project: windowsProjectPath(path.join(__dirname, \\"windows\\")),
     },
   },
 };
diff --git a/test/configure/reactNativeConfig.test.js b/test/configure/reactNativeConfig.test.js
index ae69156..353311f 100644
--- a/test/configure/reactNativeConfig.test.js
+++ b/test/configure/reactNativeConfig.test.js
@@ -23,34 +23,30 @@ describe("reactNativeConfig()", () => {
 
   test("returns Android specific config for a flatten structure", () => {
     const params = mockParams({ platforms: ["android"], flatten: true });
-    expect(reactNativeConfig(params)).toContain("AndroidManifest.xml");
-    expect(reactNativeConfig(params)).not.toContain(
-      "ReactTestApp-Dummy.xcodeproj"
-    );
-    expect(reactNativeConfig(params)).not.toContain("ReactTestApp.vcxproj");
+    expect(reactNativeConfig(params)).toContain("androidManifestPath");
+    expect(reactNativeConfig(params)).not.toContain("iosProjectPath");
+    expect(reactNativeConfig(params)).not.toContain("windowsProjectPath");
   });
 
   test("returns iOS specific config for a flatten structure", () => {
     const params = mockParams({ platforms: ["ios"], flatten: true });
-    expect(reactNativeConfig(params)).not.toContain("AndroidManifest.xml");
-    expect(reactNativeConfig(params)).toContain("ReactTestApp-Dummy.xcodeproj");
-    expect(reactNativeConfig(params)).not.toContain("ReactTestApp.vcxproj");
+    expect(reactNativeConfig(params)).not.toContain("androidManifestPath");
+    expect(reactNativeConfig(params)).toContain("iosProjectPath");
+    expect(reactNativeConfig(params)).not.toContain("windowsProjectPath");
   });
 
   test("returns macOS specific config for a flatten structure", () => {
     const params = mockParams({ platforms: ["macos"], flatten: true });
-    expect(reactNativeConfig(params)).not.toContain("AndroidManifest.xml");
-    expect(reactNativeConfig(params)).toContain("ReactTestApp-Dummy.xcodeproj");
-    expect(reactNativeConfig(params)).not.toContain("ReactTestApp.vcxproj");
+    expect(reactNativeConfig(params)).not.toContain("androidManifestPath");
+    expect(reactNativeConfig(params)).toContain("iosProjectPath");
+    expect(reactNativeConfig(params)).not.toContain("windowsProjectPath");
   });
 
   test("returns Windows specific config for a flatten structure", () => {
     const params = mockParams({ platforms: ["windows"], flatten: true });
-    expect(reactNativeConfig(params)).not.toContain("AndroidManifest.xml");
-    expect(reactNativeConfig(params)).not.toContain(
-      "ReactTestApp-Dummy.xcodeproj"
-    );
-    expect(reactNativeConfig(params)).toContain("ReactTestApp.vcxproj");
+    expect(reactNativeConfig(params)).not.toContain("androidManifestPath");
+    expect(reactNativeConfig(params)).not.toContain("iosProjectPath");
+    expect(reactNativeConfig(params)).toContain("windowsProjectPath");
   });
 
   test("throws when an unknown platform is specified", () => {
diff --git a/test/test_test_app.rb b/test/test_test_app.rb
index 3bd3af8..b0106cd 100644
--- a/test/test_test_app.rb
+++ b/test/test_test_app.rb
@@ -83,10 +83,17 @@ class TestTestApp < Minitest::Test
   end
 
   def test_react_native_pods
-    assert_equal('use_react_native-0.64', react_native_pods(Gem::Version.new('1000.0.0')))
+    assert_equal('use_react_native-0.68', react_native_pods(Gem::Version.new('1000.0.0')))
 
+    assert_equal('use_react_native-0.68', react_native_pods(Gem::Version.new('0.68.0')))
+    assert_equal('use_react_native-0.68', react_native_pods(Gem::Version.new('0.68.0-rc.1')))
+
+    assert_equal('use_react_native-0.64', react_native_pods(Gem::Version.new('0.67.3')))
+    assert_equal('use_react_native-0.64', react_native_pods(Gem::Version.new('0.66.4')))
+    assert_equal('use_react_native-0.64', react_native_pods(Gem::Version.new('0.65.1')))
+
+    assert_equal('use_react_native-0.64', react_native_pods(Gem::Version.new('0.64.3')))
     assert_equal('use_react_native-0.64', react_native_pods(Gem::Version.new('0.64.0')))
-    assert_equal('use_react_native-0.64', react_native_pods(Gem::Version.new('0.64.0-rc.1')))
 
     assert_equal('use_react_native-0.63', react_native_pods(Gem::Version.new('0.63.4')))
     assert_equal('use_react_native-0.63', react_native_pods(Gem::Version.new('0.63.0')))
diff --git a/windows/test-app.js b/windows/test-app.js
index 428c615..f499008 100755
--- a/windows/test-app.js
+++ b/windows/test-app.js
@@ -701,15 +701,22 @@ function generateSolution(destPath, { autolink, useHermes, useNuGet }) {
       rethrow
     );
 
-    copyAndReplace(
-      path.join(__dirname, "ExperimentalFeatures.props"),
-      path.join(destPath, "ExperimentalFeatures.props"),
-      {
-        ...(hermesVersion
-          ? { "<UseHermes>false</UseHermes>": `<UseHermes>true</UseHermes>` }
-          : undefined),
-      }
+    const experimentalFeaturesPropsFilename = "ExperimentalFeatures.props";
+    const experimentalFeaturesPropsPath = path.join(
+      destPath,
+      experimentalFeaturesPropsFilename
     );
+    if (!fs.existsSync(experimentalFeaturesPropsPath)) {
+      copyAndReplace(
+        path.join(__dirname, experimentalFeaturesPropsFilename),
+        experimentalFeaturesPropsPath,
+        {
+          ...(hermesVersion
+            ? { "<UseHermes>false</UseHermes>": `<UseHermes>true</UseHermes>` }
+            : undefined),
+        }
+      );
+    }
 
     // TODO: Remove when we drop support for 0.67.
     // Patch building with Visual Studio 2022. For more details, see