@otaupdate/react-native 1.0.0

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.
Files changed (57) hide show
  1. package/README.md +321 -0
  2. package/android/build.gradle +70 -0
  3. package/android/src/expo/java/com/otaupdate/OtaUpdateExpoPackage.kt +37 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/com/otaupdate/OtaUpdate.kt +98 -0
  6. package/android/src/main/java/com/otaupdate/OtaUpdateInstaller.kt +211 -0
  7. package/android/src/main/java/com/otaupdate/OtaUpdateModule.kt +278 -0
  8. package/android/src/main/java/com/otaupdate/OtaUpdatePackage.kt +15 -0
  9. package/android/src/main/java/com/otaupdate/OtaUpdateStore.kt +268 -0
  10. package/app.plugin.js +3 -0
  11. package/expo-module.config.json +6 -0
  12. package/ios/OtaUpdate.h +46 -0
  13. package/ios/OtaUpdate.m +302 -0
  14. package/ios/OtaUpdateInstaller.h +25 -0
  15. package/ios/OtaUpdateInstaller.m +278 -0
  16. package/ios/OtaUpdateStore.h +69 -0
  17. package/ios/OtaUpdateStore.m +283 -0
  18. package/lib/OtaUpdate.d.ts +28 -0
  19. package/lib/OtaUpdate.d.ts.map +1 -0
  20. package/lib/OtaUpdate.js +254 -0
  21. package/lib/OtaUpdate.js.map +1 -0
  22. package/lib/api.d.ts +36 -0
  23. package/lib/api.d.ts.map +1 -0
  24. package/lib/api.js +89 -0
  25. package/lib/api.js.map +1 -0
  26. package/lib/index.d.ts +24 -0
  27. package/lib/index.d.ts.map +1 -0
  28. package/lib/index.js +37 -0
  29. package/lib/index.js.map +1 -0
  30. package/lib/native.d.ts +34 -0
  31. package/lib/native.d.ts.map +1 -0
  32. package/lib/native.js +34 -0
  33. package/lib/native.js.map +1 -0
  34. package/lib/types.d.ts +112 -0
  35. package/lib/types.d.ts.map +1 -0
  36. package/lib/types.js +27 -0
  37. package/lib/types.js.map +1 -0
  38. package/lib/useOtaUpdate.d.ts +17 -0
  39. package/lib/useOtaUpdate.d.ts.map +1 -0
  40. package/lib/useOtaUpdate.js +81 -0
  41. package/lib/useOtaUpdate.js.map +1 -0
  42. package/lib/withOtaUpdate.d.ts +10 -0
  43. package/lib/withOtaUpdate.d.ts.map +1 -0
  44. package/lib/withOtaUpdate.js +21 -0
  45. package/lib/withOtaUpdate.js.map +1 -0
  46. package/package.json +54 -0
  47. package/plugin/build/index.d.ts +11 -0
  48. package/plugin/build/index.js +97 -0
  49. package/react-native-ota-update.podspec +43 -0
  50. package/react-native.config.js +21 -0
  51. package/src/OtaUpdate.ts +293 -0
  52. package/src/api.ts +122 -0
  53. package/src/index.ts +55 -0
  54. package/src/native.ts +64 -0
  55. package/src/types.ts +125 -0
  56. package/src/useOtaUpdate.ts +96 -0
  57. package/src/withOtaUpdate.tsx +22 -0
package/README.md ADDED
@@ -0,0 +1,321 @@
1
+ # @otaupdate/react-native
2
+
3
+ Over-the-air JS bundle updates for React Native. Ships the JS + assets of a
4
+ release to installed apps without going through the App Store or Play Store.
5
+
6
+ Works in **bare React Native** and **Expo** (via a config plugin). Native code
7
+ handles download, SHA-256 verification, unzip, bundle swap-in, and
8
+ rollback-on-failure; JS handles the update check and the app-facing API.
9
+
10
+ > **What OTA can and cannot ship.** Anything that lives in the JS bundle and its
11
+ > assets: screens, logic, images, styles. Nothing native: a new native module, a
12
+ > permission, an SDK version bump, or an `Info.plist` change still needs a store
13
+ > release. Shipping a JS bundle that calls a native API the installed binary
14
+ > doesn't have will crash the app — use `targetBinaryVersion` to keep those
15
+ > releases apart.
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @otaupdate/react-native
23
+ ```
24
+
25
+ ### Bare React Native
26
+
27
+ **iOS**
28
+
29
+ ```bash
30
+ cd ios && pod install
31
+ ```
32
+
33
+ Then point React Native at the OTA bundle. In `AppDelegate.swift` (RN 0.77+):
34
+
35
+ ```swift
36
+ import OtaUpdate
37
+
38
+ override func bundleURL() -> URL? {
39
+ #if DEBUG
40
+ return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
41
+ #else
42
+ return OtaUpdate.bundleURL() // <- was Bundle.main.url(forResource: "main", …)
43
+ #endif
44
+ }
45
+ ```
46
+
47
+ Or in `AppDelegate.mm` (RN 0.76 and earlier):
48
+
49
+ ```objc
50
+ #import <OtaUpdate/OtaUpdate.h>
51
+
52
+ - (NSURL *)getBundleURL {
53
+ #if DEBUG
54
+ return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
55
+ #else
56
+ return [OtaUpdate bundleURL]; // <- was [[NSBundle mainBundle] URLForResource:…]
57
+ #endif
58
+ }
59
+ ```
60
+
61
+ Add the configuration to `ios/<YourApp>/Info.plist`:
62
+
63
+ ```xml
64
+ <key>OtaDeploymentKey</key>
65
+ <string>YOUR_IOS_DEPLOYMENT_KEY</string>
66
+ <key>OtaServerUrl</key>
67
+ <string>https://ota.example.com</string>
68
+ ```
69
+
70
+ **Android**
71
+
72
+ Autolinking registers the module. Add the one override in
73
+ `android/app/src/main/java/.../MainApplication.kt`:
74
+
75
+ ```kotlin
76
+ import com.otaupdate.OtaUpdate
77
+
78
+ override val reactNativeHost: ReactNativeHost =
79
+ object : DefaultReactNativeHost(this) {
80
+ override fun getJSMainModuleName(): String = "index"
81
+
82
+ override fun getJSBundleFile(): String? =
83
+ OtaUpdate.getJSBundleFile(applicationContext) // <- add this
84
+ // …
85
+ }
86
+ ```
87
+
88
+ And the configuration in `android/app/src/main/res/values/strings.xml`:
89
+
90
+ ```xml
91
+ <string name="ota_deployment_key" translatable="false">YOUR_ANDROID_DEPLOYMENT_KEY</string>
92
+ <string name="ota_server_url" translatable="false">https://ota.example.com</string>
93
+ ```
94
+
95
+ ### Expo
96
+
97
+ Add the config plugin to `app.json`. There is nothing to patch by hand — the
98
+ plugin writes the configuration, and the bundle swap-in is wired up
99
+ automatically (see [how Expo differs](#how-expo-differs) below):
100
+
101
+ ```json
102
+ {
103
+ "expo": {
104
+ "plugins": [
105
+ [
106
+ "@otaupdate/react-native",
107
+ {
108
+ "serverUrl": "https://ota.example.com",
109
+ "iosDeploymentKey": "YOUR_IOS_DEPLOYMENT_KEY",
110
+ "androidDeploymentKey": "YOUR_ANDROID_DEPLOYMENT_KEY"
111
+ }
112
+ ]
113
+ ]
114
+ }
115
+ }
116
+ ```
117
+
118
+ ```bash
119
+ npx expo prebuild --clean # or just let EAS Build run it
120
+ ```
121
+
122
+ > The native module does not exist in **Expo Go**. Use a development build
123
+ > (`npx expo run:ios` / `eas build --profile development`). In Expo Go the SDK
124
+ > logs a warning and no-ops instead of crashing.
125
+
126
+ #### How Expo differs
127
+
128
+ Expo apps are **bridgeless**: `MainApplication` builds a `ReactHost` straight
129
+ from `ExpoReactHostFactory` and never exposes a `ReactNativeHost`, so there is
130
+ no `getJSBundleFile()` to override. Instead this package registers a
131
+ `ReactNativeHostHandler` — Expo's own extension point, the same one
132
+ `expo-updates` uses — which Expo asks for the bundle path at startup. Expo
133
+ autolinking discovers it; you don't wire anything up.
134
+
135
+ Two consequences worth knowing:
136
+
137
+ - The config plugin **does not modify `MainApplication`** on Android. If you go
138
+ looking for an edit there after `prebuild`, its absence is correct.
139
+ - The handler returns `null` when developer support is on, so a debug build
140
+ keeps loading from Metro and a downloaded bundle never shadows your live
141
+ reload.
142
+
143
+ This package is therefore linked by **both** linkers, and needs to be: React
144
+ Native autolinking registers `OtaUpdatePackage` (the native module JS calls),
145
+ while Expo autolinking registers `OtaUpdateExpoPackage` (the bundle path). Expo
146
+ normally drops a package from React Native autolinking when it is also an Expo
147
+ module with its own Gradle file, which would leave `NativeModules.OtaUpdate`
148
+ undefined — the shipped `react-native.config.js` opts out of that skip. Don't
149
+ delete it.
150
+
151
+ Verified against Expo SDK 57 / React Native 0.86 with the new architecture, on both platforms.
152
+
153
+ ## Platform verification status
154
+
155
+ **Android — verified end to end.** On an Expo SDK 57 / RN 0.86 release build
156
+ (new architecture, bridgeless) running on an emulator: publish → download →
157
+ SHA-256 verify → bundle swap on restart, `IMMEDIATE` in-process reload, and
158
+ rollback rescuing a device already on the bad build. Confirmed by screenshot,
159
+ native log, and server-side install reports.
160
+
161
+ **iOS — verified end to end.** On an Expo SDK 57 / RN 0.86 Release build
162
+ (Xcode 26, iPhone 17 Pro simulator, iOS 26.5): publish → download → SHA-256
163
+ verify → bundle swap on restart, `IMMEDIATE` in-process reload, and rollback
164
+ restoring the earlier bundle byte-for-byte. Confirmed by screenshot, package
165
+ hash and server-side install reports.
166
+
167
+ One library fix came out of that first real build: the podspec now sets
168
+ `DEFINES_MODULE = YES`. React Native and Expo build pods as static libraries,
169
+ and an Objective-C static library is not importable as a Swift module unless
170
+ CocoaPods emits a module map — without it every Expo app with a Swift
171
+ AppDelegate failed to compile with `no such module 'OtaUpdate'`, even though
172
+ the pod linked correctly and the Objective-C itself was fine.
173
+
174
+
175
+ ---
176
+
177
+ ## Use it
178
+
179
+ The one-liner — checks on app start and on every foreground, installs on next
180
+ restart:
181
+
182
+ ```tsx
183
+ import { withOtaUpdate } from '@otaupdate/react-native';
184
+
185
+ function App() {
186
+ return <YourApp />;
187
+ }
188
+
189
+ export default withOtaUpdate(App);
190
+ ```
191
+
192
+ That is enough for most apps. `withOtaUpdate` also calls `notifyAppReady()` on
193
+ mount, which is what arms rollback protection — see below.
194
+
195
+ ### Manual control
196
+
197
+ ```ts
198
+ import OtaUpdate, { InstallMode, SyncStatus } from '@otaupdate/react-native';
199
+
200
+ const status = await OtaUpdate.sync({
201
+ installMode: InstallMode.ON_NEXT_RESTART,
202
+ mandatoryInstallMode: InstallMode.IMMEDIATE,
203
+ onSyncStatusChange: (s) => console.log(SyncStatus[s]),
204
+ onDownloadProgress: ({ receivedBytes, totalBytes }) =>
205
+ console.log(`${Math.round((receivedBytes / totalBytes) * 100)}%`),
206
+ // Ask before installing. Mandatory releases ignore this.
207
+ shouldInstall: (update) => confirmWithUser(update.description),
208
+ });
209
+ ```
210
+
211
+ ### Update UI with the hook
212
+
213
+ ```tsx
214
+ import { useOtaUpdate, SyncStatus } from '@otaupdate/react-native';
215
+
216
+ function UpdateBanner() {
217
+ const { available, progress, isSyncing, check, update } = useOtaUpdate();
218
+
219
+ useEffect(() => { void check(); }, []);
220
+ if (!available) return null;
221
+
222
+ return (
223
+ <View>
224
+ <Text>Version {available.label} is available</Text>
225
+ {available.description ? <Text>{available.description}</Text> : null}
226
+ {progress ? (
227
+ <Text>{Math.round((progress.receivedBytes / progress.totalBytes) * 100)}%</Text>
228
+ ) : null}
229
+ <Button title="Update" onPress={() => update()} disabled={isSyncing} />
230
+ </View>
231
+ );
232
+ }
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Rollback protection — the part not to skip
238
+
239
+ An update that crashes on boot would be unrecoverable without this, so the SDK
240
+ treats every freshly installed bundle as unproven:
241
+
242
+ 1. `install()` marks the package **pending**.
243
+ 2. The next launch boots into it and flags it as *loading*.
244
+ 3. `notifyAppReady()` **confirms** it. From then on it is permanent.
245
+ 4. If the app launches again while the package is still *loading* — i.e. the
246
+ last run never reached `notifyAppReady()` — the native side reverts to the
247
+ previous bundle, blacklists the bad hash, and reports `rolled_back` to the
248
+ server so it shows up in the dashboard.
249
+
250
+ `withOtaUpdate` and `startAutoSync` call `notifyAppReady()` for you. If you wire
251
+ things up yourself, **you must call it**, after your critical startup path:
252
+
253
+ ```ts
254
+ import { notifyAppReady } from '@otaupdate/react-native';
255
+
256
+ useEffect(() => {
257
+ void notifyAppReady();
258
+ }, []);
259
+ ```
260
+
261
+ Call it too early and a crash a second later still counts as healthy; call it
262
+ too late (or never) and every update rolls itself back.
263
+
264
+ ---
265
+
266
+ ## Install modes
267
+
268
+ | Mode | When the new bundle loads |
269
+ |---|---|
270
+ | `ON_NEXT_RESTART` *(default)* | The next time the user cold-starts the app. Never interrupts. |
271
+ | `ON_NEXT_RESUME` | The next foreground, after `minimumBackgroundDuration` seconds in the background. |
272
+ | `IMMEDIATE` | Right away, with a JS reload. Reserved for mandatory fixes — it interrupts whatever the user was doing. |
273
+
274
+ ---
275
+
276
+ ## API
277
+
278
+ | Export | Purpose |
279
+ |---|---|
280
+ | `sync(options)` | Check → download → install. Concurrent calls share one run. |
281
+ | `checkForUpdate()` | Just the check. Returns `{ update, reason }`. |
282
+ | `notifyAppReady()` | Confirm the running bundle (see rollback above). |
283
+ | `restartApp(onlyIfPending?)` | Reload the JS bundle. |
284
+ | `getCurrentPackage()` | Running label, hash, pending/first-run flags. |
285
+ | `clearUpdates()` | Wipe all downloads, revert to the binary bundle. |
286
+ | `startAutoSync(options)` | The app-start/resume loop `withOtaUpdate` uses. |
287
+ | `withOtaUpdate(App, options)` | Root-component HOC. |
288
+ | `useOtaUpdate(options)` | Hook for update UI. |
289
+ | `isNativeModuleAvailable` | `false` in Expo Go and on web. |
290
+
291
+ `checkForUpdate()` returns a `reason` when there is no update — `up_to_date`,
292
+ `not_in_rollout`, `no_matching_binary_version`, or `update_app_version` (a
293
+ release exists but needs a newer native binary, i.e. prompt for a store update).
294
+
295
+ ---
296
+
297
+ ## Publishing
298
+
299
+ From the project root, with the [CLI](../cli/README.md):
300
+
301
+ ```bash
302
+ ota release-react MyApp-iOS 1.4.0 --platform ios -d Production -r 20
303
+ ```
304
+
305
+ `targetBinaryVersion` (`1.4.0` above) is the native app version the bundle is
306
+ compatible with. Devices on other binary versions never receive it.
307
+
308
+ ---
309
+
310
+ ## Troubleshooting
311
+
312
+ | Symptom | Cause |
313
+ |---|---|
314
+ | `The native module … is not available` | Native rebuild missing (`pod install` / Gradle), or running in Expo Go. A Metro reload is not enough. |
315
+ | Update downloads but never applies (bare RN) | `getJSBundleFile` / `bundleURL` override missing — the app keeps loading the bundle from the binary. |
316
+ | Update downloads but never applies (Expo) | The `ReactNativeHostHandler` was not autolinked. Re-run `npx expo prebuild --clean`; check `android/app/build/generated/autolinking` mentions `OtaUpdateExpoPackage`. |
317
+ | Download fails while the check succeeds | The presigned URL points at a host the device can't reach. Set `S3_PUBLIC_ENDPOINT` (and `PUBLIC_API_URL`) to a LAN IP or public hostname, not `localhost`. |
318
+ | `CLEARTEXT communication not permitted` | Android blocks plain HTTP from API 28. Use HTTPS, or `usesCleartextTraffic` for local testing only. |
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
+ | `no_matching_binary_version` | The release's `targetBinaryVersion` does not cover the installed native version. |
321
+ | Works in debug, not release | Debug builds load from Metro; the OTA path only runs in release builds. |
@@ -0,0 +1,70 @@
1
+ buildscript {
2
+ ext.safeExtGet = { prop, fallback ->
3
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
4
+ }
5
+ repositories {
6
+ google()
7
+ mavenCentral()
8
+ }
9
+ dependencies {
10
+ classpath("com.android.tools.build:gradle:8.6.0")
11
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.9.24')}")
12
+ }
13
+ }
14
+
15
+ apply plugin: "com.android.library"
16
+ apply plugin: "org.jetbrains.kotlin.android"
17
+
18
+ // Expo apps are bridgeless and have no ReactNativeHost to override, so the
19
+ // bundle path is supplied through Expo's ReactNativeHostHandler instead (see
20
+ // src/expo/java). That source set is only compiled when Expo is actually
21
+ // present — bare React Native projects have no expo-modules-core to link
22
+ // against and use the getJSBundleFile() override on ReactNativeHost.
23
+ def expoModulesCore = findProject(':expo-modules-core')
24
+ def isExpoProject = expoModulesCore != null
25
+
26
+ android {
27
+ namespace "com.otaupdate"
28
+ compileSdkVersion safeExtGet('compileSdkVersion', 34)
29
+
30
+ sourceSets {
31
+ main {
32
+ if (isExpoProject) {
33
+ java.srcDirs += 'src/expo/java'
34
+ }
35
+ }
36
+ }
37
+
38
+ defaultConfig {
39
+ minSdkVersion safeExtGet('minSdkVersion', 23)
40
+ targetSdkVersion safeExtGet('targetSdkVersion', 34)
41
+ }
42
+
43
+ compileOptions {
44
+ sourceCompatibility JavaVersion.VERSION_17
45
+ targetCompatibility JavaVersion.VERSION_17
46
+ }
47
+
48
+ kotlinOptions {
49
+ jvmTarget = "17"
50
+ }
51
+
52
+ lintOptions {
53
+ abortOnError false
54
+ }
55
+ }
56
+
57
+ repositories {
58
+ mavenCentral()
59
+ google()
60
+ }
61
+
62
+ dependencies {
63
+ implementation "com.facebook.react:react-android"
64
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion', '1.9.24')}"
65
+
66
+ // compileOnly: the host app already ships expo-modules-core at runtime.
67
+ if (isExpoProject) {
68
+ compileOnly project(':expo-modules-core')
69
+ }
70
+ }
@@ -0,0 +1,37 @@
1
+ package com.otaupdate
2
+
3
+ import android.content.Context
4
+ import expo.modules.core.interfaces.Package
5
+ import expo.modules.core.interfaces.ReactNativeHostHandler
6
+
7
+ /**
8
+ * Expo integration.
9
+ *
10
+ * Modern Expo apps are bridgeless: `MainApplication` builds a `ReactHost`
11
+ * straight from `ExpoReactHostFactory` and there is no `ReactNativeHost` whose
12
+ * `getJSBundleFile()` we could override. Expo's supported extension point for
13
+ * exactly this is `ReactNativeHostHandler` — the same one `expo-updates` uses —
14
+ * so we register one instead of rewriting the app's `MainApplication`.
15
+ *
16
+ * That also means the config plugin does not have to patch Kotlin source, which
17
+ * is what broke against the RN 0.86 template.
18
+ *
19
+ * Discovery: Expo autolinking scans a module's Android sources for files named
20
+ * `*Package.kt` that import `expo.modules.core.interfaces.Package`. This file
21
+ * only compiles when `:expo-modules-core` is on the classpath — see the
22
+ * conditional source set in `android/build.gradle`, which keeps bare React
23
+ * Native builds (no Expo) working.
24
+ */
25
+ class OtaUpdateExpoPackage : Package {
26
+ override fun createReactNativeHostHandlers(context: Context): List<ReactNativeHostHandler> =
27
+ listOf(
28
+ object : ReactNativeHostHandler {
29
+ override fun getJSBundleFile(useDeveloperSupport: Boolean): String? {
30
+ // Debug builds load from Metro. Handing back a downloaded bundle
31
+ // there would silently replace the developer's live-reloading code.
32
+ if (useDeveloperSupport) return null
33
+ return OtaUpdate.getJSBundleFile(context)
34
+ }
35
+ },
36
+ )
37
+ }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET" />
3
+ </manifest>
@@ -0,0 +1,98 @@
1
+ package com.otaupdate
2
+
3
+ import android.content.Context
4
+ import android.content.pm.PackageManager
5
+ import android.util.Log
6
+
7
+ /**
8
+ * Entry point used by the host app's `MainApplication`.
9
+ *
10
+ * In `ReactNativeHost`:
11
+ *
12
+ * override fun getJSBundleFile(): String? = OtaUpdate.getJSBundleFile(applicationContext)
13
+ *
14
+ * That single override is what makes updates take effect: React Native asks
15
+ * for the bundle path at start-up, and we hand back the most recent healthy
16
+ * downloaded bundle (or null, meaning "use the one in the APK").
17
+ */
18
+ object OtaUpdate {
19
+
20
+ private const val DEFAULT_ASSET_BUNDLE = "index.android.bundle"
21
+ private const val META_DEPLOYMENT_KEY = "com.otaupdate.DEPLOYMENT_KEY"
22
+ private const val META_SERVER_URL = "com.otaupdate.SERVER_URL"
23
+
24
+ @Volatile private var store: OtaUpdateStore? = null
25
+ @Volatile private var initialized = false
26
+
27
+ @JvmStatic
28
+ @Synchronized
29
+ fun store(context: Context): OtaUpdateStore =
30
+ store ?: OtaUpdateStore(context.applicationContext).also { store = it }
31
+
32
+ /**
33
+ * Resolves the JS bundle for this launch and advances the rollback state
34
+ * machine. Safe to call more than once; only the first call has effects.
35
+ */
36
+ @JvmStatic
37
+ fun getJSBundleFile(context: Context): String? = getJSBundleFile(context, DEFAULT_ASSET_BUNDLE)
38
+
39
+ @JvmStatic
40
+ @Synchronized
41
+ fun getJSBundleFile(context: Context, assetsBundleName: String): String? {
42
+ return try {
43
+ val s = store(context)
44
+ if (!initialized) {
45
+ initialized = true
46
+ s.initializeAfterRestart()
47
+ } else {
48
+ // Resolved again without a new process: an in-process reload from an
49
+ // IMMEDIATE or ON_NEXT_RESUME install. Promote the pending package,
50
+ // but do not let it look like a boot that failed to confirm.
51
+ s.promotePendingIfAny()
52
+ }
53
+ // null tells React Native to load `assets://index.android.bundle`.
54
+ s.currentBundlePath()
55
+ } catch (e: Exception) {
56
+ // Never let OTA state break app start-up.
57
+ Log.e(OtaUpdateStore.TAG, "failed to resolve OTA bundle; using the binary bundle", e)
58
+ null
59
+ }
60
+ }
61
+
62
+ // --- Build-time configuration --------------------------------------------
63
+
64
+ @JvmStatic
65
+ fun deploymentKey(context: Context): String =
66
+ stringResource(context, "ota_deployment_key")
67
+ ?: metaData(context, META_DEPLOYMENT_KEY)
68
+ ?: ""
69
+
70
+ @JvmStatic
71
+ fun serverUrl(context: Context): String =
72
+ stringResource(context, "ota_server_url") ?: metaData(context, META_SERVER_URL) ?: ""
73
+
74
+ @JvmStatic
75
+ fun appVersion(context: Context): String =
76
+ try {
77
+ context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "0.0.0"
78
+ } catch (e: PackageManager.NameNotFoundException) {
79
+ "0.0.0"
80
+ }
81
+
82
+ private fun stringResource(context: Context, name: String): String? {
83
+ val id = context.resources.getIdentifier(name, "string", context.packageName)
84
+ if (id == 0) return null
85
+ return context.getString(id).takeIf { it.isNotBlank() }
86
+ }
87
+
88
+ private fun metaData(context: Context, key: String): String? =
89
+ try {
90
+ val info = context.packageManager.getApplicationInfo(
91
+ context.packageName,
92
+ PackageManager.GET_META_DATA,
93
+ )
94
+ info.metaData?.getString(key)?.takeIf { it.isNotBlank() }
95
+ } catch (e: Exception) {
96
+ null
97
+ }
98
+ }