@vanikya/ota-react-native 0.1.9 → 0.2.2

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 (38) hide show
  1. package/README.md +135 -1
  2. package/android/src/main/AndroidManifest.xml +2 -0
  3. package/android/src/main/java/com/otaupdate/OTAUpdateModule.kt +116 -25
  4. package/app.plugin.js +247 -26
  5. package/ios/OTAUpdate.swift +29 -0
  6. package/lib/commonjs/OTAProvider.js +37 -0
  7. package/lib/commonjs/OTAProvider.js.map +1 -1
  8. package/lib/commonjs/components/OTADebugPanel.js +426 -0
  9. package/lib/commonjs/components/OTADebugPanel.js.map +1 -0
  10. package/lib/commonjs/hooks/useOTAUpdate.js +38 -2
  11. package/lib/commonjs/hooks/useOTAUpdate.js.map +1 -1
  12. package/lib/commonjs/index.js +10 -1
  13. package/lib/commonjs/index.js.map +1 -1
  14. package/lib/commonjs/utils/storage.js +79 -4
  15. package/lib/commonjs/utils/storage.js.map +1 -1
  16. package/lib/module/OTAProvider.js +38 -1
  17. package/lib/module/OTAProvider.js.map +1 -1
  18. package/lib/module/components/OTADebugPanel.js +418 -0
  19. package/lib/module/components/OTADebugPanel.js.map +1 -0
  20. package/lib/module/hooks/useOTAUpdate.js +38 -2
  21. package/lib/module/hooks/useOTAUpdate.js.map +1 -1
  22. package/lib/module/index.js +4 -1
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/utils/storage.js +79 -4
  25. package/lib/module/utils/storage.js.map +1 -1
  26. package/lib/typescript/OTAProvider.d.ts.map +1 -1
  27. package/lib/typescript/components/OTADebugPanel.d.ts +18 -0
  28. package/lib/typescript/components/OTADebugPanel.d.ts.map +1 -0
  29. package/lib/typescript/hooks/useOTAUpdate.d.ts.map +1 -1
  30. package/lib/typescript/index.d.ts +2 -1
  31. package/lib/typescript/index.d.ts.map +1 -1
  32. package/lib/typescript/utils/storage.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/OTAProvider.tsx +40 -0
  35. package/src/components/OTADebugPanel.tsx +447 -0
  36. package/src/hooks/useOTAUpdate.ts +49 -2
  37. package/src/index.ts +4 -1
  38. package/src/utils/storage.ts +105 -4
package/README.md CHANGED
@@ -46,12 +46,101 @@ eas build --platform ios
46
46
 
47
47
  ### For bare React Native apps
48
48
 
49
- Install pods (iOS):
49
+ 1. Install pods (iOS):
50
50
 
51
51
  ```bash
52
52
  cd ios && pod install
53
53
  ```
54
54
 
55
+ 2. **IMPORTANT**: You must manually configure native code to load OTA bundles. Without this, downloaded bundles will never be applied.
56
+
57
+ #### Android Setup (MainApplication.kt)
58
+
59
+ Add the `getJSBundleFile()` override inside your `ReactNativeHost`:
60
+
61
+ ```kotlin
62
+ import android.content.SharedPreferences
63
+ import java.io.File
64
+
65
+ // Inside your MainApplication class, find the ReactNativeHost and add:
66
+ override fun getJSBundleFile(): String? {
67
+ val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
68
+ val bundlePath = prefs.getString("BundlePath", null)
69
+ if (bundlePath != null && File(bundlePath).exists()) {
70
+ return bundlePath
71
+ }
72
+ return null // Falls back to default bundle
73
+ }
74
+ ```
75
+
76
+ Example full `MainApplication.kt`:
77
+
78
+ ```kotlin
79
+ package com.yourapp
80
+
81
+ import android.app.Application
82
+ import android.content.SharedPreferences
83
+ import java.io.File
84
+ import com.facebook.react.PackageList
85
+ import com.facebook.react.ReactApplication
86
+ import com.facebook.react.ReactHost
87
+ import com.facebook.react.ReactNativeHost
88
+ import com.facebook.react.defaults.DefaultReactNativeHost
89
+
90
+ class MainApplication : Application(), ReactApplication {
91
+
92
+ override val reactNativeHost: ReactNativeHost =
93
+ object : DefaultReactNativeHost(this) {
94
+ // ADD THIS METHOD for OTA updates
95
+ override fun getJSBundleFile(): String? {
96
+ val prefs: SharedPreferences = applicationContext.getSharedPreferences("OTAUpdate", android.content.Context.MODE_PRIVATE)
97
+ val bundlePath = prefs.getString("BundlePath", null)
98
+ if (bundlePath != null && File(bundlePath).exists()) {
99
+ return bundlePath
100
+ }
101
+ return null
102
+ }
103
+
104
+ override fun getPackages() = PackageList(this).packages
105
+ override fun getJSMainModuleName() = "index"
106
+ override fun getUseDeveloperSupport() = BuildConfig.DEBUG
107
+ override val isNewArchEnabled = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
108
+ override val isHermesEnabled = BuildConfig.IS_HERMES_ENABLED
109
+ }
110
+
111
+ // ... rest of your MainApplication
112
+ }
113
+ ```
114
+
115
+ #### iOS Setup (AppDelegate.swift)
116
+
117
+ Modify `bundleURL()` to check for OTA bundles first:
118
+
119
+ ```swift
120
+ // Add this helper function inside AppDelegate class
121
+ private func getOTABundleURL() -> URL? {
122
+ if let bundlePath = UserDefaults.standard.string(forKey: "OTAUpdateBundlePath") {
123
+ if FileManager.default.fileExists(atPath: bundlePath) {
124
+ return URL(fileURLWithPath: bundlePath)
125
+ }
126
+ }
127
+ return nil
128
+ }
129
+
130
+ // Modify or add this bundleURL function
131
+ func bundleURL() -> URL? {
132
+ // Check for downloaded OTA bundle first
133
+ if let otaBundle = getOTABundleURL() {
134
+ return otaBundle
135
+ }
136
+ #if DEBUG
137
+ return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
138
+ #else
139
+ return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
140
+ #endif
141
+ }
142
+ ```
143
+
55
144
  ## Quick Start
56
145
 
57
146
  ### 1. Wrap your app with OTAProvider
@@ -339,6 +428,51 @@ async function recoverFromCorruptedUpdate() {
339
428
 
340
429
  The SDK now validates downloaded bundles to prevent HTML error pages or corrupted files from being saved as bundles.
341
430
 
431
+ ## Troubleshooting
432
+
433
+ ### Update downloads but doesn't apply after restart
434
+
435
+ This is usually because the native code to load OTA bundles is missing. Check:
436
+
437
+ 1. **For Expo apps**: Make sure you rebuilt the app with EAS Build after adding the plugin. OTA updates do NOT work in Expo Go.
438
+
439
+ 2. **For bare React Native**: Make sure you added the native code changes described in the "For bare React Native apps" section above.
440
+
441
+ 3. **Verify native code was injected**: During the EAS build, look for these log messages:
442
+ - `[OTAUpdate] Android: Successfully injected getJSBundleFile`
443
+ - `[OTAUpdate] iOS: Successfully modified bundleURL`
444
+
445
+ 4. **Check if bundle path is saved**: The path is stored in:
446
+ - Android: SharedPreferences key `"BundlePath"` in `"OTAUpdate"` preferences
447
+ - iOS: UserDefaults key `"OTAUpdateBundlePath"`
448
+
449
+ ### Bundle downloads but hash verification fails
450
+
451
+ 1. Make sure your server is returning the actual JavaScript bundle, not an HTML error page
452
+ 2. Check that the bundle wasn't corrupted during upload
453
+ 3. Try re-publishing the release with `ota release`
454
+
455
+ ### App crashes after OTA update
456
+
457
+ Call `clearCorruptedBundle()` on app startup to recover:
458
+
459
+ ```tsx
460
+ import { UpdateStorage } from '@vanikya/ota-react-native';
461
+
462
+ // In your App.tsx or entry point
463
+ useEffect(() => {
464
+ const storage = new UpdateStorage();
465
+ storage.clearCorruptedBundle();
466
+ }, []);
467
+ ```
468
+
469
+ ### Debugging
470
+
471
+ Enable debug logs by checking `__DEV__` console output. The SDK logs:
472
+ - `[OTAUpdate] Bundle registered with native module`
473
+ - `[OTAUpdate] Bundle verified successfully`
474
+ - `[OTAUpdate] Update ready`
475
+
342
476
  ## License
343
477
 
344
478
  MIT
@@ -1,3 +1,5 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <!-- Internet permission is required for downloading OTA bundles -->
4
+ <uses-permission android:name="android.permission.INTERNET" />
3
5
  </manifest>
@@ -2,6 +2,8 @@ package com.otaupdate
2
2
 
3
3
  import android.content.Context
4
4
  import android.content.SharedPreferences
5
+ import android.os.Handler
6
+ import android.os.Looper
5
7
  import android.util.Base64
6
8
  import com.facebook.react.bridge.*
7
9
  import java.io.File
@@ -10,6 +12,7 @@ import java.io.InputStream
10
12
  import java.net.HttpURLConnection
11
13
  import java.net.URL
12
14
  import java.security.MessageDigest
15
+ import java.util.concurrent.Executors
13
16
 
14
17
  class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
15
18
 
@@ -17,6 +20,12 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
17
20
  reactContext.getSharedPreferences("OTAUpdate", Context.MODE_PRIVATE)
18
21
  }
19
22
 
23
+ // Use a thread pool for background operations instead of raw threads
24
+ private val executor = Executors.newFixedThreadPool(2)
25
+
26
+ // Handler to post results back to the main thread
27
+ private val mainHandler = Handler(Looper.getMainLooper())
28
+
20
29
  override fun getName(): String = "OTAUpdate"
21
30
 
22
31
  // File System Operations
@@ -100,25 +109,33 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
100
109
  // This is critical for large bundles (5MB+)
101
110
  @ReactMethod
102
111
  fun downloadFile(urlString: String, destPath: String, promise: Promise) {
103
- Thread {
112
+ executor.execute {
104
113
  var connection: HttpURLConnection? = null
105
114
  var inputStream: InputStream? = null
106
115
  var outputStream: FileOutputStream? = null
107
116
 
108
117
  try {
118
+ android.util.Log.d("OTAUpdate", "Starting download from: $urlString to: $destPath")
119
+
109
120
  val url = URL(urlString)
110
121
  connection = url.openConnection() as HttpURLConnection
111
122
  connection.connectTimeout = 30000
112
- connection.readTimeout = 60000
123
+ connection.readTimeout = 120000 // Increased read timeout for large files
113
124
  connection.requestMethod = "GET"
125
+ connection.setRequestProperty("Accept-Encoding", "identity") // Disable compression for reliable streaming
114
126
  connection.connect()
115
127
 
116
128
  val responseCode = connection.responseCode
117
129
  if (responseCode != HttpURLConnection.HTTP_OK) {
118
- promise.reject("DOWNLOAD_ERROR", "Download failed with status $responseCode")
119
- return@Thread
130
+ val errorMsg = "Download failed with status $responseCode"
131
+ android.util.Log.e("OTAUpdate", errorMsg)
132
+ mainHandler.post { promise.reject("DOWNLOAD_ERROR", errorMsg) }
133
+ return@execute
120
134
  }
121
135
 
136
+ val contentLength = connection.contentLengthLong
137
+ android.util.Log.d("OTAUpdate", "Content-Length: $contentLength bytes")
138
+
122
139
  // Ensure parent directory exists
123
140
  val destFile = File(destPath)
124
141
  destFile.parentFile?.mkdirs()
@@ -137,22 +154,36 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
137
154
 
138
155
  outputStream.flush()
139
156
 
157
+ // Verify written file size
158
+ val writtenSize = destFile.length()
159
+ android.util.Log.d("OTAUpdate", "Download complete: $totalBytesRead bytes read, $writtenSize bytes written")
160
+
161
+ if (contentLength > 0 && writtenSize != contentLength) {
162
+ val errorMsg = "File size mismatch: expected $contentLength, got $writtenSize"
163
+ android.util.Log.e("OTAUpdate", errorMsg)
164
+ destFile.delete()
165
+ mainHandler.post { promise.reject("DOWNLOAD_ERROR", errorMsg) }
166
+ return@execute
167
+ }
168
+
140
169
  val result = Arguments.createMap()
141
170
  result.putDouble("fileSize", totalBytesRead.toDouble())
142
- promise.resolve(result)
171
+ // Resolve promise on main thread to avoid React Native bridge issues
172
+ mainHandler.post { promise.resolve(result) }
143
173
 
144
174
  } catch (e: Exception) {
145
- promise.reject("DOWNLOAD_ERROR", "Failed to download file: ${e.message}", e)
175
+ android.util.Log.e("OTAUpdate", "Download failed: ${e.message}", e)
176
+ mainHandler.post { promise.reject("DOWNLOAD_ERROR", "Failed to download file: ${e.message}", e) }
146
177
  } finally {
147
178
  try {
148
179
  inputStream?.close()
149
180
  outputStream?.close()
150
181
  connection?.disconnect()
151
182
  } catch (e: Exception) {
152
- // Ignore cleanup errors
183
+ android.util.Log.w("OTAUpdate", "Error during cleanup: ${e.message}")
153
184
  }
154
185
  }
155
- }.start()
186
+ }
156
187
  }
157
188
 
158
189
  // Cryptography
@@ -174,14 +205,16 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
174
205
  // Critical for large bundles (5MB+)
175
206
  @ReactMethod
176
207
  fun calculateSHA256FromFile(filePath: String, promise: Promise) {
177
- Thread {
208
+ executor.execute {
178
209
  try {
179
210
  val file = File(filePath)
180
211
  if (!file.exists()) {
181
- promise.reject("FILE_ERROR", "File not found: $filePath")
182
- return@Thread
212
+ mainHandler.post { promise.reject("FILE_ERROR", "File not found: $filePath") }
213
+ return@execute
183
214
  }
184
215
 
216
+ android.util.Log.d("OTAUpdate", "Calculating hash for: $filePath (${file.length()} bytes)")
217
+
185
218
  val digest = MessageDigest.getInstance("SHA-256")
186
219
  val buffer = ByteArray(8192) // 8KB buffer
187
220
  var bytesRead: Int
@@ -194,11 +227,14 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
194
227
 
195
228
  val hash = digest.digest()
196
229
  val hexString = hash.joinToString("") { "%02x".format(it) }
197
- promise.resolve(hexString)
230
+ android.util.Log.d("OTAUpdate", "Hash calculated: $hexString")
231
+ // Resolve promise on main thread
232
+ mainHandler.post { promise.resolve(hexString) }
198
233
  } catch (e: Exception) {
199
- promise.reject("HASH_ERROR", "Failed to calculate hash: ${e.message}", e)
234
+ android.util.Log.e("OTAUpdate", "Hash calculation failed: ${e.message}", e)
235
+ mainHandler.post { promise.reject("HASH_ERROR", "Failed to calculate hash: ${e.message}", e) }
200
236
  }
201
- }.start()
237
+ }
202
238
  }
203
239
 
204
240
  @ReactMethod
@@ -229,21 +265,69 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
229
265
  @ReactMethod
230
266
  fun applyBundle(bundlePath: String, restart: Boolean, promise: Promise) {
231
267
  try {
268
+ // Validate bundle file exists before storing path
269
+ val bundleFile = File(bundlePath)
270
+ if (!bundleFile.exists()) {
271
+ promise.reject("APPLY_ERROR", "Bundle file does not exist: $bundlePath")
272
+ return
273
+ }
274
+ if (!bundleFile.canRead()) {
275
+ promise.reject("APPLY_ERROR", "Bundle file is not readable: $bundlePath")
276
+ return
277
+ }
278
+ if (bundleFile.length() < 100) {
279
+ promise.reject("APPLY_ERROR", "Bundle file is too small (likely corrupted): ${bundleFile.length()} bytes")
280
+ return
281
+ }
282
+
283
+ // Log for debugging
284
+ android.util.Log.d("OTAUpdate", "Applying bundle: $bundlePath (${bundleFile.length()} bytes)")
285
+
232
286
  // Store the bundle path for next launch
233
- prefs.edit().putString("BundlePath", bundlePath).apply()
287
+ // CRITICAL: Use commit() instead of apply() to ensure synchronous write
288
+ // This prevents a race condition where the app kills before the write completes
289
+ val success = prefs.edit().putString("BundlePath", bundlePath).commit()
290
+ if (!success) {
291
+ android.util.Log.e("OTAUpdate", "Failed to save bundle path to SharedPreferences")
292
+ promise.reject("APPLY_ERROR", "Failed to save bundle path")
293
+ return
294
+ }
295
+ android.util.Log.d("OTAUpdate", "Bundle path saved to SharedPreferences: $bundlePath")
296
+
297
+ // Verify the path was actually saved
298
+ val savedPath = prefs.getString("BundlePath", null)
299
+ if (savedPath != bundlePath) {
300
+ android.util.Log.e("OTAUpdate", "Bundle path verification failed: expected $bundlePath, got $savedPath")
301
+ promise.reject("APPLY_ERROR", "Bundle path verification failed")
302
+ return
303
+ }
234
304
 
235
305
  if (restart) {
236
- // Restart the app
237
- val context = reactApplicationContext
238
- val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
239
- intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP)
240
- intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
241
- context.startActivity(intent)
242
- android.os.Process.killProcess(android.os.Process.myPid())
306
+ android.util.Log.d("OTAUpdate", "Restarting app to apply bundle...")
307
+
308
+ // Resolve promise before restarting so JS knows it succeeded
309
+ promise.resolve(null)
310
+
311
+ // Give a small delay to ensure the promise is sent back to JS
312
+ mainHandler.postDelayed({
313
+ // Restart the app
314
+ val context = reactApplicationContext
315
+ val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
316
+ intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP)
317
+ intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
318
+ intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK)
319
+ context.startActivity(intent)
320
+
321
+ // Small delay before killing to allow activity to start
322
+ mainHandler.postDelayed({
323
+ android.os.Process.killProcess(android.os.Process.myPid())
324
+ }, 100)
325
+ }, 100)
326
+ } else {
327
+ promise.resolve(null)
243
328
  }
244
-
245
- promise.resolve(null)
246
329
  } catch (e: Exception) {
330
+ android.util.Log.e("OTAUpdate", "Failed to apply bundle: ${e.message}", e)
247
331
  promise.reject("APPLY_ERROR", "Failed to apply bundle: ${e.message}", e)
248
332
  }
249
333
  }
@@ -256,7 +340,8 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
256
340
 
257
341
  @ReactMethod
258
342
  fun clearPendingBundle(promise: Promise) {
259
- prefs.edit().remove("BundlePath").apply()
343
+ prefs.edit().remove("BundlePath").commit()
344
+ android.util.Log.d("OTAUpdate", "Pending bundle cleared")
260
345
  promise.resolve(null)
261
346
  }
262
347
 
@@ -273,6 +358,12 @@ class OTAUpdateModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
273
358
  return data
274
359
  }
275
360
 
361
+ // Cleanup executor when module is destroyed
362
+ override fun onCatalystInstanceDestroy() {
363
+ super.onCatalystInstanceDestroy()
364
+ executor.shutdown()
365
+ }
366
+
276
367
  companion object {
277
368
  const val NAME = "OTAUpdate"
278
369
  }