@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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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()
|