@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.
- package/README.md +135 -1
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/otaupdate/OTAUpdateModule.kt +116 -25
- package/app.plugin.js +247 -26
- package/ios/OTAUpdate.swift +29 -0
- package/lib/commonjs/OTAProvider.js +37 -0
- package/lib/commonjs/OTAProvider.js.map +1 -1
- package/lib/commonjs/components/OTADebugPanel.js +426 -0
- package/lib/commonjs/components/OTADebugPanel.js.map +1 -0
- package/lib/commonjs/hooks/useOTAUpdate.js +38 -2
- package/lib/commonjs/hooks/useOTAUpdate.js.map +1 -1
- package/lib/commonjs/index.js +10 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/storage.js +79 -4
- package/lib/commonjs/utils/storage.js.map +1 -1
- package/lib/module/OTAProvider.js +38 -1
- package/lib/module/OTAProvider.js.map +1 -1
- package/lib/module/components/OTADebugPanel.js +418 -0
- package/lib/module/components/OTADebugPanel.js.map +1 -0
- package/lib/module/hooks/useOTAUpdate.js +38 -2
- package/lib/module/hooks/useOTAUpdate.js.map +1 -1
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/storage.js +79 -4
- package/lib/module/utils/storage.js.map +1 -1
- package/lib/typescript/OTAProvider.d.ts.map +1 -1
- package/lib/typescript/components/OTADebugPanel.d.ts +18 -0
- package/lib/typescript/components/OTADebugPanel.d.ts.map +1 -0
- package/lib/typescript/hooks/useOTAUpdate.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/utils/storage.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/OTAProvider.tsx +40 -0
- package/src/components/OTADebugPanel.tsx +447 -0
- package/src/hooks/useOTAUpdate.ts +49 -2
- package/src/index.ts +4 -1
- 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
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
119
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
183
|
+
android.util.Log.w("OTAUpdate", "Error during cleanup: ${e.message}")
|
|
153
184
|
}
|
|
154
185
|
}
|
|
155
|
-
}
|
|
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
|
-
|
|
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@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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").
|
|
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
|
}
|