#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:
