|
| 1 | +# Expo 54: Manual Setup |
| 2 | + |
| 3 | +:::danger Important |
| 4 | +The Expo Team does not plan to provide support for SDK54. Follow the patch below to enable the expo-updates integration manually. |
| 5 | +::: |
| 6 | + |
| 7 | + |
| 8 | +### iOS: |
| 9 | + |
| 10 | +##### Step 1: |
| 11 | + |
| 12 | +In `ios/EXUpdates/UpdatesConfig.swift` : |
| 13 | + |
| 14 | +```diff |
| 15 | +@@ -141,7 +141,8 @@ public final class UpdatesConfig: NSObject { |
| 16 | + } |
| 17 | + |
| 18 | + private static func configDictionaryWithExpoPlist(mergingOtherDictionary: [String: Any]?) throws -> [String: Any] { |
| 19 | +- guard let configPlistPath = Bundle.main.path(forResource: PlistName, ofType: "plist") else { |
| 20 | ++ let bundle = Bundle(for: UpdatesConfig.self) |
| 21 | ++ guard let configPlistPath = bundle.path(forResource: PlistName, ofType: "plist") else { |
| 22 | + throw UpdatesConfigError.ExpoUpdatesConfigPlistError |
| 23 | + } |
| 24 | +``` |
| 25 | + |
| 26 | +##### Step 2: |
| 27 | + |
| 28 | +In `ios/EXUpdates/UpdatesUtils.swift` : |
| 29 | + |
| 30 | +```diff |
| 31 | +@@ -108,18 +108,22 @@ public final class UpdatesUtils: NSObject { |
| 32 | + return assetFilesMap |
| 33 | + } |
| 34 | + |
| 35 | ++ private static func getBundle() -> Bundle { |
| 36 | ++ return Bundle(for: UpdatesUtils.self) |
| 37 | ++ } |
| 38 | ++ |
| 39 | + internal static func url(forBundledAsset asset: UpdateAsset) -> URL? { |
| 40 | + guard let mainBundleDir = asset.mainBundleDir else { |
| 41 | +- return Bundle.main.url(forResource: asset.mainBundleFilename, withExtension: asset.type) |
| 42 | ++ return getBundle().url(forResource: asset.mainBundleFilename, withExtension: asset.type) |
| 43 | + } |
| 44 | +- return Bundle.main.url(forResource: asset.mainBundleFilename, withExtension: asset.type, subdirectory: mainBundleDir) |
| 45 | ++ return getBundle().url(forResource: asset.mainBundleFilename, withExtension: asset.type, subdirectory: mainBundleDir) |
| 46 | + } |
| 47 | + |
| 48 | + internal static func path(forBundledAsset asset: UpdateAsset) -> String? { |
| 49 | + guard let mainBundleDir = asset.mainBundleDir else { |
| 50 | +- return Bundle.main.path(forResource: asset.mainBundleFilename, ofType: asset.type) |
| 51 | ++ return getBundle().path(forResource: asset.mainBundleFilename, ofType: asset.type) |
| 52 | + } |
| 53 | +- return Bundle.main.path(forResource: asset.mainBundleFilename, ofType: asset.type, inDirectory: mainBundleDir) |
| 54 | ++ return getBundle().path(forResource: asset.mainBundleFilename, ofType: asset.type, inDirectory: mainBundleDir) |
| 55 | + } |
| 56 | + |
| 57 | +``` |
| 58 | + |
| 59 | +### Android: |
| 60 | + |
| 61 | +##### Step 1: |
| 62 | + |
| 63 | +In `expo-updates/android/**/UpdatesController.kt` : |
| 64 | + |
| 65 | +```diff |
| 66 | +@@ -2,6 +2,7 @@ package expo.modules.updates |
| 67 | + |
| 68 | + import android.content.Context |
| 69 | + import com.facebook.react.ReactApplication |
| 70 | ++import com.facebook.react.ReactHost |
| 71 | + import expo.modules.updates.events.IUpdatesEventManagerObserver |
| 72 | + import expo.modules.updates.loader.LoaderTask |
| 73 | + import expo.modules.updates.logging.UpdatesErrorCode |
| 74 | +@@ -25,6 +26,19 @@ object UpdatesController { |
| 75 | + @Volatile |
| 76 | + private var overrideConfiguration: UpdatesConfiguration? = null |
| 77 | + |
| 78 | ++ @Volatile |
| 79 | ++ private var reactHost: ReactHost? = null |
| 80 | ++ |
| 81 | ++ @JvmStatic |
| 82 | ++ fun setReactHost(reactHost: ReactHost) { |
| 83 | ++ this.reactHost = reactHost |
| 84 | ++ } |
| 85 | ++ |
| 86 | ++ @JvmStatic |
| 87 | ++ fun getReactHost(): ReactHost? { |
| 88 | ++ return reactHost |
| 89 | ++ } |
| 90 | ++ |
| 91 | +``` |
| 92 | + |
| 93 | +##### Step 2: |
| 94 | + |
| 95 | +In `expo-updates/android/**/RecreateReactContextProcedure.kt` : |
| 96 | + |
| 97 | +```diff |
| 98 | +import android.content.Context |
| 99 | +-import com.facebook.react.ReactApplication |
| 100 | + import expo.modules.updates.launcher.Launcher |
| 101 | + import expo.modules.updates.statemachine.UpdatesStateEvent |
| 102 | + import kotlinx.coroutines.CoroutineScope |
| 103 | +@@ -20,16 +19,11 @@ class RecreateReactContextProcedure( |
| 104 | + override val loggerTimerLabel = "timer-recreate-react-context" |
| 105 | + |
| 106 | + override suspend fun run(procedureContext: ProcedureContext) { |
| 107 | +- val reactApplication = context.applicationContext as? ReactApplication ?: run inner@{ |
| 108 | +- callback.onFailure(Exception("Could not reload application. Ensure you have passed the correct instance of ReactApplication into UpdatesController.initialize().")) |
| 109 | +- return |
| 110 | +- } |
| 111 | +- |
| 112 | + procedureContext.processStateEvent(UpdatesStateEvent.Restart()) |
| 113 | + callback.onSuccess() |
| 114 | + procedureScope.launch { |
| 115 | + withContext(Dispatchers.Main) { |
| 116 | +- reactApplication.restart(weakActivity?.get(), "Restart from RecreateReactContextProcedure") |
| 117 | ++ RestartReactAppExtensions.restart(weakActivity?.get(), "Restart from RecreateReactContextProcedure") |
| 118 | + } |
| 119 | + } |
| 120 | +``` |
| 121 | + |
| 122 | +##### Step 3: |
| 123 | + |
| 124 | +In `expo-updates/android/**/RelaunchProcedure.kt` : |
| 125 | + |
| 126 | +```diff |
| 127 | +@@ -43,11 +43,6 @@ class RelaunchProcedure( |
| 128 | + override val loggerTimerLabel = "timer-relaunch" |
| 129 | + |
| 130 | + override suspend fun run(procedureContext: ProcedureContext) { |
| 131 | +- val reactApplication = context as? ReactApplication ?: run inner@{ |
| 132 | +- callback.onFailure(Exception("Could not reload application. Ensure you have passed the correct instance of ReactApplication into UpdatesController.initialize().")) |
| 133 | +- return |
| 134 | +- } |
| 135 | +- |
| 136 | + procedureContext.processStateEvent(UpdatesStateEvent.Restart()) |
| 137 | + |
| 138 | + val oldLaunchAssetFile = getCurrentLauncher().launchAssetFile |
| 139 | +@@ -74,7 +69,7 @@ class RelaunchProcedure( |
| 140 | + val newLaunchAssetFile = getCurrentLauncher().launchAssetFile |
| 141 | + if (newLaunchAssetFile != null && newLaunchAssetFile != oldLaunchAssetFile) { |
| 142 | + try { |
| 143 | +- replaceLaunchAssetFileIfNeeded(reactApplication, newLaunchAssetFile) |
| 144 | ++ replaceLaunchAssetFileIfNeeded(newLaunchAssetFile) |
| 145 | + } catch (e: Exception) { |
| 146 | + logger.error("Could not reset launchAssetFile for the ReactApplication", e, UpdatesErrorCode.Unknown) |
| 147 | + } |
| 148 | +@@ -84,7 +79,7 @@ class RelaunchProcedure( |
| 149 | + procedureScope.launch { |
| 150 | + withContext(Dispatchers.Main) { |
| 151 | + reloadScreenManager?.show(weakActivity?.get()) |
| 152 | +- reactApplication.restart(weakActivity?.get(), "Restart from RelaunchProcedure") |
| 153 | ++ RestartReactAppExtensions.restart(weakActivity?.get(), "Restart from RelaunchProcedure") |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | +@@ -127,13 +122,16 @@ class RelaunchProcedure( |
| 158 | + * [com.facebook.react.ReactInstanceManager]. |
| 159 | + */ |
| 160 | + private fun replaceLaunchAssetFileIfNeeded( |
| 161 | +- reactApplication: ReactApplication, |
| 162 | + launchAssetFile: String |
| 163 | + ) { |
| 164 | + if (ReactNativeFeatureFlags.enableBridgelessArchitecture) { |
| 165 | + return |
| 166 | + } |
| 167 | + |
| 168 | ++ val reactApplication = context as? ReactApplication ?: run inner@{ |
| 169 | ++ callback.onFailure(Exception("Could not reload application. Ensure you have passed the correct instance of ReactApplication into UpdatesController.initialize().")) |
| 170 | ++ return |
| 171 | ++ } |
| 172 | + val instanceManager = reactApplication.reactNativeHost.reactInstanceManager |
| 173 | +``` |
| 174 | + |
| 175 | +##### Step 4: |
| 176 | + |
| 177 | +In `expo-updates/android/**/RestartReactAppExtensions.kt` : |
| 178 | + |
| 179 | +```diff |
| 180 | +import com.facebook.react.ReactApplication |
| 181 | ++import com.facebook.react.ReactHost |
| 182 | ++import com.facebook.react.ReactNativeHost |
| 183 | + import com.facebook.react.common.LifecycleState |
| 184 | + import expo.modules.rncompatibility.ReactNativeFeatureFlags |
| 185 | ++import expo.modules.updates.UpdatesController |
| 186 | + |
| 187 | +-/** |
| 188 | +- * An extension for [ReactApplication] to restart the app |
| 189 | +- * |
| 190 | +- * @param activity For bridgeless mode if the ReactHost is destroyed, we need an Activity to resume it. |
| 191 | +- * @param reason The restart reason. Only used on bridgeless mode. |
| 192 | +- */ |
| 193 | +-internal fun ReactApplication.restart(activity: Activity?, reason: String) { |
| 194 | +- if (ReactNativeFeatureFlags.enableBridgelessArchitecture) { |
| 195 | +- val reactHost = this.reactHost |
| 196 | +- check(reactHost != null) |
| 197 | +- if (reactHost.lifecycleState != LifecycleState.RESUMED && activity != null) { |
| 198 | +- reactHost.onHostResume(activity) |
| 199 | ++private fun getReactHostReflectively(app: ReactApplication): ReactHost? { |
| 200 | ++ return runCatching { |
| 201 | ++ app.javaClass.getMethod("getReactHost").invoke(app) as? ReactHost |
| 202 | ++ }.getOrNull() |
| 203 | ++} |
| 204 | ++ |
| 205 | ++private fun getReactNativeHostReflectively(app: ReactApplication): ReactNativeHost? { |
| 206 | ++ return runCatching { |
| 207 | ++ app.javaClass.getMethod("getReactNativeHost").invoke(app) as? ReactNativeHost |
| 208 | ++ }.getOrNull() |
| 209 | ++} |
| 210 | ++ |
| 211 | ++object RestartReactAppExtensions { |
| 212 | ++ /** |
| 213 | ++ * A function to restart the app |
| 214 | ++ * |
| 215 | ++ * @param activity For bridgeless mode if the ReactHost is destroyed, we need an Activity to resume it. |
| 216 | ++ * @param reason The restart reason. Only used on bridgeless mode. |
| 217 | ++ */ |
| 218 | ++ fun restart(activity: Activity?, reason: String) { |
| 219 | ++ if (ReactNativeFeatureFlags.enableBridgelessArchitecture) { |
| 220 | ++ val reactHost = UpdatesController.getReactHost() ?: getReactHostReflectively(activity?.application as ReactApplication) |
| 221 | ++ check(reactHost != null) |
| 222 | ++ if (reactHost.lifecycleState != LifecycleState.RESUMED && activity != null) { |
| 223 | ++ reactHost.onHostResume(activity) |
| 224 | ++ } |
| 225 | ++ reactHost.reload(reason) |
| 226 | ++ return |
| 227 | + } |
| 228 | +- reactHost.reload(reason) |
| 229 | +- return |
| 230 | +- } |
| 231 | + |
| 232 | +- reactNativeHost.reactInstanceManager.recreateReactContextInBackground() |
| 233 | ++ getReactNativeHostReflectively(activity?.application as ReactApplication)?.reactInstanceManager?.recreateReactContextInBackground() |
| 234 | ++ } |
| 235 | +``` |
| 236 | + |
| 237 | +<hr/> |
| 238 | + |
| 239 | +To see a complete patch, please visit: |
| 240 | + |
| 241 | +- [SDK54](https://github.com/callstack/react-native-brownfield/tree/main/.yarn/patches/expo-updates-npm-29.0.16-1c5c89eb83.patch) |
0 commit comments