@hot-updater/react-native 0.28.0 → 0.29.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 (73) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +156 -7
  2. package/android/src/main/java/com/hotupdater/CohortService.kt +73 -0
  3. package/android/src/main/java/com/hotupdater/DecompressService.kt +28 -22
  4. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +1 -1
  5. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +12 -0
  6. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +55 -0
  7. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +19 -7
  8. package/android/src/newarch/HotUpdaterModule.kt +16 -19
  9. package/android/src/oldarch/HotUpdaterModule.kt +20 -20
  10. package/android/src/oldarch/HotUpdaterSpec.kt +12 -2
  11. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +153 -31
  12. package/ios/HotUpdater/Internal/CohortService.swift +63 -0
  13. package/ios/HotUpdater/Internal/DecompressService.swift +53 -30
  14. package/ios/HotUpdater/Internal/HotUpdater.mm +111 -59
  15. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +28 -0
  16. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +24 -8
  17. package/lib/commonjs/DefaultResolver.js +3 -5
  18. package/lib/commonjs/DefaultResolver.js.map +1 -1
  19. package/lib/commonjs/checkForUpdate.js +2 -0
  20. package/lib/commonjs/checkForUpdate.js.map +1 -1
  21. package/lib/commonjs/index.js +13 -0
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/commonjs/native.js +193 -18
  24. package/lib/commonjs/native.js.map +1 -1
  25. package/lib/commonjs/native.spec.js +361 -4
  26. package/lib/commonjs/native.spec.js.map +1 -1
  27. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  28. package/lib/commonjs/types.js.map +1 -1
  29. package/lib/module/DefaultResolver.js +3 -5
  30. package/lib/module/DefaultResolver.js.map +1 -1
  31. package/lib/module/checkForUpdate.js +3 -1
  32. package/lib/module/checkForUpdate.js.map +1 -1
  33. package/lib/module/index.js +14 -1
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/module/native.js +187 -14
  36. package/lib/module/native.js.map +1 -1
  37. package/lib/module/native.spec.js +361 -4
  38. package/lib/module/native.spec.js.map +1 -1
  39. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  40. package/lib/module/types.js.map +1 -1
  41. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/index.d.ts +14 -1
  43. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/native.d.ts +39 -8
  45. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +28 -0
  47. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/types.d.ts +4 -0
  49. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/wrap.d.ts +1 -1
  51. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  52. package/lib/typescript/module/index.d.ts +14 -1
  53. package/lib/typescript/module/index.d.ts.map +1 -1
  54. package/lib/typescript/module/native.d.ts +39 -8
  55. package/lib/typescript/module/native.d.ts.map +1 -1
  56. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +28 -0
  57. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  58. package/lib/typescript/module/types.d.ts +4 -0
  59. package/lib/typescript/module/types.d.ts.map +1 -1
  60. package/lib/typescript/module/wrap.d.ts +1 -1
  61. package/package.json +6 -6
  62. package/src/DefaultResolver.ts +4 -4
  63. package/src/checkForUpdate.ts +4 -0
  64. package/src/index.ts +21 -0
  65. package/src/native.spec.ts +400 -4
  66. package/src/native.ts +265 -20
  67. package/src/specs/NativeHotUpdater.ts +32 -0
  68. package/src/types.ts +5 -0
  69. package/src/wrap.tsx +1 -1
  70. package/lib/typescript/commonjs/native.spec.d.ts +0 -2
  71. package/lib/typescript/commonjs/native.spec.d.ts.map +0 -1
  72. package/lib/typescript/module/native.spec.d.ts +0 -2
  73. package/lib/typescript/module/native.spec.d.ts.map +0 -1
@@ -4,6 +4,7 @@ import android.os.StatFs
4
4
  import android.util.Log
5
5
  import kotlinx.coroutines.Dispatchers
6
6
  import kotlinx.coroutines.withContext
7
+ import org.json.JSONObject
7
8
  import java.io.File
8
9
  import java.net.URL
9
10
 
@@ -80,6 +81,18 @@ interface BundleStorageService {
80
81
  */
81
82
  fun getBaseURL(): String
82
83
 
84
+ /**
85
+ * Gets the current active bundle ID from bundle storage.
86
+ * Reads manifest.json first and falls back to older metadata when needed.
87
+ */
88
+ fun getBundleId(): String?
89
+
90
+ /**
91
+ * Gets the current manifest from bundle storage.
92
+ * Returns an empty map when manifest.json is missing or invalid.
93
+ */
94
+ fun getManifest(): Map<String, Any?>
95
+
83
96
  /**
84
97
  * Restores the original bundle and clears downloaded bundle state.
85
98
  * @return true if the reset was successful
@@ -102,6 +115,12 @@ class BundleFileStorageService(
102
115
  private const val TAG = "BundleStorage"
103
116
  }
104
117
 
118
+ private data class ActiveBundleMetadataSnapshot(
119
+ val activeBundleId: String,
120
+ val bundleId: String?,
121
+ val manifest: Map<String, Any?>,
122
+ )
123
+
105
124
  init {
106
125
  // Ensure bundle store directory exists
107
126
  getBundleStoreDir().mkdirs()
@@ -112,6 +131,10 @@ class BundleFileStorageService(
112
131
 
113
132
  private var currentLaunchReport: LaunchReport? = null
114
133
 
134
+ @Volatile
135
+ private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot? = null
136
+ private val activeBundleMetadataLock = Any()
137
+
115
138
  // MARK: - Bundle Store Directory
116
139
 
117
140
  private fun getBundleStoreDir(): File {
@@ -182,6 +205,121 @@ class BundleFileStorageService(
182
205
  else -> null
183
206
  }
184
207
 
208
+ private fun getActiveBundleId(): String? {
209
+ val metadata = loadMetadataOrNull()
210
+ return when {
211
+ metadata?.stagingBundleId != null -> metadata.stagingBundleId
212
+ metadata?.stableBundleId != null -> metadata.stableBundleId
213
+ else -> extractBundleIdFromCurrentURL()
214
+ }
215
+ }
216
+
217
+ private fun getActiveBundleMetadataSnapshot(): ActiveBundleMetadataSnapshot? {
218
+ val activeBundleId =
219
+ getActiveBundleId() ?: run {
220
+ clearActiveBundleMetadataSnapshot()
221
+ return null
222
+ }
223
+
224
+ activeBundleMetadataSnapshot
225
+ ?.takeIf { it.activeBundleId == activeBundleId }
226
+ ?.let { return it }
227
+
228
+ synchronized(activeBundleMetadataLock) {
229
+ activeBundleMetadataSnapshot
230
+ ?.takeIf { it.activeBundleId == activeBundleId }
231
+ ?.let { return it }
232
+
233
+ val bundleDir = File(getBundleStoreDir(), activeBundleId)
234
+ if (!bundleDir.exists()) {
235
+ activeBundleMetadataSnapshot = null
236
+ return null
237
+ }
238
+
239
+ return resolveActiveBundleMetadataSnapshot(bundleDir).also {
240
+ activeBundleMetadataSnapshot = it
241
+ }
242
+ }
243
+ }
244
+
245
+ private fun clearActiveBundleMetadataSnapshot() {
246
+ synchronized(activeBundleMetadataLock) {
247
+ activeBundleMetadataSnapshot = null
248
+ }
249
+ }
250
+
251
+ private fun resolveActiveBundleMetadataSnapshot(bundleDir: File): ActiveBundleMetadataSnapshot {
252
+ val manifest = readManifestFromBundleDir(bundleDir) ?: emptyMap()
253
+ val manifestBundleId =
254
+ (manifest["bundleId"] as? String)
255
+ ?.trim()
256
+ ?.takeIf { it.isNotEmpty() }
257
+
258
+ return ActiveBundleMetadataSnapshot(
259
+ activeBundleId = bundleDir.name,
260
+ bundleId = manifestBundleId ?: readCompatibilityBundleIdFromBundleDir(bundleDir),
261
+ manifest = manifest,
262
+ )
263
+ }
264
+
265
+ private fun readCompatibilityBundleIdFromBundleDir(bundleDir: File): String? {
266
+ val compatibilityBundleIdFile = File(bundleDir, compatibilityBundleIdFilename())
267
+ if (!compatibilityBundleIdFile.exists()) {
268
+ return null
269
+ }
270
+
271
+ return try {
272
+ compatibilityBundleIdFile.readText().trim().takeIf { it.isNotEmpty() }
273
+ } catch (e: Exception) {
274
+ Log.w(
275
+ TAG,
276
+ "Failed to read compatibility bundle metadata from ${compatibilityBundleIdFile.absolutePath}: ${e.message}",
277
+ )
278
+ null
279
+ }
280
+ }
281
+
282
+ private fun compatibilityBundleIdFilename(): String = "BUNDLE_ID"
283
+
284
+ private fun readManifestFromBundleDir(bundleDir: File): Map<String, Any?>? {
285
+ val manifestFile = File(bundleDir, "manifest.json")
286
+ if (!manifestFile.exists()) {
287
+ return null
288
+ }
289
+
290
+ return try {
291
+ JSONObject(manifestFile.readText()).let(::jsonObjectToMap)
292
+ } catch (e: Exception) {
293
+ Log.w(TAG, "Failed to read manifest from ${manifestFile.absolutePath}: ${e.message}")
294
+ null
295
+ }
296
+ }
297
+
298
+ private fun jsonObjectToMap(jsonObject: JSONObject): Map<String, Any?> {
299
+ val result = linkedMapOf<String, Any?>()
300
+ val keys = jsonObject.keys()
301
+
302
+ while (keys.hasNext()) {
303
+ val key = keys.next()
304
+ result[key] = jsonValueToKotlin(jsonObject.opt(key))
305
+ }
306
+
307
+ return result
308
+ }
309
+
310
+ private fun jsonArrayToList(jsonArray: org.json.JSONArray): List<Any?> =
311
+ List(jsonArray.length()) { index ->
312
+ jsonValueToKotlin(jsonArray.opt(index))
313
+ }
314
+
315
+ private fun jsonValueToKotlin(value: Any?): Any? =
316
+ when (value) {
317
+ JSONObject.NULL -> null
318
+ is JSONObject -> jsonObjectToMap(value)
319
+ is org.json.JSONArray -> jsonArrayToList(value)
320
+ else -> value
321
+ }
322
+
185
323
  /**
186
324
  * Checks if isolationKey has changed and cleans up old bundles if needed.
187
325
  * This handles migration when isolationKey format changes.
@@ -400,6 +538,7 @@ class BundleFileStorageService(
400
538
  override fun setBundleURL(localPath: String?): Boolean {
401
539
  Log.d(TAG, "setBundleURL: $localPath")
402
540
  preferences.setItem("HotUpdaterBundleURL", localPath)
541
+ clearActiveBundleMetadataSnapshot()
403
542
  return true
404
543
  }
405
544
 
@@ -779,13 +918,7 @@ class BundleFileStorageService(
779
918
  */
780
919
  override fun getBaseURL(): String {
781
920
  return try {
782
- val metadata = loadMetadataOrNull()
783
- val activeBundleId =
784
- when {
785
- metadata?.stagingBundleId != null -> metadata.stagingBundleId
786
- metadata?.stableBundleId != null -> metadata.stableBundleId
787
- else -> extractBundleIdFromCurrentURL()
788
- }
921
+ val activeBundleId = getActiveBundleId()
789
922
 
790
923
  if (activeBundleId != null) {
791
924
  val bundleDir = File(getBundleStoreDir(), activeBundleId)
@@ -801,6 +934,22 @@ class BundleFileStorageService(
801
934
  }
802
935
  }
803
936
 
937
+ override fun getBundleId(): String? =
938
+ try {
939
+ getActiveBundleMetadataSnapshot()?.bundleId
940
+ } catch (e: Exception) {
941
+ Log.e(TAG, "Error getting bundle ID: ${e.message}")
942
+ null
943
+ }
944
+
945
+ override fun getManifest(): Map<String, Any?> =
946
+ try {
947
+ getActiveBundleMetadataSnapshot()?.manifest ?: emptyMap()
948
+ } catch (e: Exception) {
949
+ Log.e(TAG, "Error getting manifest: ${e.message}")
950
+ emptyMap()
951
+ }
952
+
804
953
  override suspend fun resetChannel(): Boolean =
805
954
  withContext(Dispatchers.IO) {
806
955
  if (!setBundleURL(null)) {
@@ -0,0 +1,73 @@
1
+ package com.hotupdater
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import android.provider.Settings
6
+ import java.util.UUID
7
+
8
+ class CohortService(
9
+ private val context: Context,
10
+ ) {
11
+ private val prefs: SharedPreferences =
12
+ context.getSharedPreferences("HotUpdaterCohort", Context.MODE_PRIVATE)
13
+
14
+ companion object {
15
+ // Keep the legacy key so existing custom cohorts continue to work.
16
+ private const val COHORT_KEY = "custom_cohort"
17
+ private const val FALLBACK_IDENTIFIER_KEY = "fallback_identifier"
18
+ }
19
+
20
+ private fun hashString(value: String): Int {
21
+ var hash = 0
22
+ for (char in value) {
23
+ hash = (hash shl 5) - hash + char.code
24
+ }
25
+ return hash
26
+ }
27
+
28
+ private fun defaultNumericCohort(identifier: String): String {
29
+ val hash = hashString(identifier)
30
+ val normalized = ((hash % 1000) + 1000) % 1000
31
+ return (normalized + 1).toString()
32
+ }
33
+
34
+ private fun fallbackIdentifier(): String {
35
+ val fallback = prefs.getString(FALLBACK_IDENTIFIER_KEY, null)
36
+ if (!fallback.isNullOrEmpty()) {
37
+ return fallback
38
+ }
39
+
40
+ val generated = UUID.randomUUID().toString()
41
+ prefs.edit().putString(FALLBACK_IDENTIFIER_KEY, generated).apply()
42
+ return generated
43
+ }
44
+
45
+ fun setCohort(cohort: String) {
46
+ if (cohort.isEmpty()) {
47
+ return
48
+ }
49
+ prefs.edit().putString(COHORT_KEY, cohort).apply()
50
+ }
51
+
52
+ fun getCohort(): String {
53
+ val cohort = prefs.getString(COHORT_KEY, null)
54
+ if (!cohort.isNullOrEmpty()) {
55
+ return cohort
56
+ }
57
+
58
+ val androidId =
59
+ Settings.Secure.getString(
60
+ context.contentResolver,
61
+ Settings.Secure.ANDROID_ID,
62
+ )
63
+ val initialCohort =
64
+ if (!androidId.isNullOrEmpty()) {
65
+ defaultNumericCohort(androidId)
66
+ } else {
67
+ defaultNumericCohort(fallbackIdentifier())
68
+ }
69
+
70
+ prefs.edit().putString(COHORT_KEY, initialCohort).apply()
71
+ return initialCohort
72
+ }
73
+ }
@@ -12,14 +12,14 @@ class DecompressService {
12
12
  private const val TAG = "DecompressService"
13
13
  }
14
14
 
15
- // Array of available strategies in order of detection priority
16
- // Order matters: Try ZIP first (clear magic bytes), then TAR.GZ (GZIP magic bytes), then TAR.BR (fallback)
17
- private val strategies =
15
+ // Strategies with reliable file signatures that can be validated cheaply.
16
+ // TAR.BR is attempted only as the final fallback because Brotli has no reliable magic bytes.
17
+ private val signatureStrategies =
18
18
  listOf(
19
19
  ZipDecompressionStrategy(),
20
20
  TarGzDecompressionStrategy(),
21
- TarBrDecompressionStrategy(),
22
21
  )
22
+ private val tarBrStrategy = TarBrDecompressionStrategy()
23
23
 
24
24
  /**
25
25
  * Extracts a compressed file to the destination directory.
@@ -39,45 +39,51 @@ class DecompressService {
39
39
  val fileName = file.name
40
40
  val fileSize = if (file.exists()) file.length() else 0L
41
41
 
42
- // Try each strategy's validation
43
- for (strategy in strategies) {
42
+ // Try each signature-based strategy first.
43
+ for (strategy in signatureStrategies) {
44
44
  if (strategy.isValid(filePath)) {
45
45
  Log.d(TAG, "Using strategy for $fileName")
46
46
  return strategy.decompress(filePath, destinationPath, progressCallback)
47
47
  }
48
48
  }
49
49
 
50
- // No valid strategy found - provide detailed error message
51
- val errorMessage =
52
- """
53
- Failed to decompress file: $fileName ($fileSize bytes)
50
+ Log.d(TAG, "No ZIP/TAR.GZ signature matched for $fileName, trying TAR.BR fallback")
54
51
 
55
- Tried strategies: ZIP (magic bytes 0x504B0304), TAR.GZ (magic bytes 0x1F8B), TAR.BR (file extension)
56
-
57
- Supported formats:
58
- - ZIP archives (.zip)
59
- - GZIP compressed TAR archives (.tar.gz)
60
- - Brotli compressed TAR archives (.tar.br)
61
-
62
- Please verify the file is not corrupted and matches one of the supported formats.
63
- """.trimIndent()
52
+ if (tarBrStrategy.decompress(filePath, destinationPath, progressCallback)) {
53
+ Log.d(TAG, "Using TAR.BR fallback for $fileName")
54
+ return true
55
+ }
64
56
 
57
+ val errorMessage = createInvalidArchiveMessage(fileName, fileSize)
65
58
  Log.e(TAG, errorMessage)
66
59
  return false
67
60
  }
68
61
 
69
62
  /**
70
- * Validates if a file is a valid compressed archive.
63
+ * Validates if a file matches one of the signature-based archive formats.
71
64
  * @param filePath Path to the file to validate
72
65
  * @return true if the file is a valid compressed archive
73
66
  */
74
67
  fun isValidZipFile(filePath: String): Boolean {
75
- for (strategy in strategies) {
68
+ for (strategy in signatureStrategies) {
76
69
  if (strategy.isValid(filePath)) {
77
70
  return true
78
71
  }
79
72
  }
80
- Log.d(TAG, "No valid strategy found for file: $filePath")
73
+ Log.d(TAG, "No ZIP/TAR.GZ signature matched for file: $filePath. TAR.BR is handled during extraction fallback.")
81
74
  return false
82
75
  }
76
+
77
+ private fun createInvalidArchiveMessage(
78
+ fileName: String,
79
+ fileSize: Long,
80
+ ): String =
81
+ """
82
+ The downloaded bundle file is not a valid compressed archive: $fileName ($fileSize bytes)
83
+
84
+ Supported formats:
85
+ - ZIP archives (.zip)
86
+ - GZIP compressed TAR archives (.tar.gz)
87
+ - Brotli compressed TAR archives (.tar.br)
88
+ """.trimIndent()
83
89
  }
@@ -48,7 +48,7 @@ class HotUpdaterException(
48
48
  fun extractionFormatError(cause: Throwable? = null) =
49
49
  HotUpdaterException(
50
50
  "EXTRACTION_FORMAT_ERROR",
51
- "Invalid or corrupted bundle archive format",
51
+ "The downloaded bundle file is not a valid compressed archive",
52
52
  cause,
53
53
  )
54
54
 
@@ -389,6 +389,18 @@ class HotUpdaterImpl {
389
389
  */
390
390
  fun getBaseURL(): String = bundleStorage.getBaseURL()
391
391
 
392
+ /**
393
+ * Gets the current active bundle ID from bundle storage.
394
+ * Reads manifest.json first and falls back to the legacy BUNDLE_ID file.
395
+ * Built-in bundle fallback is handled in JS.
396
+ */
397
+ fun getBundleId(): String? = bundleStorage.getBundleId()
398
+
399
+ /**
400
+ * Gets the current manifest from bundle storage.
401
+ */
402
+ fun getManifest(): Map<String, Any?> = bundleStorage.getManifest()
403
+
392
404
  suspend fun resetChannel(): Boolean {
393
405
  val success = bundleStorage.resetChannel()
394
406
  if (success) {
@@ -0,0 +1,55 @@
1
+ package com.hotupdater
2
+
3
+ import com.facebook.react.bridge.WritableArray
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.bridge.WritableNativeArray
6
+ import com.facebook.react.bridge.WritableNativeMap
7
+
8
+ internal fun Map<String, Any?>.toWritableNativeMap(): WritableNativeMap {
9
+ val result = WritableNativeMap()
10
+ forEach { (key, value) ->
11
+ result.putReactValue(key, value)
12
+ }
13
+ return result
14
+ }
15
+
16
+ internal fun List<*>.toWritableNativeArray(): WritableNativeArray {
17
+ val result = WritableNativeArray()
18
+ forEach { value ->
19
+ result.pushReactValue(value)
20
+ }
21
+ return result
22
+ }
23
+
24
+ private fun WritableMap.putReactValue(
25
+ key: String,
26
+ value: Any?,
27
+ ) {
28
+ when (value) {
29
+ null -> putNull(key)
30
+ is Boolean -> putBoolean(key, value)
31
+ is Number -> putDouble(key, value.toDouble())
32
+ is String -> putString(key, value)
33
+ is Map<*, *> -> {
34
+ @Suppress("UNCHECKED_CAST")
35
+ putMap(key, (value as Map<String, Any?>).toWritableNativeMap())
36
+ }
37
+ is List<*> -> putArray(key, value.toWritableNativeArray())
38
+ else -> putString(key, value.toString())
39
+ }
40
+ }
41
+
42
+ private fun WritableArray.pushReactValue(value: Any?) {
43
+ when (value) {
44
+ null -> pushNull()
45
+ is Boolean -> pushBoolean(value)
46
+ is Number -> pushDouble(value.toDouble())
47
+ is String -> pushString(value)
48
+ is Map<*, *> -> {
49
+ @Suppress("UNCHECKED_CAST")
50
+ pushMap((value as Map<String, Any?>).toWritableNativeMap())
51
+ }
52
+ is List<*> -> pushArray(value.toWritableNativeArray())
53
+ else -> pushString(value.toString())
54
+ }
55
+ }
@@ -25,15 +25,27 @@ class TarBrDecompressionStrategy : DecompressionStrategy {
25
25
  return false
26
26
  }
27
27
 
28
- // Brotli has no standard magic bytes, check file extension
29
- val lowercasedPath = filePath.lowercase()
30
- val isBrotli = lowercasedPath.endsWith(".tar.br") || lowercasedPath.endsWith(".br")
28
+ return try {
29
+ FileInputStream(file).use { fileInputStream ->
30
+ BufferedInputStream(fileInputStream).use { bufferedInputStream ->
31
+ BrotliInputStream(bufferedInputStream).use { brotliInputStream ->
32
+ TarArchiveInputStream(brotliInputStream).use { tarInputStream ->
33
+ val firstEntry = tarInputStream.getNextEntry()
34
+ val isValid = firstEntry != null
31
35
 
32
- if (!isBrotli) {
33
- Log.d(TAG, "Invalid file: not a .tar.br or .br file")
34
- }
36
+ if (!isValid) {
37
+ Log.d(TAG, "Invalid file: tar archive has no entries")
38
+ }
35
39
 
36
- return isBrotli
40
+ isValid
41
+ }
42
+ }
43
+ }
44
+ }
45
+ } catch (e: Exception) {
46
+ Log.d(TAG, "Invalid file: Brotli/TAR validation failed - ${e.message}")
47
+ false
48
+ }
37
49
  }
38
50
 
39
51
  override fun decompress(
@@ -19,6 +19,7 @@ class HotUpdaterModule internal constructor(
19
19
  reactContext: ReactApplicationContext,
20
20
  ) : HotUpdaterSpec(reactContext) {
21
21
  private val mReactApplicationContext: ReactApplicationContext = reactContext
22
+ private val cohortService = CohortService(reactContext)
22
23
 
23
24
  // Managed coroutine scope for the module lifecycle
24
25
  private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
@@ -145,26 +146,9 @@ class HotUpdaterModule internal constructor(
145
146
  // No-op
146
147
  }
147
148
 
148
- override fun notifyAppReady(): WritableNativeMap {
149
- val result = WritableNativeMap()
150
- val impl = getInstance()
151
- val statusMap = impl.notifyAppReady()
152
-
153
- result.putString("status", statusMap["status"] as? String ?: "STABLE")
154
- statusMap["crashedBundleId"]?.let {
155
- result.putString("crashedBundleId", it as String)
156
- }
149
+ override fun notifyAppReady(): WritableNativeMap = getInstance().notifyAppReady().toWritableNativeMap()
157
150
 
158
- return result
159
- }
160
-
161
- override fun getCrashHistory(): WritableNativeArray {
162
- val impl = getInstance()
163
- val crashHistory = impl.getCrashHistory()
164
- val result = WritableNativeArray()
165
- crashHistory.forEach { result.pushString(it) }
166
- return result
167
- }
151
+ override fun getCrashHistory(): WritableNativeArray = getInstance().getCrashHistory().toWritableNativeArray()
168
152
 
169
153
  override fun clearCrashHistory(): Boolean {
170
154
  val impl = getInstance()
@@ -176,6 +160,19 @@ class HotUpdaterModule internal constructor(
176
160
  return impl.getBaseURL()
177
161
  }
178
162
 
163
+ override fun setCohort(cohort: String) {
164
+ cohortService.setCohort(cohort)
165
+ }
166
+
167
+ override fun getCohort(): String = cohortService.getCohort()
168
+
169
+ override fun getBundleId(): String? {
170
+ val impl = getInstance()
171
+ return impl.getBundleId()
172
+ }
173
+
174
+ override fun getManifest(): WritableNativeMap = getInstance().getManifest().toWritableNativeMap()
175
+
179
176
  override fun resetChannel(promise: Promise) {
180
177
  moduleScope.launch {
181
178
  try {
@@ -7,6 +7,7 @@ import com.facebook.react.bridge.Promise
7
7
  import com.facebook.react.bridge.ReactApplicationContext
8
8
  import com.facebook.react.bridge.ReactMethod
9
9
  import com.facebook.react.bridge.ReadableMap
10
+ import com.facebook.react.bridge.WritableNativeArray
10
11
  import com.facebook.react.bridge.WritableNativeMap
11
12
  import com.facebook.react.modules.core.DeviceEventManagerModule
12
13
  import kotlinx.coroutines.CoroutineScope
@@ -14,13 +15,12 @@ import kotlinx.coroutines.Dispatchers
14
15
  import kotlinx.coroutines.SupervisorJob
15
16
  import kotlinx.coroutines.cancel
16
17
  import kotlinx.coroutines.launch
17
- import org.json.JSONArray
18
- import org.json.JSONObject
19
18
 
20
19
  class HotUpdaterModule internal constructor(
21
20
  context: ReactApplicationContext,
22
21
  ) : HotUpdaterSpec(context) {
23
22
  private val mReactApplicationContext: ReactApplicationContext = context
23
+ private val cohortService = CohortService(context)
24
24
 
25
25
  // Managed coroutine scope for the module lifecycle
26
26
  private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
@@ -153,26 +153,10 @@ class HotUpdaterModule internal constructor(
153
153
  }
154
154
 
155
155
  @ReactMethod(isBlockingSynchronousMethod = true)
156
- override fun notifyAppReady(): String {
157
- val result = JSONObject()
158
-
159
- val impl = getInstance()
160
- val statusMap = impl.notifyAppReady()
161
-
162
- result.put("status", statusMap["status"] as? String ?: "STABLE")
163
- statusMap["crashedBundleId"]?.let {
164
- result.put("crashedBundleId", it as String)
165
- }
166
-
167
- return result.toString()
168
- }
156
+ override fun notifyAppReady(): WritableNativeMap = getInstance().notifyAppReady().toWritableNativeMap()
169
157
 
170
158
  @ReactMethod(isBlockingSynchronousMethod = true)
171
- override fun getCrashHistory(): String {
172
- val impl = getInstance()
173
- val crashHistory = impl.getCrashHistory()
174
- return JSONArray(crashHistory).toString()
175
- }
159
+ override fun getCrashHistory(): WritableNativeArray = getInstance().getCrashHistory().toWritableNativeArray()
176
160
 
177
161
  @ReactMethod(isBlockingSynchronousMethod = true)
178
162
  override fun clearCrashHistory(): Boolean {
@@ -186,7 +170,23 @@ class HotUpdaterModule internal constructor(
186
170
  return impl.getBaseURL()
187
171
  }
188
172
 
173
+ @ReactMethod(isBlockingSynchronousMethod = true)
174
+ override fun getBundleId(): String? {
175
+ val impl = getInstance()
176
+ return impl.getBundleId()
177
+ }
178
+
179
+ @ReactMethod(isBlockingSynchronousMethod = true)
180
+ override fun getManifest(): WritableNativeMap = getInstance().getManifest().toWritableNativeMap()
181
+
189
182
  @ReactMethod
183
+ override fun setCohort(cohort: String) {
184
+ cohortService.setCohort(cohort)
185
+ }
186
+
187
+ @ReactMethod(isBlockingSynchronousMethod = true)
188
+ override fun getCohort(): String = cohortService.getCohort()
189
+
190
190
  override fun resetChannel(promise: Promise) {
191
191
  moduleScope.launch {
192
192
  try {
@@ -4,6 +4,8 @@ import com.facebook.react.bridge.Promise
4
4
  import com.facebook.react.bridge.ReactApplicationContext
5
5
  import com.facebook.react.bridge.ReactContextBaseJavaModule
6
6
  import com.facebook.react.bridge.ReadableMap
7
+ import com.facebook.react.bridge.WritableArray
8
+ import com.facebook.react.bridge.WritableMap
7
9
 
8
10
  abstract class HotUpdaterSpec internal constructor(
9
11
  context: ReactApplicationContext,
@@ -17,13 +19,21 @@ abstract class HotUpdaterSpec internal constructor(
17
19
 
18
20
  abstract fun reloadProcess(promise: Promise)
19
21
 
20
- abstract fun notifyAppReady(): String
22
+ abstract fun notifyAppReady(): WritableMap
21
23
 
22
- abstract fun getCrashHistory(): String
24
+ abstract fun getCrashHistory(): WritableArray
23
25
 
24
26
  abstract fun clearCrashHistory(): Boolean
25
27
 
26
28
  abstract fun getBaseURL(): String
27
29
 
30
+ abstract fun setCohort(customId: String)
31
+
32
+ abstract fun getCohort(): String
33
+
34
+ abstract fun getBundleId(): String?
35
+
36
+ abstract fun getManifest(): WritableMap
37
+
28
38
  abstract fun resetChannel(promise: Promise)
29
39
  }