@hot-updater/react-native 0.23.0 → 0.24.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 (91) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +393 -49
  2. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +204 -0
  3. package/android/src/main/java/com/hotupdater/HotUpdater.kt +48 -36
  4. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +134 -0
  5. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +168 -95
  6. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +15 -3
  7. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +17 -12
  8. package/android/src/newarch/HotUpdaterModule.kt +88 -23
  9. package/android/src/oldarch/HotUpdaterModule.kt +89 -22
  10. package/android/src/oldarch/HotUpdaterSpec.kt +6 -0
  11. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +401 -77
  12. package/ios/HotUpdater/Internal/BundleMetadata.swift +177 -0
  13. package/ios/HotUpdater/Internal/HotUpdater.mm +213 -47
  14. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +96 -25
  15. package/ios/HotUpdater/Internal/SignatureVerifier.swift +35 -29
  16. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +2 -2
  17. package/ios/HotUpdater/Public/HotUpdater.h +8 -2
  18. package/lib/commonjs/checkForUpdate.js +31 -28
  19. package/lib/commonjs/checkForUpdate.js.map +1 -1
  20. package/lib/commonjs/error.js +45 -1
  21. package/lib/commonjs/error.js.map +1 -1
  22. package/lib/commonjs/fetchUpdateInfo.js +7 -45
  23. package/lib/commonjs/fetchUpdateInfo.js.map +1 -1
  24. package/lib/commonjs/index.js +237 -208
  25. package/lib/commonjs/index.js.map +1 -1
  26. package/lib/commonjs/native.js +103 -3
  27. package/lib/commonjs/native.js.map +1 -1
  28. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  29. package/lib/commonjs/wrap.js +39 -1
  30. package/lib/commonjs/wrap.js.map +1 -1
  31. package/lib/module/checkForUpdate.js +32 -26
  32. package/lib/module/checkForUpdate.js.map +1 -1
  33. package/lib/module/error.js +45 -0
  34. package/lib/module/error.js.map +1 -1
  35. package/lib/module/fetchUpdateInfo.js +7 -45
  36. package/lib/module/fetchUpdateInfo.js.map +1 -1
  37. package/lib/module/index.js +238 -203
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/native.js +87 -2
  40. package/lib/module/native.js.map +1 -1
  41. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  42. package/lib/module/wrap.js +40 -2
  43. package/lib/module/wrap.js.map +1 -1
  44. package/lib/typescript/commonjs/checkForUpdate.d.ts +11 -13
  45. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/error.d.ts +120 -0
  47. package/lib/typescript/commonjs/error.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +3 -5
  49. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/index.d.ts +35 -41
  51. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/native.d.ts +58 -2
  53. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +62 -0
  55. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/wrap.d.ts +76 -5
  57. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  58. package/lib/typescript/module/checkForUpdate.d.ts +11 -13
  59. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  60. package/lib/typescript/module/error.d.ts +120 -0
  61. package/lib/typescript/module/error.d.ts.map +1 -1
  62. package/lib/typescript/module/fetchUpdateInfo.d.ts +3 -5
  63. package/lib/typescript/module/fetchUpdateInfo.d.ts.map +1 -1
  64. package/lib/typescript/module/index.d.ts +35 -41
  65. package/lib/typescript/module/index.d.ts.map +1 -1
  66. package/lib/typescript/module/native.d.ts +58 -2
  67. package/lib/typescript/module/native.d.ts.map +1 -1
  68. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +62 -0
  69. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  70. package/lib/typescript/module/wrap.d.ts +76 -5
  71. package/lib/typescript/module/wrap.d.ts.map +1 -1
  72. package/package.json +8 -7
  73. package/plugin/build/withHotUpdater.js +55 -4
  74. package/src/checkForUpdate.ts +51 -40
  75. package/src/error.ts +153 -0
  76. package/src/fetchUpdateInfo.ts +10 -58
  77. package/src/index.ts +283 -206
  78. package/src/native.ts +88 -2
  79. package/src/specs/NativeHotUpdater.ts +63 -0
  80. package/src/wrap.tsx +131 -9
  81. package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +0 -52
  82. package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +0 -24
  83. package/lib/commonjs/runUpdateProcess.js +0 -69
  84. package/lib/commonjs/runUpdateProcess.js.map +0 -1
  85. package/lib/module/runUpdateProcess.js +0 -64
  86. package/lib/module/runUpdateProcess.js.map +0 -1
  87. package/lib/typescript/commonjs/runUpdateProcess.d.ts +0 -49
  88. package/lib/typescript/commonjs/runUpdateProcess.d.ts.map +0 -1
  89. package/lib/typescript/module/runUpdateProcess.d.ts +0 -49
  90. package/lib/typescript/module/runUpdateProcess.d.ts.map +0 -1
  91. package/src/runUpdateProcess.ts +0 -80
@@ -9,11 +9,33 @@ import kotlinx.coroutines.withContext
9
9
  /**
10
10
  * Core implementation class for HotUpdater functionality
11
11
  */
12
- class HotUpdaterImpl(
13
- private val context: Context,
14
- private val bundleStorage: BundleStorageService,
15
- private val preferences: PreferencesService,
16
- ) {
12
+ class HotUpdaterImpl {
13
+ private val context: Context
14
+ private val bundleStorage: BundleStorageService
15
+ private val preferences: PreferencesService
16
+
17
+ /**
18
+ * Primary constructor with dependency injection (for testing)
19
+ */
20
+ constructor(
21
+ context: Context,
22
+ bundleStorage: BundleStorageService,
23
+ preferences: PreferencesService,
24
+ ) {
25
+ this.context = context.applicationContext
26
+ this.bundleStorage = bundleStorage
27
+ this.preferences = preferences
28
+ }
29
+
30
+ /**
31
+ * Convenience constructor for simple usage
32
+ */
33
+ constructor(context: Context) : this(
34
+ context = context,
35
+ bundleStorage = createBundleStorage(context),
36
+ preferences = createPreferences(context),
37
+ )
38
+
17
39
  /**
18
40
  * Gets the app version
19
41
  * @param context Application context
@@ -38,8 +60,62 @@ class HotUpdaterImpl(
38
60
  }
39
61
 
40
62
  companion object {
63
+ private const val TAG = "HotUpdaterImpl"
41
64
  private const val DEFAULT_CHANNEL = "production"
42
65
 
66
+ /**
67
+ * Create BundleStorageService with all dependencies
68
+ */
69
+ private fun createBundleStorage(context: Context): BundleStorageService {
70
+ val appContext = context.applicationContext
71
+ val fileSystem = FileManagerService(appContext)
72
+ val preferences = createPreferences(appContext)
73
+ val downloadService = OkHttpDownloadService()
74
+ val decompressService = DecompressService()
75
+
76
+ return BundleFileStorageService(
77
+ appContext,
78
+ fileSystem,
79
+ downloadService,
80
+ decompressService,
81
+ preferences,
82
+ )
83
+ }
84
+
85
+ /**
86
+ * Create PreferencesService with isolation key
87
+ */
88
+ private fun createPreferences(context: Context): PreferencesService {
89
+ val appContext = context.applicationContext
90
+ val isolationKey = getIsolationKey(appContext)
91
+ return VersionedPreferencesService(appContext, isolationKey)
92
+ }
93
+
94
+ /**
95
+ * Gets the complete isolation key for preferences storage
96
+ * @param context Application context
97
+ * @return The isolation key in format: HotUpdaterPrefs_{fingerprintOrVersion}_{channel}
98
+ */
99
+ private fun getIsolationKey(context: Context): String {
100
+ // Get fingerprint hash directly from resources
101
+ val fingerprintId = context.resources.getIdentifier("hot_updater_fingerprint_hash", "string", context.packageName)
102
+ val fingerprintHash =
103
+ if (fingerprintId != 0) {
104
+ context.getString(fingerprintId).takeIf { it.isNotEmpty() }
105
+ } else {
106
+ null
107
+ }
108
+
109
+ // Get app version and channel
110
+ val appVersion = getAppVersion(context) ?: "unknown"
111
+ val appChannel = getChannel(context)
112
+
113
+ // Use fingerprint if available, otherwise use app version
114
+ val baseKey = if (!fingerprintHash.isNullOrEmpty()) fingerprintHash else appVersion
115
+
116
+ return "HotUpdaterPrefs_${baseKey}_$appChannel"
117
+ }
118
+
43
119
  fun getAppVersion(context: Context): String? =
44
120
  try {
45
121
  val packageInfo =
@@ -68,97 +144,72 @@ class HotUpdaterImpl(
68
144
  }
69
145
 
70
146
  /**
71
- * Gets the complete isolation key for preferences storage
72
- * @param context Application context
73
- * @return The isolation key in format: HotUpdaterPrefs_{fingerprintOrVersion}_{channel}
147
+ * Get minimum bundle ID string
148
+ * @return The minimum bundle ID string
74
149
  */
75
- fun getIsolationKey(context: Context): String {
76
- // Get fingerprint hash directly from resources
77
- val fingerprintId = context.resources.getIdentifier("hot_updater_fingerprint_hash", "string", context.packageName)
78
- val fingerprintHash =
79
- if (fingerprintId != 0) {
80
- context.getString(fingerprintId).takeIf { it.isNotEmpty() }
81
- } else {
82
- null
83
- }
84
-
85
- // Get app version and channel
86
- val appVersion = getAppVersion(context) ?: "unknown"
87
- val appChannel = getChannel(context)
88
-
89
- // Use fingerprint if available, otherwise use app version
90
- val baseKey = if (!fingerprintHash.isNullOrEmpty()) fingerprintHash else appVersion
91
-
92
- // Build complete isolation key
93
- return "HotUpdaterPrefs_${baseKey}_$appChannel"
94
- }
95
- }
150
+ fun getMinBundleId(): String = BuildConfig.MIN_BUNDLE_ID.takeIf { it != "null" } ?: generateMinBundleIdFromBuildTimestamp()
96
151
 
97
- /**
98
- * Get minimum bundle ID string
99
- * @return The minimum bundle ID string
100
- */
101
- fun getMinBundleId(): String = BuildConfig.MIN_BUNDLE_ID.takeIf { it != "null" } ?: generateMinBundleIdFromBuildTimestamp()
102
-
103
- /**
104
- * Generates a bundle ID based on build timestamp
105
- * @return The generated minimum bundle ID string
106
- */
107
- private fun generateMinBundleIdFromBuildTimestamp(): String =
108
- try {
109
- val buildTimestampMs = BuildConfig.BUILD_TIMESTAMP
110
- val bytes =
111
- ByteArray(16).apply {
112
- this[0] = ((buildTimestampMs shr 40) and 0xFF).toByte()
113
- this[1] = ((buildTimestampMs shr 32) and 0xFF).toByte()
114
- this[2] = ((buildTimestampMs shr 24) and 0xFF).toByte()
115
- this[3] = ((buildTimestampMs shr 16) and 0xFF).toByte()
116
- this[4] = ((buildTimestampMs shr 8) and 0xFF).toByte()
117
- this[5] = (buildTimestampMs and 0xFF).toByte()
118
- this[6] = 0x70.toByte()
119
- this[7] = 0x00.toByte()
120
- this[8] = 0x80.toByte()
121
- this[9] = 0x00.toByte()
122
- this[10] = 0x00.toByte()
123
- this[11] = 0x00.toByte()
124
- this[12] = 0x00.toByte()
125
- this[13] = 0x00.toByte()
126
- this[14] = 0x00.toByte()
127
- this[15] = 0x00.toByte()
128
- }
129
- String.format(
130
- "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
131
- bytes[0].toInt() and 0xFF,
132
- bytes[1].toInt() and 0xFF,
133
- bytes[2].toInt() and 0xFF,
134
- bytes[3].toInt() and 0xFF,
135
- bytes[4].toInt() and 0xFF,
136
- bytes[5].toInt() and 0xFF,
137
- bytes[6].toInt() and 0xFF,
138
- bytes[7].toInt() and 0xFF,
139
- bytes[8].toInt() and 0xFF,
140
- bytes[9].toInt() and 0xFF,
141
- bytes[10].toInt() and 0xFF,
142
- bytes[11].toInt() and 0xFF,
143
- bytes[12].toInt() and 0xFF,
144
- bytes[13].toInt() and 0xFF,
145
- bytes[14].toInt() and 0xFF,
146
- bytes[15].toInt() and 0xFF,
147
- )
148
- } catch (e: Exception) {
149
- "00000000-0000-0000-0000-000000000000"
150
- }
152
+ /**
153
+ * Generates a bundle ID based on build timestamp
154
+ * @return The generated minimum bundle ID string
155
+ */
156
+ private fun generateMinBundleIdFromBuildTimestamp(): String =
157
+ try {
158
+ val buildTimestampMs = BuildConfig.BUILD_TIMESTAMP
159
+ val bytes =
160
+ ByteArray(16).apply {
161
+ this[0] = ((buildTimestampMs shr 40) and 0xFF).toByte()
162
+ this[1] = ((buildTimestampMs shr 32) and 0xFF).toByte()
163
+ this[2] = ((buildTimestampMs shr 24) and 0xFF).toByte()
164
+ this[3] = ((buildTimestampMs shr 16) and 0xFF).toByte()
165
+ this[4] = ((buildTimestampMs shr 8) and 0xFF).toByte()
166
+ this[5] = (buildTimestampMs and 0xFF).toByte()
167
+ this[6] = 0x70.toByte()
168
+ this[7] = 0x00.toByte()
169
+ this[8] = 0x80.toByte()
170
+ this[9] = 0x00.toByte()
171
+ this[10] = 0x00.toByte()
172
+ this[11] = 0x00.toByte()
173
+ this[12] = 0x00.toByte()
174
+ this[13] = 0x00.toByte()
175
+ this[14] = 0x00.toByte()
176
+ this[15] = 0x00.toByte()
177
+ }
178
+ String.format(
179
+ "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
180
+ bytes[0].toInt() and 0xFF,
181
+ bytes[1].toInt() and 0xFF,
182
+ bytes[2].toInt() and 0xFF,
183
+ bytes[3].toInt() and 0xFF,
184
+ bytes[4].toInt() and 0xFF,
185
+ bytes[5].toInt() and 0xFF,
186
+ bytes[6].toInt() and 0xFF,
187
+ bytes[7].toInt() and 0xFF,
188
+ bytes[8].toInt() and 0xFF,
189
+ bytes[9].toInt() and 0xFF,
190
+ bytes[10].toInt() and 0xFF,
191
+ bytes[11].toInt() and 0xFF,
192
+ bytes[12].toInt() and 0xFF,
193
+ bytes[13].toInt() and 0xFF,
194
+ bytes[14].toInt() and 0xFF,
195
+ bytes[15].toInt() and 0xFF,
196
+ )
197
+ } catch (e: Exception) {
198
+ "00000000-0000-0000-0000-000000000000"
199
+ }
151
200
 
152
- /**
153
- * Gets the current fingerprint hash
154
- * @return The fingerprint hash or null if not set
155
- */
156
- fun getFingerprintHash(): String? {
157
- val id = context.resources.getIdentifier("hot_updater_fingerprint_hash", "string", context.packageName)
158
- return if (id != 0) {
159
- context.getString(id).takeIf { it.isNotEmpty() }
160
- } else {
161
- null
201
+ /**
202
+ * Gets the current fingerprint hash
203
+ * @param context Application context
204
+ * @return The fingerprint hash or null if not set
205
+ */
206
+ fun getFingerprintHash(context: Context): String? {
207
+ val id = context.resources.getIdentifier("hot_updater_fingerprint_hash", "string", context.packageName)
208
+ return if (id != 0) {
209
+ context.getString(id).takeIf { it.isNotEmpty() }
210
+ } else {
211
+ null
212
+ }
162
213
  }
163
214
  }
164
215
 
@@ -187,14 +238,16 @@ class HotUpdaterImpl(
187
238
  * @param fileUrl URL of the bundle file to download (or null to reset)
188
239
  * @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
189
240
  * @param progressCallback Callback for download progress updates
190
- * @return true if the update was successful
241
+ * @throws HotUpdaterException if the update fails
191
242
  */
192
243
  suspend fun updateBundle(
193
244
  bundleId: String,
194
245
  fileUrl: String?,
195
246
  fileHash: String?,
196
247
  progressCallback: (Double) -> Unit,
197
- ): Boolean = bundleStorage.updateBundle(bundleId, fileUrl, fileHash, progressCallback)
248
+ ) {
249
+ bundleStorage.updateBundle(bundleId, fileUrl, fileHash, progressCallback)
250
+ }
198
251
 
199
252
  /**
200
253
  * Reloads the React Native application
@@ -217,4 +270,24 @@ class HotUpdaterImpl(
217
270
  Log.e("HotUpdaterImpl", "Failed to reload application", e)
218
271
  }
219
272
  }
273
+
274
+ /**
275
+ * Notifies the system that the app has successfully started with the given bundle.
276
+ * If the bundle matches the staging bundle, it promotes to stable.
277
+ * @param bundleId The ID of the currently running bundle
278
+ * @return Map containing status and optional crashedBundleId
279
+ */
280
+ fun notifyAppReady(bundleId: String): Map<String, Any?> = bundleStorage.notifyAppReady(bundleId)
281
+
282
+ /**
283
+ * Gets the crashed bundle history.
284
+ * @return List of crashed bundle IDs
285
+ */
286
+ fun getCrashHistory(): List<String> = bundleStorage.getCrashHistory().bundles.map { it.bundleId }
287
+
288
+ /**
289
+ * Clears the crashed bundle history.
290
+ * @return true if clearing was successful
291
+ */
292
+ fun clearCrashHistory(): Boolean = bundleStorage.clearCrashHistory()
220
293
  }
@@ -20,6 +20,14 @@ import java.net.URL
20
20
  import java.net.UnknownHostException
21
21
  import java.util.concurrent.TimeUnit
22
22
 
23
+ /**
24
+ * Exception for incomplete downloads with size information
25
+ */
26
+ class IncompleteDownloadException(
27
+ val expectedSize: Long,
28
+ val actualSize: Long,
29
+ ) : IOException("Download incomplete: received $actualSize bytes, expected $expectedSize bytes")
30
+
23
31
  /**
24
32
  * Result wrapper for download operations
25
33
  */
@@ -255,12 +263,16 @@ class OkHttpDownloadService : DownloadService {
255
263
  // Verify file size
256
264
  val finalSize = destination.length()
257
265
  if (finalSize != totalSize) {
258
- val errorMsg = "Download incomplete: $finalSize / $totalSize bytes"
259
- Log.d(TAG, errorMsg)
266
+ Log.d(TAG, "Download incomplete: $finalSize / $totalSize bytes")
260
267
 
261
268
  // Delete incomplete file
262
269
  destination.delete()
263
- return@withContext DownloadResult.Error(IOException(errorMsg))
270
+ return@withContext DownloadResult.Error(
271
+ IncompleteDownloadException(
272
+ expectedSize = totalSize,
273
+ actualSize = finalSize,
274
+ ),
275
+ )
264
276
  }
265
277
 
266
278
  Log.d(TAG, "Download completed successfully: $finalSize bytes")
@@ -35,24 +35,29 @@ sealed class SignatureVerificationException(
35
35
  "Public key format is invalid. Ensure the public key is in PEM format (BEGIN PUBLIC KEY)",
36
36
  )
37
37
 
38
+ class MissingFileHash :
39
+ SignatureVerificationException(
40
+ "File hash is missing or empty. Ensure the bundle update includes a valid file hash",
41
+ )
42
+
38
43
  class InvalidSignatureFormat :
39
44
  SignatureVerificationException(
40
- "Signature format is invalid. The signature must be base64-encoded",
45
+ "Signature format is invalid or corrupted. The signature data is malformed or cannot be decoded",
41
46
  )
42
47
 
43
- class VerificationFailed :
48
+ class SignatureVerificationFailed :
44
49
  SignatureVerificationException(
45
50
  "Bundle signature verification failed. The bundle may be corrupted or tampered with",
46
51
  )
47
52
 
48
- class HashMismatch :
53
+ class FileHashMismatch :
49
54
  SignatureVerificationException(
50
- "Bundle hash verification failed. The bundle file hash does not match. File may be corrupted",
55
+ "File hash verification failed. The bundle file hash does not match the expected value. File may be corrupted",
51
56
  )
52
57
 
53
- class HashCalculationFailed :
58
+ class FileReadFailed :
54
59
  SignatureVerificationException(
55
- "Failed to calculate file hash. Could not read file for hash verification",
60
+ "Failed to read file for verification. Could not read file for hash verification",
56
61
  )
57
62
 
58
63
  class UnsignedNotAllowed :
@@ -156,7 +161,7 @@ object SignatureVerifier {
156
161
  // Rule: null/empty fileHash → REJECT
157
162
  if (fileHash.isNullOrEmpty()) {
158
163
  Log.e(TAG, "fileHash is null or empty. Rejecting update.")
159
- throw SignatureVerificationException.VerificationFailed()
164
+ throw SignatureVerificationException.MissingFileHash()
160
165
  }
161
166
 
162
167
  if (isSignedFormat(fileHash)) {
@@ -193,7 +198,7 @@ object SignatureVerifier {
193
198
  * Verifies SHA256 hash of a file.
194
199
  * @param bundleFile The file to verify
195
200
  * @param expectedHash Expected SHA256 hash (hex string)
196
- * @throws SignatureVerificationException.HashMismatch if verification fails
201
+ * @throws SignatureVerificationException.FileHashMismatch if verification fails
197
202
  */
198
203
  fun verifyHash(
199
204
  bundleFile: File,
@@ -203,7 +208,7 @@ object SignatureVerifier {
203
208
 
204
209
  if (!HashUtils.verifyHash(bundleFile, expectedHash)) {
205
210
  Log.e(TAG, "Hash mismatch!")
206
- throw SignatureVerificationException.HashMismatch()
211
+ throw SignatureVerificationException.FileHashMismatch()
207
212
  }
208
213
 
209
214
  Log.i(TAG, "✅ Hash verified successfully")
@@ -242,7 +247,7 @@ object SignatureVerifier {
242
247
  HashUtils.calculateSHA256(bundleFile)
243
248
  ?: run {
244
249
  Log.e(TAG, "Failed to calculate file hash")
245
- throw SignatureVerificationException.HashCalculationFailed()
250
+ throw SignatureVerificationException.FileReadFailed()
246
251
  }
247
252
 
248
253
  Log.d(TAG, "Calculated file hash: $fileHashHex")
@@ -269,7 +274,7 @@ object SignatureVerifier {
269
274
  Log.i(TAG, "✅ Signature verified successfully")
270
275
  } else {
271
276
  Log.e(TAG, "❌ Signature verification failed")
272
- throw SignatureVerificationException.VerificationFailed()
277
+ throw SignatureVerificationException.SignatureVerificationFailed()
273
278
  }
274
279
  } catch (e: SignatureVerificationException) {
275
280
  throw e
@@ -318,7 +323,7 @@ object SignatureVerifier {
318
323
  * Converts hex string to ByteArray.
319
324
  * @param hexString Hex-encoded string
320
325
  * @return ByteArray
321
- * @throws SignatureVerificationException.InvalidSignatureFormat if conversion fails
326
+ * @throws SignatureVerificationException.SignatureVerificationFailed if conversion fails
322
327
  */
323
328
  private fun hexToByteArray(hexString: String): ByteArray {
324
329
  try {
@@ -6,6 +6,7 @@ import androidx.lifecycle.lifecycleScope
6
6
  import com.facebook.react.bridge.Promise
7
7
  import com.facebook.react.bridge.ReactApplicationContext
8
8
  import com.facebook.react.bridge.ReadableMap
9
+ import com.facebook.react.bridge.WritableNativeArray
9
10
  import com.facebook.react.bridge.WritableNativeMap
10
11
  import com.facebook.react.modules.core.DeviceEventManagerModule
11
12
  import kotlinx.coroutines.CoroutineScope
@@ -19,10 +20,17 @@ class HotUpdaterModule internal constructor(
19
20
 
20
21
  override fun getName(): String = NAME
21
22
 
23
+ /**
24
+ * Gets the singleton HotUpdaterImpl instance
25
+ */
26
+ private fun getInstance(): HotUpdaterImpl = HotUpdater.getInstance(mReactApplicationContext)
27
+
22
28
  override fun reload(promise: Promise) {
23
29
  CoroutineScope(Dispatchers.Main.immediate).launch {
24
30
  try {
25
- HotUpdater.reload(mReactApplicationContext)
31
+ val impl = getInstance()
32
+ val currentActivity = mReactApplicationContext.currentActivity
33
+ impl.reload(currentActivity)
26
34
  promise.resolve(null)
27
35
  } catch (e: Exception) {
28
36
  Log.d("HotUpdater", "Failed to reload", e)
@@ -32,41 +40,66 @@ class HotUpdaterModule internal constructor(
32
40
  }
33
41
 
34
42
  override fun updateBundle(
35
- params: ReadableMap,
43
+ params: ReadableMap?,
36
44
  promise: Promise,
37
45
  ) {
38
46
  (mReactApplicationContext.currentActivity as FragmentActivity?)?.lifecycleScope?.launch {
39
47
  try {
40
- val bundleId = params.getString("bundleId")!!
48
+ // Parameter validation
49
+ if (params == null) {
50
+ promise.reject("UNKNOWN_ERROR", "Missing or invalid parameters for updateBundle")
51
+ return@launch
52
+ }
53
+
54
+ val bundleId = params.getString("bundleId")
55
+ if (bundleId == null || bundleId.isEmpty()) {
56
+ promise.reject("MISSING_BUNDLE_ID", "Missing or empty 'bundleId'")
57
+ return@launch
58
+ }
59
+
41
60
  val fileUrl = params.getString("fileUrl")
42
- val fileHash = params.getString("fileHash")
43
- val isSuccess =
44
- HotUpdater.updateBundle(
45
- mReactApplicationContext,
46
- bundleId,
47
- fileUrl,
48
- fileHash,
49
- ) { progress ->
50
- val progressParams =
51
- WritableNativeMap().apply {
52
- putDouble("progress", progress)
53
- }
54
-
55
- this@HotUpdaterModule
56
- .mReactApplicationContext
57
- .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
58
- .emit("onProgress", progressParams)
61
+
62
+ // Validate fileUrl format if provided
63
+ if (fileUrl != null && fileUrl.isNotEmpty()) {
64
+ try {
65
+ java.net.URL(fileUrl)
66
+ } catch (e: java.net.MalformedURLException) {
67
+ promise.reject("INVALID_FILE_URL", "Invalid 'fileUrl' provided: $fileUrl")
68
+ return@launch
59
69
  }
60
- promise.resolve(isSuccess)
70
+ }
71
+
72
+ val fileHash = params.getString("fileHash")
73
+
74
+ val impl = getInstance()
75
+
76
+ impl.updateBundle(
77
+ bundleId,
78
+ fileUrl,
79
+ fileHash,
80
+ ) { progress ->
81
+ val progressParams =
82
+ WritableNativeMap().apply {
83
+ putDouble("progress", progress)
84
+ }
85
+
86
+ this@HotUpdaterModule
87
+ .mReactApplicationContext
88
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
89
+ .emit("onProgress", progressParams)
90
+ }
91
+ promise.resolve(true)
92
+ } catch (e: HotUpdaterException) {
93
+ promise.reject(e.code, e.message)
61
94
  } catch (e: Exception) {
62
- promise.reject("updateBundle", e)
95
+ promise.reject("UNKNOWN_ERROR", e.message ?: "An unknown error occurred")
63
96
  }
64
97
  }
65
98
  }
66
99
 
67
100
  override fun getTypedExportedConstants(): Map<String, Any?> {
68
101
  val constants: MutableMap<String, Any?> = HashMap()
69
- constants["MIN_BUNDLE_ID"] = HotUpdater.getMinBundleId(mReactApplicationContext)
102
+ constants["MIN_BUNDLE_ID"] = HotUpdater.getMinBundleId()
70
103
  constants["APP_VERSION"] = HotUpdater.getAppVersion(mReactApplicationContext)
71
104
  constants["CHANNEL"] = HotUpdater.getChannel(mReactApplicationContext)
72
105
  constants["FINGERPRINT_HASH"] = HotUpdater.getFingerprintHash(mReactApplicationContext)
@@ -85,6 +118,38 @@ class HotUpdaterModule internal constructor(
85
118
  // No-op
86
119
  }
87
120
 
121
+ override fun notifyAppReady(params: ReadableMap?): WritableNativeMap {
122
+ val result = WritableNativeMap()
123
+ val bundleId = params?.getString("bundleId")
124
+ if (bundleId == null) {
125
+ result.putString("status", "STABLE")
126
+ return result
127
+ }
128
+
129
+ val impl = getInstance()
130
+ val statusMap = impl.notifyAppReady(bundleId)
131
+
132
+ result.putString("status", statusMap["status"] as? String ?: "STABLE")
133
+ statusMap["crashedBundleId"]?.let {
134
+ result.putString("crashedBundleId", it as String)
135
+ }
136
+
137
+ return result
138
+ }
139
+
140
+ override fun getCrashHistory(): WritableNativeArray {
141
+ val impl = getInstance()
142
+ val crashHistory = impl.getCrashHistory()
143
+ val result = WritableNativeArray()
144
+ crashHistory.forEach { result.pushString(it) }
145
+ return result
146
+ }
147
+
148
+ override fun clearCrashHistory(): Boolean {
149
+ val impl = getInstance()
150
+ return impl.clearCrashHistory()
151
+ }
152
+
88
153
  companion object {
89
154
  const val NAME = "HotUpdater"
90
155
  }