@otaupdate/react-native 1.0.4 → 1.0.6

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
@@ -112,6 +112,34 @@ And the configuration in `android/app/src/main/res/values/strings.xml`:
112
112
  <string name="ota_server_url" translatable="false">https://ota.example.com</string>
113
113
  ```
114
114
 
115
+ **Android — newer (bridgeless) template.** Some RN versions generate a
116
+ `MainApplication.kt` with no `ReactNativeHost`/`getJSBundleFile()` at all —
117
+ it builds `reactHost` directly via `getDefaultReactHost(...)`. If yours looks
118
+ like this, use `OtaUpdate.createReactHost(...)` in its place instead of the
119
+ override above (requires `@otaupdate/react-native@1.0.5`+ — see the version
120
+ note under [Install](#install)):
121
+
122
+ ```kotlin
123
+ import com.otaupdate.OtaUpdate
124
+
125
+ override val reactHost: ReactHost by lazy {
126
+ OtaUpdate.createReactHost(
127
+ context = applicationContext,
128
+ packageList =
129
+ PackageList(this).packages.apply {
130
+ // add(MyReactNativePackage())
131
+ },
132
+ )
133
+ }
134
+ ```
135
+
136
+ This isn't just a drop-in replacement for wiring convenience — it's what
137
+ makes mandatory/resume installs apply with **no restart and no visible
138
+ flash**, the same way Expo's own update mechanism works. The plain
139
+ `getDefaultReactHost(...)` call only supports the safer-but-visible
140
+ process-restart behavior (see [Platform verification status](#platform-verification-status)).
141
+ The `strings.xml` configuration above is unchanged either way.
142
+
115
143
  ### Expo
116
144
 
117
145
  Add the config plugin to `app.json`. There is nothing to patch by hand — the
@@ -178,6 +206,21 @@ SHA-256 verify → bundle swap on restart, `IMMEDIATE` in-process reload, and
178
206
  rollback rescuing a device already on the bad build. Confirmed by screenshot,
179
207
  native log, and server-side install reports.
180
208
 
209
+ **Bare RN Android specifically** applies a mandatory/resume install one of
210
+ two ways, depending on how `MainApplication.kt` is wired (see the Android
211
+ install steps above). `getDefaultReactHost(...)`
212
+ (`@otaupdate/react-native@1.0.4`, RN 0.7x, or `1.0.5`+ without the
213
+ `createReactHost` change) restarts the process — verified against a real
214
+ bare RN 0.87 build on an emulator, logcat-traced end to end: publish →
215
+ download → SHA-256 verify → install → confirmed healthy, including catching
216
+ and fixing a real infinite-reload loop and a lost-update race along the way
217
+ (see the Troubleshooting table). `OtaUpdate.createReactHost(...)` (`1.0.5`+,
218
+ RN 0.80+) is designed to swap the bundle in place with no restart — its
219
+ underlying mechanism (`ReactHostImpl` re-reading the delegate's bundle
220
+ loader on every reload) was confirmed by decompiling the actual compiled RN
221
+ runtime, but the end-to-end in-place-swap path itself has not yet been
222
+ run on a real device. Treat it as promising but unverified until confirmed.
223
+
181
224
  **iOS — verified end to end.** On an Expo SDK 57 / RN 0.86 Release build
182
225
  (Xcode 26, iPhone 17 Pro simulator, iOS 26.5): publish → download → SHA-256
183
226
  verify → bundle swap on restart, `IMMEDIATE` in-process reload, and rollback
@@ -342,3 +385,5 @@ compatible with. Devices on other binary versions never receive it.
342
385
  | 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. |
343
386
  | 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. |
344
387
  | 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. |
388
+ | Android: a normal (non-mandatory) update applies once, then randomly reverts to an older release and re-downloads | Fixed in 1.0.6/1.0.7. `org.json`'s `optString(key)` returns the literal string `"null"` (not Kotlin `null`) when a saved field was JSON `null` — every time `currentHash`/`pendingHash` was genuinely unset and then reloaded from disk, it silently became the four-character string `"null"` instead of real `null`, which then got treated as a real (but non-existent) package and fell back to the binary bundle. This was present since the SDK's first release; iOS was never affected (its store type-checks instead of string-coercing). |
389
+ | Android: a release published with no description shows the literal text "null" | Fixed in 1.0.6/1.0.7. Same `org.json` `optString()` behavior as above, applied to the nullable `description` field. |
@@ -87,7 +87,12 @@ class OtaUpdateModule(private val reactContext: ReactApplicationContext) :
87
87
  val map = Arguments.createMap().apply {
88
88
  putString("packageHash", hash)
89
89
  putString("label", info.optString("label"))
90
- putString("description", info.optString("description").takeIf { it.isNotEmpty() })
90
+ // Not info.optString("description") org.json's optString() returns
91
+ // the literal string "null" (not Kotlin null) for a JSONObject.NULL
92
+ // value, and description is nullable (see OtaUpdateStore.recordPackage),
93
+ // so a release published with no description would show the text
94
+ // "null" instead of none.
95
+ putString("description", info.opt("description") as? String)
91
96
  putBoolean("isMandatory", info.optBoolean("isMandatory"))
92
97
  putString("bundlePath", info.optString("bundlePath"))
93
98
  putDouble("size", info.optLong("size").toDouble())
@@ -62,9 +62,20 @@ class OtaUpdateStore(private val context: Context) {
62
62
  if (!statusFile.exists()) return
63
63
  try {
64
64
  val json = JSONObject(statusFile.readText())
65
- currentHash = json.optString("currentHash").ifEmpty { null }
66
- lastConfirmedHash = json.optString("lastConfirmedHash").ifEmpty { null }
67
- pendingHash = json.optString("pendingHash").ifEmpty { null }
65
+ // NOT json.optString(key) — org.json's optString() returns the literal
66
+ // string "null" (not Kotlin null, not "") when the stored value is
67
+ // JSONObject.NULL, which `save()` below writes for every one of these
68
+ // fields whenever they're genuinely unset. That turned "no pending
69
+ // update" into a fake pending hash of "null" on every single load,
70
+ // which then got promoted into currentHash, resolved to no real
71
+ // package, and silently fell back to the binary bundle — the exact
72
+ // "reverts to an older app" symptom this was causing. opt(key) returns
73
+ // the raw value (String, JSONObject.NULL, or absent), so casting to
74
+ // String? correctly yields null for anything that isn't an actual
75
+ // string.
76
+ currentHash = json.opt("currentHash") as? String
77
+ lastConfirmedHash = json.opt("lastConfirmedHash") as? String
78
+ pendingHash = json.opt("pendingHash") as? String
68
79
  pendingIsLoading = json.optBoolean("pendingIsLoading", false)
69
80
 
70
81
  val failed = json.optJSONArray("failedHashes") ?: JSONArray()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otaupdate/react-native",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Over-the-air JS bundle updates for React Native \u2014 bare and Expo",
5
5
  "license": "MIT",
6
6
  "publishConfig": {