@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
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
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
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
package/ios/OtaUpdateStore.m
CHANGED
|
@@ -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;
|