@otaupdate/react-native 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -319,3 +319,6 @@ compatible with. Devices on other binary versions never receive it.
319
319
  | Every update rolls back | `notifyAppReady()` is never reached — usually an early crash, or the call sits behind a screen the user has to navigate to. |
320
320
  | `no_matching_binary_version` | The release's `targetBinaryVersion` does not cover the installed native version. |
321
321
  | Works in debug, not release | Debug builds load from Metro; the OTA path only runs in release builds. |
322
+ | Android: mandatory update reload-loops (bare RN) | Fixed in 1.0.2. Before that, `IMMEDIATE`/`ON_NEXT_RESUME` tried an in-place JS reload that reused a bundle path fixed at process start, so the same mandatory update kept re-triggering forever. 1.0.2 does a real process restart instead — update to it. |
323
+ | A newer update never shows up, app stays on an older-than-expected release | Fixed in 1.0.3. `notifyApplicationReady()` used to clear *any* pending hash unconditionally — if a second update was downloaded and queued for a future restart while the first one was still being confirmed, its pending state was silently wiped and the device never advanced to it. |
324
+ | Android: old screen briefly visible during a mandatory/resume install | Improved in 1.0.3. The process-restart fix in 1.0.2 is correct but showed Android's default activity-transition animation (old screen sliding/fading out). 1.0.3 suppresses it via `overridePendingTransition(0, 0)` so the switch is instant. |
@@ -1,9 +1,9 @@
1
1
  package com.otaupdate
2
2
 
3
+ import android.content.Intent
3
4
  import android.os.Handler
4
5
  import android.os.Looper
5
6
  import android.util.Log
6
- import com.facebook.react.ReactApplication
7
7
  import com.facebook.react.bridge.Arguments
8
8
  import com.facebook.react.bridge.LifecycleEventListener
9
9
  import com.facebook.react.bridge.Promise
@@ -221,33 +221,53 @@ class OtaUpdateModule(private val reactContext: ReactApplicationContext) :
221
221
  }
222
222
 
223
223
  /**
224
- * Recreates the React context so the new bundle is loaded. Handles both the
225
- * bridge and bridgeless (new architecture) hosts; the bridgeless path is
226
- * reached reflectively so this module still compiles against RN 0.71.
224
+ * Restarts the whole process so the new bundle is loaded.
225
+ *
226
+ * This used to try an in-place JS-context reload (ReactHost.reload() via
227
+ * reflection on the new architecture, recreateReactContextInBackground() as
228
+ * a bridge-mode fallback) — but on Android BOTH of those reuse a bundle
229
+ * loader/path that was fixed once at ReactInstanceManager/ReactHost
230
+ * construction time (see ReactInstanceManager#mBundleLoader, a `final`
231
+ * field, and DefaultReactHost's internal singleton caching). Neither ever
232
+ * re-consults MainApplication's getJSBundleFile()/jsBundleFilePath, so an
233
+ * IMMEDIATE or ON_NEXT_RESUME install silently kept reloading the OLD
234
+ * bundle forever — for a mandatory update this produced an infinite
235
+ * reload loop, since the freshly-reloaded old bundle immediately saw the
236
+ * same "mandatory update available" response and tried to install again.
237
+ *
238
+ * A genuine process kill + relaunch is the only reliable way to force
239
+ * MainApplication to re-run from scratch and pick up the new bundle path —
240
+ * the same technique restart libraries like react-native-restart use.
241
+ * iOS does not need this: RCTTriggerReloadCommandListeners re-queries
242
+ * bundleURL() fresh on every reload, so it was never affected.
227
243
  */
228
244
  private fun reload() {
229
- val application = reactContext.applicationContext
230
- if (application !is ReactApplication) {
231
- Log.e(OtaUpdateStore.TAG, "Application does not implement ReactApplication — cannot restart")
232
- return
233
- }
234
-
245
+ val context = reactContext.applicationContext
235
246
  try {
236
- val reactHost = runCatching {
237
- ReactApplication::class.java.getMethod("getReactHost").invoke(application)
238
- }.getOrNull()
239
-
240
- if (reactHost != null) {
241
- reactHost.javaClass.methods
242
- .firstOrNull { it.name == "reload" && it.parameterTypes.size == 1 }
243
- ?.invoke(reactHost, "OTA update applied")
244
- ?: Log.e(OtaUpdateStore.TAG, "ReactHost has no reload(String) method")
245
- return
247
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
248
+ ?: throw IllegalStateException("no launch intent for ${context.packageName}")
249
+ val restartIntent = Intent.makeRestartActivityTask(launchIntent.component)
250
+
251
+ // Starting the restart from the current Activity (when one is attached)
252
+ // instead of the bare application context lets us suppress the default
253
+ // activity-transition animation right after otherwise Android
254
+ // animates the old screen sliding/fading out while the new one comes
255
+ // in, so the old content stays visibly on screen for the transition's
256
+ // duration. overridePendingTransition(0, 0) makes the switch instant
257
+ // instead. Falls back to starting from the application context if no
258
+ // activity is currently attached — the restart still works, just with
259
+ // the default (visible) transition.
260
+ val activity = reactContext.currentActivity
261
+ if (activity != null) {
262
+ activity.startActivity(restartIntent)
263
+ @Suppress("DEPRECATION")
264
+ activity.overridePendingTransition(0, 0)
265
+ } else {
266
+ context.startActivity(restartIntent)
246
267
  }
247
-
248
- application.reactNativeHost.reactInstanceManager.recreateReactContextInBackground()
268
+ Runtime.getRuntime().exit(0)
249
269
  } catch (e: Exception) {
250
- Log.e(OtaUpdateStore.TAG, "failed to restart the React context", e)
270
+ Log.e(OtaUpdateStore.TAG, "failed to restart the app process", e)
251
271
  }
252
272
  }
253
273
 
@@ -147,10 +147,21 @@ class OtaUpdateStore(private val context: Context) {
147
147
  return true
148
148
  }
149
149
 
150
- /** Confirms the running bundle. After this it can never be rolled back. */
150
+ /**
151
+ * Confirms the running bundle. After this it can never be rolled back.
152
+ *
153
+ * Only confirms when `pendingIsLoading` is true — meaning this session
154
+ * actually booted into `pendingHash` via `promotePendingIfAny()`. A
155
+ * `pendingHash` set by `markPending()` describes a *different*, newer
156
+ * package downloaded and queued for a *future* restart, not something
157
+ * running right now. Clearing it here — as an earlier version of this
158
+ * method did unconditionally — silently discarded that queued update: the
159
+ * next restart would find nothing pending and keep loading the older
160
+ * package, i.e. "the newly downloaded update never shows up."
161
+ */
151
162
  @Synchronized
152
163
  fun notifyApplicationReady() {
153
- if (pendingHash == null) return
164
+ if (pendingHash == null || !pendingIsLoading) return
154
165
  Log.i(TAG, "update ${pendingHash} confirmed healthy")
155
166
  lastConfirmedHash = currentHash
156
167
  pendingHash = null
@@ -143,9 +143,19 @@ static NSString *const kLogTag = @"[OtaUpdate]";
143
143
  }
144
144
  }
145
145
 
146
+ /**
147
+ * Confirms the running bundle. After this it can never be rolled back.
148
+ *
149
+ * Only confirms when pendingIsLoading is YES — meaning this session actually
150
+ * booted into pendingHash via promotePendingIfAny. A pendingHash set by
151
+ * markPending: describes a *different*, newer package downloaded and queued
152
+ * for a *future* restart, not something running right now. Clearing it here
153
+ * unconditionally (as an earlier version of this method did) silently
154
+ * discarded that queued update.
155
+ */
146
156
  - (void)notifyApplicationReady {
147
157
  @synchronized(self) {
148
- if (!self.pendingHash) return;
158
+ if (!self.pendingHash || !self.pendingIsLoading) return;
149
159
  NSLog(@"%@ update %@ confirmed healthy", kLogTag, self.pendingHash);
150
160
  self.lastConfirmedHash = self.currentHash;
151
161
  self.pendingHash = nil;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otaupdate/react-native",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Over-the-air JS bundle updates for React Native \u2014 bare and Expo",
5
5
  "license": "MIT",
6
6
  "publishConfig": {