@vexor-push/react-native-code-push 0.2.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/LICENSE +20 -0
  2. package/README.md +61 -0
  3. package/android/build.gradle +129 -0
  4. package/android/generated/java/com/vexorpush/codepush/NativeVexorCodePushSpec.java +91 -0
  5. package/android/generated/jni/CMakeLists.txt +36 -0
  6. package/android/generated/jni/RNVexorCodePushSpec-generated.cpp +110 -0
  7. package/android/generated/jni/RNVexorCodePushSpec.h +31 -0
  8. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ComponentDescriptors.cpp +22 -0
  9. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ComponentDescriptors.h +24 -0
  10. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/EventEmitters.cpp +16 -0
  11. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/EventEmitters.h +17 -0
  12. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/Props.cpp +19 -0
  13. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/Props.h +18 -0
  14. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/RNVexorCodePushSpecJSI-generated.cpp +129 -0
  15. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/RNVexorCodePushSpecJSI.h +184 -0
  16. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ShadowNodes.cpp +17 -0
  17. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ShadowNodes.h +23 -0
  18. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/States.cpp +16 -0
  19. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/States.h +19 -0
  20. package/android/gradle.properties +5 -0
  21. package/android/src/main/AndroidManifest.xml +3 -0
  22. package/android/src/main/AndroidManifestNew.xml +2 -0
  23. package/android/src/main/java/com/vexorpush/codepush/CrashHandler.kt +78 -0
  24. package/android/src/main/java/com/vexorpush/codepush/SharedPrefs.kt +60 -0
  25. package/android/src/main/java/com/vexorpush/codepush/Utils.kt +105 -0
  26. package/android/src/main/java/com/vexorpush/codepush/VexorCodePush.kt +100 -0
  27. package/android/src/main/java/com/vexorpush/codepush/VexorCodePushModule.kt +698 -0
  28. package/android/src/newarch/VexorCodePushSpec.kt +7 -0
  29. package/android/src/oldarch/VexorCodePushSpec.kt +24 -0
  30. package/app.plugin.js +1 -0
  31. package/current-version.js +27 -0
  32. package/deployment-state.js +33 -0
  33. package/index.d.ts +101 -0
  34. package/index.js +535 -0
  35. package/ios/VexorCodePush.h +13 -0
  36. package/ios/VexorCodePush.mm +949 -0
  37. package/ios/generated/RNVexorCodePushSpec/ComponentDescriptors.cpp +22 -0
  38. package/ios/generated/RNVexorCodePushSpec/ComponentDescriptors.h +24 -0
  39. package/ios/generated/RNVexorCodePushSpec/EventEmitters.cpp +16 -0
  40. package/ios/generated/RNVexorCodePushSpec/EventEmitters.h +17 -0
  41. package/ios/generated/RNVexorCodePushSpec/Props.cpp +19 -0
  42. package/ios/generated/RNVexorCodePushSpec/Props.h +18 -0
  43. package/ios/generated/RNVexorCodePushSpec/RCTComponentViewHelpers.h +18 -0
  44. package/ios/generated/RNVexorCodePushSpec/RNVexorCodePushSpec-generated.mm +130 -0
  45. package/ios/generated/RNVexorCodePushSpec/RNVexorCodePushSpec.h +109 -0
  46. package/ios/generated/RNVexorCodePushSpec/ShadowNodes.cpp +17 -0
  47. package/ios/generated/RNVexorCodePushSpec/ShadowNodes.h +23 -0
  48. package/ios/generated/RNVexorCodePushSpec/States.cpp +16 -0
  49. package/ios/generated/RNVexorCodePushSpec/States.h +19 -0
  50. package/ios/generated/RNVexorCodePushSpecJSI-generated.cpp +129 -0
  51. package/ios/generated/RNVexorCodePushSpecJSI.h +184 -0
  52. package/package.json +51 -0
  53. package/plugin/src/index.ts +92 -0
  54. package/plugin/tsconfig.json +9 -0
  55. package/react-native-code-push.podspec +48 -0
  56. package/react-native.config.js +13 -0
  57. package/src/NativeVexorCodePush.ts +21 -0
@@ -0,0 +1,698 @@
1
+ package com.vexorpush.codepush
2
+
3
+ import android.content.Context
4
+ import com.facebook.react.bridge.Promise
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.ReactMethod
7
+ import com.jakewharton.processphoenix.ProcessPhoenix
8
+ import com.vexorpush.codepush.VexorCodePush.Companion.getVersionCode
9
+ import com.vexorpush.codepush.Common
10
+ import com.vexorpush.codepush.Common.CURRENT_VERSION_CODE
11
+ import com.vexorpush.codepush.Common.PATH
12
+ import com.vexorpush.codepush.Common.PREVIOUS_PATH
13
+ import com.vexorpush.codepush.Common.VERSION
14
+ import com.vexorpush.codepush.Common.PREVIOUS_VERSION
15
+ import com.vexorpush.codepush.Common.METADATA
16
+ import com.vexorpush.codepush.Common.BUNDLE_HISTORY
17
+ import com.vexorpush.codepush.SharedPrefs
18
+ import kotlinx.coroutines.CoroutineScope
19
+ import kotlinx.coroutines.Dispatchers
20
+ import kotlinx.coroutines.SupervisorJob
21
+ import kotlinx.coroutines.cancel
22
+ import kotlinx.coroutines.launch
23
+ import kotlinx.coroutines.withContext
24
+ import android.util.Base64
25
+ import com.facebook.react.bridge.UiThreadUtil
26
+ import java.util.concurrent.Executors
27
+ import java.io.File
28
+ import java.io.FileOutputStream
29
+ import java.io.IOException
30
+ import java.net.HttpURLConnection
31
+ import java.net.URL
32
+ import android.util.Log
33
+ import org.json.JSONArray
34
+ import org.json.JSONObject
35
+
36
+ private const val DEBUG_TAG = "VEXOR-CODEPUSH-DEBUG"
37
+
38
+ private fun shortValue(value: String?): String {
39
+ if (value.isNullOrBlank()) return value ?: ""
40
+ return if (value.length > 120) "...${value.takeLast(120)}" else value
41
+ }
42
+
43
+ data class BundleVersion(
44
+ val version: Int,
45
+ val path: String,
46
+ val timestamp: Long,
47
+ val metadata: String? = null
48
+ )
49
+
50
+ class VexorCodePushModule internal constructor(context: ReactApplicationContext) :
51
+ VexorCodePushSpec(context) {
52
+ private val utils: Utils = Utils(context)
53
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
54
+ private val fileWriterExecutor = Executors.newSingleThreadExecutor()
55
+
56
+ override fun getName(): String {
57
+ return NAME
58
+ }
59
+
60
+ override fun invalidate() {
61
+ super.invalidate()
62
+ scope.cancel()
63
+ fileWriterExecutor.shutdown()
64
+ }
65
+
66
+ private fun loadBundleHistory(): List<BundleVersion> {
67
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
68
+ val historyJson = sharedPrefs.getString(BUNDLE_HISTORY)
69
+
70
+ // If history exists, load it
71
+ if (!historyJson.isNullOrEmpty()) {
72
+ return try {
73
+ val jsonArray = JSONArray(historyJson)
74
+ (0 until jsonArray.length()).map { i ->
75
+ val obj = jsonArray.getJSONObject(i)
76
+ BundleVersion(
77
+ version = obj.getInt("version"),
78
+ path = obj.getString("path"),
79
+ timestamp = obj.getLong("timestamp"),
80
+ metadata = if (obj.has("metadata") && !obj.isNull("metadata")) obj.getString("metadata") else null
81
+ )
82
+ }
83
+ } catch (e: Exception) {
84
+ emptyList()
85
+ }
86
+ }
87
+
88
+ // Bootstrap bundle history from the current and previous bundle pointers
89
+ val currentPath = sharedPrefs.getString(PATH)
90
+ val currentVersion = sharedPrefs.getString(VERSION)
91
+ val previousPath = sharedPrefs.getString(PREVIOUS_PATH)
92
+ val previousVersion = sharedPrefs.getString(PREVIOUS_VERSION)
93
+
94
+ if (currentPath.isNullOrEmpty()) {
95
+ return emptyList()
96
+ }
97
+
98
+ // Migrate current bundle
99
+ val migratedHistory = mutableListOf<BundleVersion>()
100
+
101
+ // Add current bundle if has version
102
+ if (!currentVersion.isNullOrEmpty()) {
103
+ try {
104
+ val version = currentVersion.toInt()
105
+ val bundleFile = File(currentPath)
106
+ if (bundleFile.exists()) {
107
+ migratedHistory.add(
108
+ BundleVersion(
109
+ version = version,
110
+ path = currentPath,
111
+ timestamp = bundleFile.lastModified(), // Use file modification time
112
+ metadata = null
113
+ )
114
+ )
115
+ }
116
+ } catch (e: Exception) {
117
+ // Version is not a number, skip
118
+ }
119
+ }
120
+
121
+ // Add previous bundle if exists
122
+ if (!previousPath.isNullOrEmpty() && !previousVersion.isNullOrEmpty()) {
123
+ try {
124
+ val version = previousVersion.toInt()
125
+ val bundleFile = File(previousPath)
126
+ if (bundleFile.exists()) {
127
+ migratedHistory.add(
128
+ BundleVersion(
129
+ version = version,
130
+ path = previousPath,
131
+ timestamp = bundleFile.lastModified(),
132
+ metadata = null
133
+ )
134
+ )
135
+ }
136
+ } catch (e: Exception) {
137
+ // Version is not a number, skip
138
+ }
139
+ }
140
+
141
+ // Save migrated history if any
142
+ if (migratedHistory.isNotEmpty()) {
143
+ saveBundleHistory(migratedHistory.sortedByDescending { it.version })
144
+ }
145
+
146
+ return migratedHistory.sortedByDescending { it.version }
147
+ }
148
+
149
+ private fun saveBundleHistory(history: List<BundleVersion>) {
150
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
151
+ val jsonArray = JSONArray()
152
+ history.forEach { bundle ->
153
+ val obj = JSONObject()
154
+ obj.put("version", bundle.version)
155
+ obj.put("path", bundle.path)
156
+ obj.put("timestamp", bundle.timestamp)
157
+ if (bundle.metadata != null) {
158
+ obj.put("metadata", bundle.metadata)
159
+ } else {
160
+ obj.put("metadata", JSONObject.NULL)
161
+ }
162
+ jsonArray.put(obj)
163
+ }
164
+ sharedPrefs.putString(BUNDLE_HISTORY, jsonArray.toString())
165
+ Log.d(DEBUG_TAG, "saveBundleHistory versions=${history.map { it.version }}")
166
+ }
167
+
168
+ private fun extractFolderName(path: String): String {
169
+ val file = File(path)
170
+ return file.parentFile?.name ?: ""
171
+ }
172
+
173
+ private fun saveBundleVersion(
174
+ newPath: String,
175
+ version: Int,
176
+ maxVersions: Int,
177
+ metadata: String?
178
+ ) {
179
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
180
+ val history = loadBundleHistory()
181
+
182
+ // Add new version
183
+ val newBundle = BundleVersion(
184
+ version = version,
185
+ path = newPath,
186
+ timestamp = System.currentTimeMillis(),
187
+ metadata = metadata
188
+ )
189
+
190
+ // Combine and sort by version descending
191
+ val updatedHistory = (listOf(newBundle) + history)
192
+ .sortedByDescending { it.version }
193
+ .distinctBy { it.version } // Remove duplicates by version
194
+
195
+ // Keep only maxVersions most recent
196
+ val finalHistory = updatedHistory.take(maxVersions)
197
+
198
+ // Delete old versions beyond limit
199
+ val versionsToKeep = finalHistory.map { it.version }.toSet()
200
+ updatedHistory.forEach { bundle ->
201
+ if (bundle.version !in versionsToKeep) {
202
+ val bundleFile = File(bundle.path)
203
+ val parentDir = bundleFile.parentFile
204
+ if (parentDir != null && parentDir.exists() && parentDir.isDirectory) {
205
+ utils.deleteDirectory(parentDir)
206
+ }
207
+ }
208
+ }
209
+
210
+ // Save updated history
211
+ saveBundleHistory(finalHistory)
212
+
213
+ // Set current path
214
+ sharedPrefs.putString(PATH, newPath)
215
+ sharedPrefs.putString(VERSION, version.toString())
216
+ Log.d(
217
+ DEBUG_TAG,
218
+ "saveBundleVersion version=$version path=${shortValue(newPath)} maxVersions=$maxVersions kept=${finalHistory.map { it.version }}"
219
+ )
220
+ }
221
+
222
+ private fun processBundleFile(
223
+ path: String?,
224
+ extension: String?,
225
+ version: Int?,
226
+ maxVersions: Int?,
227
+ metadata: String?
228
+ ): Boolean {
229
+ if (path != null) {
230
+ val file = File(path)
231
+ Log.d(
232
+ DEBUG_TAG,
233
+ "processBundleFile input path=${shortValue(path)} extension=$extension version=$version exists=${file.exists()} isFile=${file.isFile}"
234
+ )
235
+ if (file.exists() && file.isFile) {
236
+ val fileUnzip = utils.extractZipFile(file, extension ?: ".bundle", version)
237
+ Log.d(DEBUG_TAG, "processBundleFile extracted path=${shortValue(fileUnzip)} version=$version")
238
+ if (fileUnzip != null) {
239
+ file.delete()
240
+ utils.deleteOldBundleIfneeded(null)
241
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
242
+ val oldPath = sharedPrefs.getString(PATH)
243
+
244
+ // If version is provided, save to history system
245
+ if (version != null) {
246
+ val maxVersionsToKeep = maxVersions ?: Common.DEFAULT_MAX_BUNDLE_VERSIONS
247
+ saveBundleVersion(fileUnzip, version, maxVersionsToKeep, metadata)
248
+ } else {
249
+ // No version (e.g., Git update) - just set path, no history
250
+ sharedPrefs.putString(PATH, fileUnzip)
251
+ }
252
+
253
+ sharedPrefs.putString(
254
+ CURRENT_VERSION_CODE,
255
+ reactApplicationContext.getVersionCode()
256
+ )
257
+ Log.d(
258
+ DEBUG_TAG,
259
+ "processBundleFile saved path=${shortValue(fileUnzip)} version=${sharedPrefs.getString(VERSION)} appVersion=${reactApplicationContext.getVersionCode()}"
260
+ )
261
+ return true
262
+ } else {
263
+ file.delete()
264
+ throw Exception("File unzip failed or path is invalid: $file")
265
+ }
266
+ } else {
267
+ throw Exception("File not exist: $file")
268
+ }
269
+ } else {
270
+ throw Exception("Invalid path: $path")
271
+ }
272
+ }
273
+
274
+ @ReactMethod
275
+ override fun downloadAndInstallBundle(
276
+ url: String?,
277
+ headersJson: String?,
278
+ extension: String?,
279
+ version: Double?,
280
+ maxVersions: Double?,
281
+ metadata: String?,
282
+ promise: Promise
283
+ ) {
284
+ if (url.isNullOrBlank()) {
285
+ promise.reject("INVALID_URL", "Download URL is required", null)
286
+ return
287
+ }
288
+
289
+ scope.launch {
290
+ try {
291
+ val bundleFile = downloadBundleToCache(url, headersJson)
292
+ val result = processBundleFile(
293
+ bundleFile.absolutePath,
294
+ extension,
295
+ version?.toInt(),
296
+ maxVersions?.toInt(),
297
+ metadata,
298
+ )
299
+ withContext(Dispatchers.Main) {
300
+ promise.resolve(result)
301
+ }
302
+ } catch (e: Exception) {
303
+ withContext(Dispatchers.Main) {
304
+ Log.e(DEBUG_TAG, "downloadAndInstallBundle error=${e.message}", e)
305
+ promise.reject("DOWNLOAD_INSTALL_ERROR", e)
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ private fun downloadBundleToCache(url: String, headersJson: String?): File {
312
+ val connection = (URL(url).openConnection() as HttpURLConnection).apply {
313
+ connectTimeout = 30000
314
+ readTimeout = 120000
315
+ requestMethod = "GET"
316
+ instanceFollowRedirects = true
317
+ }
318
+
319
+ if (!headersJson.isNullOrBlank()) {
320
+ val headers = JSONObject(headersJson)
321
+ headers.keys().forEach { key ->
322
+ val value = headers.optString(key, "")
323
+ if (key.isNotBlank() && value.isNotBlank()) {
324
+ connection.setRequestProperty(key, value)
325
+ }
326
+ }
327
+ }
328
+
329
+ val statusCode = connection.responseCode
330
+ if (statusCode !in 200..299) {
331
+ connection.disconnect()
332
+ throw IOException("Download failed with HTTP status $statusCode")
333
+ }
334
+
335
+ val outputDir = File(reactApplicationContext.cacheDir, "vexor-code-push")
336
+ if (!outputDir.exists()) outputDir.mkdirs()
337
+ val outputFile = File(outputDir, "${System.currentTimeMillis()}-update.zip")
338
+
339
+ connection.inputStream.use { input ->
340
+ FileOutputStream(outputFile).use { output ->
341
+ input.copyTo(output)
342
+ output.flush()
343
+ }
344
+ }
345
+ connection.disconnect()
346
+ return outputFile
347
+ }
348
+
349
+ @ReactMethod
350
+ override fun setupBundlePath(
351
+ path: String?,
352
+ extension: String?,
353
+ version: Double?,
354
+ maxVersions: Double?,
355
+ metadata: String?,
356
+ promise: Promise
357
+ ) {
358
+ Log.d(
359
+ DEBUG_TAG,
360
+ "setupBundlePath start path=${shortValue(path)} extension=$extension version=$version maxVersions=$maxVersions"
361
+ )
362
+ scope.launch {
363
+ try {
364
+ val versionInt = version?.toInt()
365
+ val maxVersionsInt = maxVersions?.toInt()
366
+ val result = processBundleFile(path, extension, versionInt, maxVersionsInt, metadata)
367
+ withContext(Dispatchers.Main) {
368
+ Log.d(DEBUG_TAG, "setupBundlePath result=$result version=$versionInt")
369
+ promise.resolve(result)
370
+ }
371
+ } catch (e: Exception) {
372
+ withContext(Dispatchers.Main) {
373
+ Log.e(DEBUG_TAG, "setupBundlePath error=${e.message}", e)
374
+ promise.reject("SET_ERROR", e)
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ @ReactMethod
381
+ override fun deleteBundle(i: Double, promise: Promise) {
382
+ scope.launch {
383
+ try {
384
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
385
+ val currentPath = sharedPrefs.getString(PATH)
386
+
387
+ // Delete current bundle from file system
388
+ val isDeleted = utils.deleteOldBundleIfneeded(PATH)
389
+
390
+ // Remove current bundle from history if exists
391
+ if (currentPath != null && currentPath.isNotEmpty()) {
392
+ val history = loadBundleHistory()
393
+ val updatedHistory = history.filter { it.path != currentPath }
394
+ saveBundleHistory(updatedHistory)
395
+ }
396
+
397
+ // Clear paths and version (no longer clear PREVIOUS_PATH, use history instead)
398
+ sharedPrefs.putString(PATH, "")
399
+ sharedPrefs.putString(VERSION, "0")
400
+
401
+ withContext(Dispatchers.Main) {
402
+ promise.resolve(isDeleted)
403
+ }
404
+ } catch (e: Exception) {
405
+ withContext(Dispatchers.Main) {
406
+ promise.reject("DELETE_BUNDLE_ERROR", e)
407
+ }
408
+ }
409
+ }
410
+ }
411
+
412
+ @ReactMethod
413
+ override fun restart() {
414
+ val activity = reactApplicationContext.currentActivity
415
+ val context: Context = activity ?: reactApplicationContext
416
+ UiThreadUtil.runOnUiThread {
417
+ Log.d(DEBUG_TAG, "restart triggerRebirth")
418
+ ProcessPhoenix.triggerRebirth(context)
419
+ }
420
+ }
421
+
422
+ @ReactMethod
423
+ override fun getCurrentVersion(a: Double, promise: Promise) {
424
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
425
+ val version = sharedPrefs.getString(VERSION)
426
+ Log.d(DEBUG_TAG, "getCurrentVersion version=$version")
427
+ if (version != "") {
428
+ promise.resolve(version)
429
+ } else {
430
+ promise.resolve("0")
431
+ }
432
+ }
433
+
434
+ @ReactMethod
435
+ override fun setCurrentVersion(version: String?, promise: Promise) {
436
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
437
+ // No longer save PREVIOUS_VERSION, use history instead
438
+ sharedPrefs.putString(VERSION, version)
439
+ Log.d(DEBUG_TAG, "setCurrentVersion version=$version")
440
+ promise.resolve(true)
441
+ }
442
+
443
+ @ReactMethod
444
+ override fun getUpdateMetadata(a: Double, promise: Promise) {
445
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
446
+ val metadata = sharedPrefs.getString(METADATA)
447
+ if (metadata != "") {
448
+ promise.resolve(metadata);
449
+ } else {
450
+ promise.resolve(null);
451
+ }
452
+ }
453
+
454
+ @ReactMethod
455
+ override fun setUpdateMetadata(metadata: String?, promise: Promise) {
456
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
457
+ sharedPrefs.putString(METADATA, metadata)
458
+ promise.resolve(true)
459
+ }
460
+
461
+
462
+ @ReactMethod
463
+ override fun setExactBundlePath(path: String?, promise: Promise) {
464
+ val file = File(path)
465
+ if (file.exists() && file.isFile) {
466
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
467
+ sharedPrefs.putString(PATH, path)
468
+ sharedPrefs.putString(
469
+ CURRENT_VERSION_CODE,
470
+ reactApplicationContext.getVersionCode()
471
+ )
472
+ promise.resolve(true)
473
+ } else {
474
+ promise.resolve(false)
475
+ }
476
+ }
477
+
478
+ @ReactMethod
479
+ override fun rollbackToPreviousBundle(a: Double, promise: Promise) {
480
+ scope.launch {
481
+ try {
482
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
483
+ val currentPath = sharedPrefs.getString(PATH)
484
+
485
+ // Use history to find previous version (closest to current)
486
+ val history = loadBundleHistory()
487
+ if (history.isNotEmpty() && currentPath != null && currentPath.isNotEmpty()) {
488
+ // Find current bundle in history
489
+ val currentBundle = history.find { it.path == currentPath }
490
+ if (currentBundle != null) {
491
+ // Find previous version (version < current, max version = closest to current)
492
+ val previousBundle = history
493
+ .filter { it.version < currentBundle.version }
494
+ .maxByOrNull { it.version }
495
+
496
+ if (previousBundle != null && File(previousBundle.path).exists()) {
497
+ // Rollback to previous bundle from history
498
+ val isDeleted = utils.deleteOldBundleIfneeded(PATH)
499
+ if (isDeleted) {
500
+ sharedPrefs.putString(PATH, previousBundle.path)
501
+ sharedPrefs.putString(VERSION, previousBundle.version.toString())
502
+
503
+ withContext(Dispatchers.Main) {
504
+ promise.resolve(true)
505
+ }
506
+ return@launch
507
+ }
508
+ }
509
+ }
510
+ }
511
+
512
+ withContext(Dispatchers.Main) {
513
+ promise.resolve(false)
514
+ }
515
+ } catch (e: Exception) {
516
+ withContext(Dispatchers.Main) {
517
+ promise.reject("ROLLBACK_ERROR", e)
518
+ }
519
+ }
520
+ }
521
+ }
522
+
523
+ @ReactMethod
524
+ override fun getBundleList(a: Double, promise: Promise) {
525
+ scope.launch {
526
+ try {
527
+ val history = loadBundleHistory()
528
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
529
+ val activePath = sharedPrefs.getString(PATH)
530
+
531
+ val bundleList = history.map { bundle ->
532
+ val folderName = extractFolderName(bundle.path)
533
+ val bundleObj = JSONObject()
534
+ bundleObj.put("id", folderName)
535
+ bundleObj.put("version", bundle.version)
536
+ bundleObj.put("date", bundle.timestamp)
537
+ bundleObj.put("path", bundle.path)
538
+ bundleObj.put("isActive", bundle.path == activePath)
539
+ if (bundle.metadata != null) {
540
+ try {
541
+ // Try to parse as JSON, if fails use as string
542
+ val metadataJson = JSONObject(bundle.metadata)
543
+ bundleObj.put("metadata", metadataJson)
544
+ } catch (e: Exception) {
545
+ bundleObj.put("metadata", bundle.metadata)
546
+ }
547
+ } else {
548
+ bundleObj.put("metadata", JSONObject.NULL)
549
+ }
550
+ bundleObj
551
+ }
552
+
553
+ val jsonArray = JSONArray()
554
+ bundleList.forEach { jsonArray.put(it) }
555
+
556
+ withContext(Dispatchers.Main) {
557
+ promise.resolve(jsonArray.toString())
558
+ }
559
+ } catch (e: Exception) {
560
+ withContext(Dispatchers.Main) {
561
+ promise.reject("GET_BUNDLE_LIST_ERROR", e)
562
+ }
563
+ }
564
+ }
565
+ }
566
+
567
+ @ReactMethod
568
+ override fun deleteBundleById(id: String, promise: Promise) {
569
+ scope.launch {
570
+ try {
571
+ val history = loadBundleHistory()
572
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
573
+ val activePath = sharedPrefs.getString(PATH)
574
+
575
+ val bundleToDelete = history.find { extractFolderName(it.path) == id }
576
+ if (bundleToDelete == null) {
577
+ withContext(Dispatchers.Main) {
578
+ promise.resolve(false)
579
+ }
580
+ return@launch
581
+ }
582
+
583
+ // If deleting active bundle, rollback to oldest remaining bundle or clear
584
+ if (bundleToDelete.path == activePath) {
585
+ val remainingBundles = history.filter { it.path != bundleToDelete.path }
586
+ if (remainingBundles.isNotEmpty()) {
587
+ val oldestBundle = remainingBundles.minByOrNull { it.version }
588
+ if (oldestBundle != null) {
589
+ sharedPrefs.putString(PATH, oldestBundle.path)
590
+ sharedPrefs.putString(VERSION, oldestBundle.version.toString())
591
+ } else {
592
+ sharedPrefs.putString(PATH, "")
593
+ sharedPrefs.putString(VERSION, "")
594
+ }
595
+ } else {
596
+ sharedPrefs.putString(PATH, "")
597
+ sharedPrefs.putString(VERSION, "")
598
+ }
599
+ }
600
+
601
+ // Delete bundle folder
602
+ val isDeleted = utils.deleteOldBundleIfneeded(bundleToDelete.path)
603
+
604
+ // Remove from history
605
+ val updatedHistory = history.filter { it.path != bundleToDelete.path }
606
+ saveBundleHistory(updatedHistory)
607
+
608
+ withContext(Dispatchers.Main) {
609
+ promise.resolve(isDeleted)
610
+ }
611
+ } catch (e: Exception) {
612
+ withContext(Dispatchers.Main) {
613
+ promise.reject("DELETE_BUNDLE_ERROR", e)
614
+ }
615
+ }
616
+ }
617
+ }
618
+
619
+ @ReactMethod
620
+ override fun clearAllBundles(a: Double, promise: Promise) {
621
+ scope.launch {
622
+ try {
623
+ val history = loadBundleHistory()
624
+ val sharedPrefs = SharedPrefs(reactApplicationContext)
625
+
626
+ // Delete all bundle folders
627
+ history.forEach { bundle ->
628
+ utils.deleteOldBundleIfneeded(bundle.path)
629
+ }
630
+
631
+ // Clear history
632
+ saveBundleHistory(emptyList())
633
+
634
+ // Clear current path and version
635
+ sharedPrefs.putString(PATH, "")
636
+ sharedPrefs.putString(VERSION, "")
637
+
638
+ withContext(Dispatchers.Main) {
639
+ promise.resolve(true)
640
+ }
641
+ } catch (e: Exception) {
642
+ withContext(Dispatchers.Main) {
643
+ promise.reject("CLEAR_ALL_BUNDLES_ERROR", e)
644
+ }
645
+ }
646
+ }
647
+
648
+ }
649
+
650
+ override fun writeFile(
651
+ path: String?,
652
+ base64Content: String?,
653
+ encoding: String?,
654
+ promise: Promise
655
+ ) {
656
+ if (path == null || base64Content == null) {
657
+ promise.reject("INVALID_ARG", "Path and base64Content are required", null)
658
+ return
659
+ }
660
+
661
+ fileWriterExecutor.execute {
662
+ try {
663
+ // Decode base64 to bytes
664
+ val bytes = Base64.decode(base64Content, Base64.DEFAULT)
665
+
666
+ // Ensure parent directory exists
667
+ val file = File(path)
668
+ val parentDir = file.parentFile
669
+ if (parentDir != null && !parentDir.exists()) {
670
+ parentDir.mkdirs()
671
+ }
672
+
673
+ // Write file on background thread
674
+ FileOutputStream(file).use { fos ->
675
+ fos.write(bytes)
676
+ fos.flush()
677
+ }
678
+
679
+ // Resolve on UI thread (React Native requirement)
680
+ UiThreadUtil.runOnUiThread {
681
+ promise.resolve(true)
682
+ }
683
+ } catch (e: IOException) {
684
+ UiThreadUtil.runOnUiThread {
685
+ promise.reject("WRITE_ERROR", "Failed to write file: ${e.message}", e)
686
+ }
687
+ } catch (e: Exception) {
688
+ UiThreadUtil.runOnUiThread {
689
+ promise.reject("WRITE_ERROR", "Unexpected error: ${e.message}", e)
690
+ }
691
+ }
692
+ }
693
+ }
694
+
695
+ companion object {
696
+ const val NAME = "VexorCodePush"
697
+ }
698
+ }
@@ -0,0 +1,7 @@
1
+ package com.vexorpush.codepush
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+
5
+ abstract class VexorCodePushSpec internal constructor(context: ReactApplicationContext) :
6
+ NativeVexorCodePushSpec(context) {
7
+ }