Expo 54: Manual Setup

Important

The Expo Team does not plan to provide support for SDK54. Follow the patch below to enable the expo-updates integration manually.

iOS:

Step 1:

In ios/EXUpdates/UpdatesConfig.swift :

@@ -141,7 +141,8 @@ public final class UpdatesConfig: NSObject {
   }
 
   private static func configDictionaryWithExpoPlist(mergingOtherDictionary: [String: Any]?) throws -> [String: Any] {
-    guard let configPlistPath = Bundle.main.path(forResource: PlistName, ofType: "plist") else {
+    let bundle = Bundle(for: UpdatesConfig.self)
+    guard let configPlistPath = bundle.path(forResource: PlistName, ofType: "plist") else {
       throw UpdatesConfigError.ExpoUpdatesConfigPlistError
     }
Step 2:

In ios/EXUpdates/UpdatesUtils.swift :

@@ -108,18 +108,22 @@ public final class UpdatesUtils: NSObject {
     return assetFilesMap
   }
 
+  private static func getBundle() -> Bundle {
+    return Bundle(for: UpdatesUtils.self)
+  }
+
   internal static func url(forBundledAsset asset: UpdateAsset) -> URL? {
     guard let mainBundleDir = asset.mainBundleDir else {
-      return Bundle.main.url(forResource: asset.mainBundleFilename, withExtension: asset.type)
+      return getBundle().url(forResource: asset.mainBundleFilename, withExtension: asset.type)
     }
-    return Bundle.main.url(forResource: asset.mainBundleFilename, withExtension: asset.type, subdirectory: mainBundleDir)
+    return getBundle().url(forResource: asset.mainBundleFilename, withExtension: asset.type, subdirectory: mainBundleDir)
   }
 
   internal static func path(forBundledAsset asset: UpdateAsset) -> String? {
     guard let mainBundleDir = asset.mainBundleDir else {
-      return Bundle.main.path(forResource: asset.mainBundleFilename, ofType: asset.type)
+      return getBundle().path(forResource: asset.mainBundleFilename, ofType: asset.type)
     }
-    return Bundle.main.path(forResource: asset.mainBundleFilename, ofType: asset.type, inDirectory: mainBundleDir)
+    return getBundle().path(forResource: asset.mainBundleFilename, ofType: asset.type, inDirectory: mainBundleDir)
   }
 

Android:

Step 1:

In expo-updates/android/**/UpdatesController.kt :

@@ -2,6 +2,7 @@ package expo.modules.updates
 
 import android.content.Context
 import com.facebook.react.ReactApplication
+import com.facebook.react.ReactHost
 import expo.modules.updates.events.IUpdatesEventManagerObserver
 import expo.modules.updates.loader.LoaderTask
 import expo.modules.updates.logging.UpdatesErrorCode
@@ -25,6 +26,19 @@ object UpdatesController {
   @Volatile
   private var overrideConfiguration: UpdatesConfiguration? = null
 
+  @Volatile
+  private var reactHost: ReactHost? = null
+
+  @JvmStatic
+  fun setReactHost(reactHost: ReactHost) {
+    this.reactHost = reactHost
+  }
+
+  @JvmStatic
+  fun getReactHost(): ReactHost? {
+    return reactHost
+  }
+
Step 2:

In expo-updates/android/**/RecreateReactContextProcedure.kt :

import android.content.Context
-import com.facebook.react.ReactApplication
 import expo.modules.updates.launcher.Launcher
 import expo.modules.updates.statemachine.UpdatesStateEvent
 import kotlinx.coroutines.CoroutineScope
@@ -20,16 +19,11 @@ class RecreateReactContextProcedure(
   override val loggerTimerLabel = "timer-recreate-react-context"
 
   override suspend fun run(procedureContext: ProcedureContext) {
-    val reactApplication = context.applicationContext as? ReactApplication ?: run inner@{
-      callback.onFailure(Exception("Could not reload application. Ensure you have passed the correct instance of ReactApplication into UpdatesController.initialize()."))
-      return
-    }
-
     procedureContext.processStateEvent(UpdatesStateEvent.Restart())
     callback.onSuccess()
     procedureScope.launch {
       withContext(Dispatchers.Main) {
-        reactApplication.restart(weakActivity?.get(), "Restart from RecreateReactContextProcedure")
+        RestartReactAppExtensions.restart(weakActivity?.get(), "Restart from RecreateReactContextProcedure")
       }
     }
Step 3:

In expo-updates/android/**/RelaunchProcedure.kt :

@@ -43,11 +43,6 @@ class RelaunchProcedure(
   override val loggerTimerLabel = "timer-relaunch"
 
   override suspend fun run(procedureContext: ProcedureContext) {
-    val reactApplication = context as? ReactApplication ?: run inner@{
-      callback.onFailure(Exception("Could not reload application. Ensure you have passed the correct instance of ReactApplication into UpdatesController.initialize()."))
-      return
-    }
-
     procedureContext.processStateEvent(UpdatesStateEvent.Restart())
 
     val oldLaunchAssetFile = getCurrentLauncher().launchAssetFile
@@ -74,7 +69,7 @@ class RelaunchProcedure(
     val newLaunchAssetFile = getCurrentLauncher().launchAssetFile
     if (newLaunchAssetFile != null && newLaunchAssetFile != oldLaunchAssetFile) {
       try {
-        replaceLaunchAssetFileIfNeeded(reactApplication, newLaunchAssetFile)
+        replaceLaunchAssetFileIfNeeded(newLaunchAssetFile)
       } catch (e: Exception) {
         logger.error("Could not reset launchAssetFile for the ReactApplication", e, UpdatesErrorCode.Unknown)
       }
@@ -84,7 +79,7 @@ class RelaunchProcedure(
     procedureScope.launch {
       withContext(Dispatchers.Main) {
         reloadScreenManager?.show(weakActivity?.get())
-        reactApplication.restart(weakActivity?.get(), "Restart from RelaunchProcedure")
+        RestartReactAppExtensions.restart(weakActivity?.get(), "Restart from RelaunchProcedure")
       }
     }
 
@@ -127,13 +122,16 @@ class RelaunchProcedure(
    * [com.facebook.react.ReactInstanceManager].
    */
   private fun replaceLaunchAssetFileIfNeeded(
-    reactApplication: ReactApplication,
     launchAssetFile: String
   ) {
     if (ReactNativeFeatureFlags.enableBridgelessArchitecture) {
       return
     }
 
+    val reactApplication = context as? ReactApplication ?: run inner@{
+      callback.onFailure(Exception("Could not reload application. Ensure you have passed the correct instance of ReactApplication into UpdatesController.initialize()."))
+      return
+    }
     val instanceManager = reactApplication.reactNativeHost.reactInstanceManager
Step 4:

In expo-updates/android/**/RestartReactAppExtensions.kt :

import com.facebook.react.ReactApplication
+import com.facebook.react.ReactHost
+import com.facebook.react.ReactNativeHost
 import com.facebook.react.common.LifecycleState
 import expo.modules.rncompatibility.ReactNativeFeatureFlags
+import expo.modules.updates.UpdatesController
 
-/**
- * An extension for [ReactApplication] to restart the app
- *
- * @param activity For bridgeless mode if the ReactHost is destroyed, we need an Activity to resume it.
- * @param reason The restart reason. Only used on bridgeless mode.
- */
-internal fun ReactApplication.restart(activity: Activity?, reason: String) {
-  if (ReactNativeFeatureFlags.enableBridgelessArchitecture) {
-    val reactHost = this.reactHost
-    check(reactHost != null)
-    if (reactHost.lifecycleState != LifecycleState.RESUMED && activity != null) {
-      reactHost.onHostResume(activity)
+private fun getReactHostReflectively(app: ReactApplication): ReactHost? {
+  return runCatching {
+    app.javaClass.getMethod("getReactHost").invoke(app) as? ReactHost
+  }.getOrNull()
+}
+
+private fun getReactNativeHostReflectively(app: ReactApplication): ReactNativeHost? {
+  return runCatching {
+    app.javaClass.getMethod("getReactNativeHost").invoke(app) as? ReactNativeHost
+  }.getOrNull()
+}
+
+object RestartReactAppExtensions {
+  /**
+   * A function to restart the app
+   *
+   * @param activity For bridgeless mode if the ReactHost is destroyed, we need an Activity to resume it.
+   * @param reason The restart reason. Only used on bridgeless mode.
+   */
+  fun restart(activity: Activity?, reason: String) {
+    if (ReactNativeFeatureFlags.enableBridgelessArchitecture) {
+      val reactHost = UpdatesController.getReactHost() ?: getReactHostReflectively(activity?.application as ReactApplication)
+      check(reactHost != null)
+      if (reactHost.lifecycleState != LifecycleState.RESUMED && activity != null) {
+        reactHost.onHostResume(activity)
+      }
+      reactHost.reload(reason)
+      return
     }
-    reactHost.reload(reason)
-    return
-  }
 
-  reactNativeHost.reactInstanceManager.recreateReactContextInBackground()
+    getReactNativeHostReflectively(activity?.application as ReactApplication)?.reactInstanceManager?.recreateReactContextInBackground()
+  }

To see a complete patch, please visit:

Need React or React Native expertise you can count on?