@hot-updater/react-native 0.30.12 → 0.31.1

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 (119) hide show
  1. package/HotUpdater.podspec +1 -0
  2. package/android/build.gradle +1 -0
  3. package/android/src/main/java/com/hotupdater/BsdiffPatch.kt +284 -0
  4. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +885 -5
  5. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +13 -2
  6. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +37 -8
  7. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +48 -12
  8. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +60 -22
  9. package/android/src/newarch/HotUpdater.kt +14 -2
  10. package/android/src/newarch/HotUpdaterModule.kt +112 -1
  11. package/android/src/oldarch/HotUpdater.kt +14 -2
  12. package/android/src/oldarch/HotUpdaterModule.kt +107 -3
  13. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +46 -0
  14. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +29 -1
  15. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -1
  16. package/ios/HotUpdater/Internal/BsdiffPatchBridge.h +16 -0
  17. package/ios/HotUpdater/Internal/BsdiffPatchBridge.mm +343 -0
  18. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1093 -47
  19. package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +2 -0
  20. package/ios/HotUpdater/Internal/HotUpdater.mm +12 -2
  21. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +64 -5
  22. package/ios/HotUpdater/Internal/NotificationExtension.swift +2 -1
  23. package/ios/HotUpdater/Internal/SignatureVerifier.swift +28 -5
  24. package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +11 -0
  25. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +11 -5
  26. package/lib/commonjs/DefaultResolver.js +6 -2
  27. package/lib/commonjs/DefaultResolver.js.map +1 -1
  28. package/lib/commonjs/DefaultResolver.spec.js +79 -3
  29. package/lib/commonjs/DefaultResolver.spec.js.map +1 -1
  30. package/lib/commonjs/checkForUpdate.js +6 -0
  31. package/lib/commonjs/checkForUpdate.js.map +1 -1
  32. package/lib/commonjs/index.js +53 -18
  33. package/lib/commonjs/index.js.map +1 -1
  34. package/lib/commonjs/index.spec.js +117 -1
  35. package/lib/commonjs/index.spec.js.map +1 -1
  36. package/lib/commonjs/native.js +40 -16
  37. package/lib/commonjs/native.js.map +1 -1
  38. package/lib/commonjs/native.spec.js +119 -3
  39. package/lib/commonjs/native.spec.js.map +1 -1
  40. package/lib/commonjs/sdkVersion.js +8 -0
  41. package/lib/commonjs/sdkVersion.js.map +1 -0
  42. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  43. package/lib/commonjs/store.js +72 -5
  44. package/lib/commonjs/store.js.map +1 -1
  45. package/lib/commonjs/store.spec.js +265 -8
  46. package/lib/commonjs/store.spec.js.map +1 -1
  47. package/lib/commonjs/wrap.js +19 -5
  48. package/lib/commonjs/wrap.js.map +1 -1
  49. package/lib/commonjs/wrap.spec.js +74 -0
  50. package/lib/commonjs/wrap.spec.js.map +1 -1
  51. package/lib/module/DefaultResolver.js +6 -2
  52. package/lib/module/DefaultResolver.js.map +1 -1
  53. package/lib/module/DefaultResolver.spec.js +78 -3
  54. package/lib/module/DefaultResolver.spec.js.map +1 -1
  55. package/lib/module/checkForUpdate.js +6 -0
  56. package/lib/module/checkForUpdate.js.map +1 -1
  57. package/lib/module/index.js +53 -18
  58. package/lib/module/index.js.map +1 -1
  59. package/lib/module/index.spec.js +117 -1
  60. package/lib/module/index.spec.js.map +1 -1
  61. package/lib/module/native.js +40 -16
  62. package/lib/module/native.js.map +1 -1
  63. package/lib/module/native.spec.js +119 -3
  64. package/lib/module/native.spec.js.map +1 -1
  65. package/lib/module/sdkVersion.js +4 -0
  66. package/lib/module/sdkVersion.js.map +1 -0
  67. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  68. package/lib/module/store.js +72 -5
  69. package/lib/module/store.js.map +1 -1
  70. package/lib/module/store.spec.js +265 -8
  71. package/lib/module/store.spec.js.map +1 -1
  72. package/lib/module/wrap.js +19 -5
  73. package/lib/module/wrap.js.map +1 -1
  74. package/lib/module/wrap.spec.js +74 -0
  75. package/lib/module/wrap.spec.js.map +1 -1
  76. package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -2
  78. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/index.d.ts +36 -8
  80. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/native.d.ts +56 -3
  82. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/sdkVersion.d.ts +2 -0
  84. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +27 -0
  86. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/store.d.ts +5 -0
  88. package/lib/typescript/commonjs/store.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/wrap.d.ts +43 -29
  90. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  91. package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
  92. package/lib/typescript/module/checkForUpdate.d.ts +2 -2
  93. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  94. package/lib/typescript/module/index.d.ts +36 -8
  95. package/lib/typescript/module/index.d.ts.map +1 -1
  96. package/lib/typescript/module/native.d.ts +56 -3
  97. package/lib/typescript/module/native.d.ts.map +1 -1
  98. package/lib/typescript/module/sdkVersion.d.ts +2 -0
  99. package/lib/typescript/module/sdkVersion.d.ts.map +1 -0
  100. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +27 -0
  101. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  102. package/lib/typescript/module/store.d.ts +5 -0
  103. package/lib/typescript/module/store.d.ts.map +1 -1
  104. package/lib/typescript/module/wrap.d.ts +43 -29
  105. package/lib/typescript/module/wrap.d.ts.map +1 -1
  106. package/package.json +20 -10
  107. package/src/DefaultResolver.spec.ts +103 -3
  108. package/src/DefaultResolver.ts +9 -2
  109. package/src/checkForUpdate.ts +9 -2
  110. package/src/index.spec.ts +149 -1
  111. package/src/index.ts +99 -40
  112. package/src/native.spec.ts +139 -1
  113. package/src/native.ts +145 -31
  114. package/src/sdkVersion.ts +1 -0
  115. package/src/specs/NativeHotUpdater.ts +28 -0
  116. package/src/store.spec.ts +324 -9
  117. package/src/store.ts +133 -7
  118. package/src/wrap.spec.ts +91 -0
  119. package/src/wrap.tsx +79 -42
@@ -282,10 +282,21 @@ class HotUpdaterImpl {
282
282
  bundleId: String,
283
283
  fileUrl: String?,
284
284
  fileHash: String?,
285
+ manifestUrl: String?,
286
+ manifestFileHash: String?,
287
+ changedAssets: Map<String, ChangedAssetDescriptor>?,
285
288
  channel: String?,
286
- progressCallback: (Double) -> Unit,
289
+ progressCallback: (UpdateProgressPayload) -> Unit,
287
290
  ) {
288
- bundleStorage.updateBundle(bundleId, fileUrl, fileHash, progressCallback)
291
+ bundleStorage.updateBundle(
292
+ bundleId,
293
+ fileUrl,
294
+ fileHash,
295
+ manifestUrl,
296
+ manifestFileHash,
297
+ changedAssets,
298
+ progressCallback,
299
+ )
289
300
 
290
301
  if (!channel.isNullOrEmpty()) {
291
302
  if (channel == getDefaultChannel()) {
@@ -44,6 +44,12 @@ sealed class DownloadResult {
44
44
  /**
45
45
  * Interface for download operations
46
46
  */
47
+ data class DownloadProgress(
48
+ val progress: Double,
49
+ val downloadedBytes: Long,
50
+ val totalBytes: Long? = null,
51
+ )
52
+
47
53
  interface DownloadService {
48
54
  /**
49
55
  * Downloads a file from a URL
@@ -57,7 +63,7 @@ interface DownloadService {
57
63
  fileUrl: URL,
58
64
  destination: File,
59
65
  fileSizeCallback: ((Long) -> Unit)? = null,
60
- progressCallback: (Double) -> Unit,
66
+ progressCallback: (DownloadProgress) -> Unit,
61
67
  ): DownloadResult
62
68
  }
63
69
 
@@ -66,7 +72,7 @@ interface DownloadService {
66
72
  */
67
73
  private class ProgressResponseBody(
68
74
  private val responseBody: ResponseBody,
69
- private val progressCallback: (Double) -> Unit,
75
+ private val progressCallback: (DownloadProgress) -> Unit,
70
76
  ) : ResponseBody() {
71
77
  private var bufferedSource: BufferedSource? = null
72
78
 
@@ -95,8 +101,25 @@ private class ProgressResponseBody(
95
101
  val currentTime = System.currentTimeMillis()
96
102
 
97
103
  if (currentTime - lastProgressTime >= 100) {
98
- val progress = totalBytesRead.toDouble() / contentLength()
99
- progressCallback.invoke(progress)
104
+ val totalBytes = contentLength()
105
+ if (totalBytes > 0) {
106
+ val progress = totalBytesRead.toDouble() / totalBytes
107
+ progressCallback.invoke(
108
+ DownloadProgress(
109
+ progress = progress,
110
+ downloadedBytes = totalBytesRead,
111
+ totalBytes = totalBytes,
112
+ ),
113
+ )
114
+ } else {
115
+ progressCallback.invoke(
116
+ DownloadProgress(
117
+ progress = 0.0,
118
+ downloadedBytes = totalBytesRead,
119
+ totalBytes = null,
120
+ ),
121
+ )
122
+ }
100
123
  lastProgressTime = currentTime
101
124
  }
102
125
  return bytesRead
@@ -127,7 +150,7 @@ class OkHttpDownloadService : DownloadService {
127
150
  fileUrl: URL,
128
151
  destination: File,
129
152
  fileSizeCallback: ((Long) -> Unit)?,
130
- progressCallback: (Double) -> Unit,
153
+ progressCallback: (DownloadProgress) -> Unit,
131
154
  ): DownloadResult =
132
155
  withContext(Dispatchers.IO) {
133
156
  var attempt = 0
@@ -166,7 +189,7 @@ class OkHttpDownloadService : DownloadService {
166
189
  fileUrl: URL,
167
190
  destination: File,
168
191
  fileSizeCallback: ((Long) -> Unit)?,
169
- progressCallback: (Double) -> Unit,
192
+ progressCallback: (DownloadProgress) -> Unit,
170
193
  ): DownloadResult =
171
194
  withContext(Dispatchers.IO) {
172
195
  // Make sure parent directories exist
@@ -235,7 +258,7 @@ class OkHttpDownloadService : DownloadService {
235
258
 
236
259
  // Verify file size
237
260
  val finalSize = destination.length()
238
- if (totalSize > 0 && finalSize != totalSize) {
261
+ if (totalSize >= 0 && finalSize != totalSize) {
239
262
  Log.d(TAG, "Download incomplete: $finalSize / $totalSize bytes")
240
263
 
241
264
  // Delete incomplete file
@@ -249,7 +272,13 @@ class OkHttpDownloadService : DownloadService {
249
272
  }
250
273
 
251
274
  Log.d(TAG, "Download completed successfully: $finalSize bytes")
252
- progressCallback.invoke(1.0)
275
+ progressCallback.invoke(
276
+ DownloadProgress(
277
+ progress = 1.0,
278
+ downloadedBytes = finalSize,
279
+ totalBytes = if (totalSize > 0) totalSize else null,
280
+ ),
281
+ )
253
282
  DownloadResult.Success(destination)
254
283
  } catch (e: Exception) {
255
284
  response.close()
@@ -26,30 +26,66 @@ private fun WritableMap.putReactValue(
26
26
  value: Any?,
27
27
  ) {
28
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)
29
+ null -> {
30
+ putNull(key)
31
+ }
32
+
33
+ is Boolean -> {
34
+ putBoolean(key, value)
35
+ }
36
+
37
+ is Number -> {
38
+ putDouble(key, value.toDouble())
39
+ }
40
+
41
+ is String -> {
42
+ putString(key, value)
43
+ }
44
+
33
45
  is Map<*, *> -> {
34
46
  @Suppress("UNCHECKED_CAST")
35
47
  putMap(key, (value as Map<String, Any?>).toWritableNativeMap())
36
48
  }
37
- is List<*> -> putArray(key, value.toWritableNativeArray())
38
- else -> putString(key, value.toString())
49
+
50
+ is List<*> -> {
51
+ putArray(key, value.toWritableNativeArray())
52
+ }
53
+
54
+ else -> {
55
+ putString(key, value.toString())
56
+ }
39
57
  }
40
58
  }
41
59
 
42
60
  private fun WritableArray.pushReactValue(value: Any?) {
43
61
  when (value) {
44
- null -> pushNull()
45
- is Boolean -> pushBoolean(value)
46
- is Number -> pushDouble(value.toDouble())
47
- is String -> pushString(value)
62
+ null -> {
63
+ pushNull()
64
+ }
65
+
66
+ is Boolean -> {
67
+ pushBoolean(value)
68
+ }
69
+
70
+ is Number -> {
71
+ pushDouble(value.toDouble())
72
+ }
73
+
74
+ is String -> {
75
+ pushString(value)
76
+ }
77
+
48
78
  is Map<*, *> -> {
49
79
  @Suppress("UNCHECKED_CAST")
50
80
  pushMap((value as Map<String, Any?>).toWritableNativeMap())
51
81
  }
52
- is List<*> -> pushArray(value.toWritableNativeArray())
53
- else -> pushString(value.toString())
82
+
83
+ is List<*> -> {
84
+ pushArray(value.toWritableNativeArray())
85
+ }
86
+
87
+ else -> {
88
+ pushString(value.toString())
89
+ }
54
90
  }
55
91
  }
@@ -247,30 +247,36 @@ object SignatureVerifier {
247
247
 
248
248
  Log.d(TAG, "Calculated file hash: $fileHashHex")
249
249
 
250
- // Decode signature from base64
251
- val signatureBytes =
252
- try {
253
- Base64.decode(signatureBase64, Base64.DEFAULT)
254
- } catch (e: Exception) {
255
- Log.e(TAG, "Failed to decode signature from base64", e)
256
- throw SignatureVerificationException.InvalidSignatureFormat()
257
- }
258
-
259
- // Convert hex fileHash to bytes
260
- val fileHashBytes = hexToByteArray(fileHashHex)
250
+ verifySignatureForHash(publicKey, fileHashHex, signatureBase64)
251
+ } catch (e: SignatureVerificationException) {
252
+ throw e
253
+ } catch (e: Exception) {
254
+ Log.e(TAG, "Signature verification error", e)
255
+ throw SignatureVerificationException.SecurityFrameworkError(e)
256
+ }
257
+ }
261
258
 
262
- // Verify signature using RSA-SHA256
263
- val verifier = Signature.getInstance("SHA256withRSA")
264
- verifier.initVerify(publicKey)
265
- verifier.update(fileHashBytes)
266
- val isValid = verifier.verify(signatureBytes)
259
+ /**
260
+ * Verifies an RSA-SHA256 signature for an explicit SHA256 file hash.
261
+ */
262
+ fun verifyHashSignature(
263
+ context: Context,
264
+ fileHash: String,
265
+ signatureBase64: String,
266
+ ) {
267
+ val publicKeyPEM =
268
+ getPublicKeyFromConfig(context)
269
+ ?: run {
270
+ Log.e(TAG, "Cannot verify signature: public key not configured in strings.xml")
271
+ throw SignatureVerificationException.PublicKeyNotConfigured()
272
+ }
267
273
 
268
- if (isValid) {
269
- Log.i(TAG, "✅ Signature verified successfully")
270
- } else {
271
- Log.e(TAG, "❌ Signature verification failed")
272
- throw SignatureVerificationException.SignatureVerificationFailed()
273
- }
274
+ try {
275
+ verifySignatureForHash(
276
+ createPublicKey(publicKeyPEM),
277
+ fileHash,
278
+ signatureBase64,
279
+ )
274
280
  } catch (e: SignatureVerificationException) {
275
281
  throw e
276
282
  } catch (e: Exception) {
@@ -279,6 +285,38 @@ object SignatureVerifier {
279
285
  }
280
286
  }
281
287
 
288
+ private fun verifySignatureForHash(
289
+ publicKey: PublicKey,
290
+ fileHash: String,
291
+ signatureBase64: String,
292
+ ) {
293
+ if (signatureBase64.isEmpty()) {
294
+ throw SignatureVerificationException.InvalidSignatureFormat()
295
+ }
296
+
297
+ val signatureBytes =
298
+ try {
299
+ Base64.decode(signatureBase64, Base64.DEFAULT)
300
+ } catch (e: Exception) {
301
+ Log.e(TAG, "Failed to decode signature from base64", e)
302
+ throw SignatureVerificationException.InvalidSignatureFormat()
303
+ }
304
+
305
+ val fileHashBytes = hexToByteArray(fileHash)
306
+
307
+ val verifier = Signature.getInstance("SHA256withRSA")
308
+ verifier.initVerify(publicKey)
309
+ verifier.update(fileHashBytes)
310
+ val isValid = verifier.verify(signatureBytes)
311
+
312
+ if (isValid) {
313
+ Log.i(TAG, "✅ Signature verified successfully")
314
+ } else {
315
+ Log.e(TAG, "❌ Signature verification failed")
316
+ throw SignatureVerificationException.SignatureVerificationFailed()
317
+ }
318
+ }
319
+
282
320
  /**
283
321
  * Converts PEM-formatted public key to PublicKey.
284
322
  * @param publicKeyPEM Public key in PEM format
@@ -46,10 +46,22 @@ class HotUpdater {
46
46
  bundleId: String,
47
47
  fileUrl: String?,
48
48
  fileHash: String?,
49
+ manifestUrl: String?,
50
+ manifestFileHash: String?,
51
+ changedAssets: Map<String, ChangedAssetDescriptor>?,
49
52
  channel: String?,
50
- progressCallback: (Double) -> Unit,
53
+ progressCallback: (UpdateProgressPayload) -> Unit,
51
54
  ) {
52
- getInstance(context).updateBundle(bundleId, fileUrl, fileHash, channel, progressCallback)
55
+ getInstance(context).updateBundle(
56
+ bundleId,
57
+ fileUrl,
58
+ fileHash,
59
+ manifestUrl,
60
+ manifestFileHash,
61
+ changedAssets,
62
+ channel,
63
+ progressCallback,
64
+ )
53
65
  }
54
66
 
55
67
  /**
@@ -37,6 +37,76 @@ class HotUpdaterModule internal constructor(
37
37
  */
38
38
  private fun getInstance(): HotUpdaterImpl = HotUpdater.getInstance(mReactApplicationContext)
39
39
 
40
+ private fun parseChangedAssets(params: ReadableMap): Map<String, ChangedAssetDescriptor>? {
41
+ if (!params.hasKey("changedAssets") || params.isNull("changedAssets")) {
42
+ return null
43
+ }
44
+
45
+ val changedAssetsMap = params.getMap("changedAssets") ?: return null
46
+ val parsedAssets = linkedMapOf<String, ChangedAssetDescriptor>()
47
+ val iterator = changedAssetsMap.keySetIterator()
48
+
49
+ while (iterator.hasNextKey()) {
50
+ val assetPath = iterator.nextKey()
51
+ val assetMap = changedAssetsMap.getMap(assetPath) ?: continue
52
+ val assetHash = assetMap.getString("fileHash") ?: continue
53
+ val patchMap = assetMap.getMap("patch")
54
+ val patch =
55
+ if (patchMap != null) {
56
+ val algorithm = patchMap.getString("algorithm")
57
+ val baseBundleId = patchMap.getString("baseBundleId")
58
+ val baseFileHash = patchMap.getString("baseFileHash")
59
+ val patchFileHash = patchMap.getString("patchFileHash")
60
+ val patchUrl = patchMap.getString("patchUrl")
61
+
62
+ if (
63
+ algorithm != null &&
64
+ baseBundleId != null &&
65
+ baseFileHash != null &&
66
+ patchFileHash != null &&
67
+ patchUrl != null
68
+ ) {
69
+ BsdiffPatchDescriptor(
70
+ algorithm = algorithm,
71
+ baseBundleId = baseBundleId,
72
+ baseFileHash = baseFileHash,
73
+ patchFileHash = patchFileHash,
74
+ patchUrl = patchUrl,
75
+ )
76
+ } else {
77
+ null
78
+ }
79
+ } else {
80
+ null
81
+ }
82
+ val fileMap =
83
+ if (assetMap.hasKey("file") && !assetMap.isNull("file")) {
84
+ assetMap.getMap("file")
85
+ } else {
86
+ null
87
+ }
88
+ val assetUrl = fileMap?.getString("url")
89
+ val fileCompression =
90
+ if (fileMap != null && fileMap.hasKey("compression") && !fileMap.isNull("compression")) {
91
+ fileMap.getString("compression")
92
+ } else {
93
+ null
94
+ }
95
+ if (assetUrl == null && patch == null) {
96
+ continue
97
+ }
98
+ parsedAssets[assetPath] =
99
+ ChangedAssetDescriptor(
100
+ fileUrl = assetUrl,
101
+ fileHash = assetHash,
102
+ fileCompression = fileCompression,
103
+ patch = patch,
104
+ )
105
+ }
106
+
107
+ return parsedAssets
108
+ }
109
+
40
110
  override fun reload(promise: Promise) {
41
111
  moduleScope.launch {
42
112
  try {
@@ -88,6 +158,9 @@ class HotUpdaterModule internal constructor(
88
158
  }
89
159
 
90
160
  val fileHash = params.getString("fileHash")
161
+ val manifestUrl = params.getString("manifestUrl")
162
+ val manifestFileHash = params.getString("manifestFileHash")
163
+ val changedAssets = parseChangedAssets(params)
91
164
  val channel = params.getString("channel")
92
165
 
93
166
  val impl = getInstance()
@@ -96,6 +169,9 @@ class HotUpdaterModule internal constructor(
96
169
  bundleId,
97
170
  fileUrl,
98
171
  fileHash,
172
+ manifestUrl,
173
+ manifestFileHash,
174
+ changedAssets,
99
175
  channel,
100
176
  ) { progress ->
101
177
  // Post to Main thread for React Native event emission
@@ -103,7 +179,42 @@ class HotUpdaterModule internal constructor(
103
179
  try {
104
180
  val progressParams =
105
181
  WritableNativeMap().apply {
106
- putDouble("progress", progress)
182
+ putDouble("progress", progress.progress)
183
+ putString("artifactType", progress.artifactType)
184
+ progress.downloadedBytes?.let { putDouble("downloadedBytes", it.toDouble()) }
185
+ progress.totalBytes?.let { putDouble("totalBytes", it.toDouble()) }
186
+ progress.details?.let { details ->
187
+ putMap(
188
+ "details",
189
+ WritableNativeMap().apply {
190
+ putInt("totalFilesCount", details.totalFilesCount)
191
+ putInt("completedFilesCount", details.completedFilesCount)
192
+ putArray(
193
+ "files",
194
+ WritableNativeArray().apply {
195
+ details.files.forEach { file ->
196
+ pushMap(
197
+ WritableNativeMap().apply {
198
+ putString("path", file.path)
199
+ putString("downloadPath", file.downloadPath)
200
+ putString("status", file.status)
201
+ putDouble("progress", file.progress)
202
+ putInt("order", file.order)
203
+ file.downloadedBytes?.let {
204
+ putDouble(
205
+ "downloadedBytes",
206
+ it.toDouble(),
207
+ )
208
+ }
209
+ file.totalBytes?.let { putDouble("totalBytes", it.toDouble()) }
210
+ },
211
+ )
212
+ }
213
+ },
214
+ )
215
+ },
216
+ )
217
+ }
107
218
  }
108
219
 
109
220
  this@HotUpdaterModule
@@ -45,10 +45,22 @@ class HotUpdater {
45
45
  bundleId: String,
46
46
  fileUrl: String?,
47
47
  fileHash: String?,
48
+ manifestUrl: String?,
49
+ manifestFileHash: String?,
50
+ changedAssets: Map<String, ChangedAssetDescriptor>?,
48
51
  channel: String?,
49
- progressCallback: (Double) -> Unit,
52
+ progressCallback: (UpdateProgressPayload) -> Unit,
50
53
  ) {
51
- getInstance(context).updateBundle(bundleId, fileUrl, fileHash, channel, progressCallback)
54
+ getInstance(context).updateBundle(
55
+ bundleId,
56
+ fileUrl,
57
+ fileHash,
58
+ manifestUrl,
59
+ manifestFileHash,
60
+ changedAssets,
61
+ channel,
62
+ progressCallback,
63
+ )
52
64
  }
53
65
 
54
66
  /**
@@ -3,11 +3,11 @@ package com.hotupdater
3
3
  import android.os.Handler
4
4
  import android.os.Looper
5
5
  import android.util.Log
6
+ import com.facebook.react.bridge.Arguments
6
7
  import com.facebook.react.bridge.Promise
7
8
  import com.facebook.react.bridge.ReactApplicationContext
8
9
  import com.facebook.react.bridge.ReactMethod
9
10
  import com.facebook.react.bridge.ReadableMap
10
- import com.facebook.react.bridge.WritableNativeMap
11
11
  import com.facebook.react.modules.core.DeviceEventManagerModule
12
12
  import kotlinx.coroutines.CoroutineScope
13
13
  import kotlinx.coroutines.Dispatchers
@@ -37,6 +37,76 @@ class HotUpdaterModule internal constructor(
37
37
  */
38
38
  private fun getInstance(): HotUpdaterImpl = HotUpdater.getInstance(mReactApplicationContext)
39
39
 
40
+ private fun parseChangedAssets(params: ReadableMap): Map<String, ChangedAssetDescriptor>? {
41
+ if (!params.hasKey("changedAssets") || params.isNull("changedAssets")) {
42
+ return null
43
+ }
44
+
45
+ val changedAssetsMap = params.getMap("changedAssets") ?: return null
46
+ val parsedAssets = linkedMapOf<String, ChangedAssetDescriptor>()
47
+ val iterator = changedAssetsMap.keySetIterator()
48
+
49
+ while (iterator.hasNextKey()) {
50
+ val assetPath = iterator.nextKey()
51
+ val assetMap = changedAssetsMap.getMap(assetPath) ?: continue
52
+ val assetHash = assetMap.getString("fileHash") ?: continue
53
+ val patchMap = assetMap.getMap("patch")
54
+ val patch =
55
+ if (patchMap != null) {
56
+ val algorithm = patchMap.getString("algorithm")
57
+ val baseBundleId = patchMap.getString("baseBundleId")
58
+ val baseFileHash = patchMap.getString("baseFileHash")
59
+ val patchFileHash = patchMap.getString("patchFileHash")
60
+ val patchUrl = patchMap.getString("patchUrl")
61
+
62
+ if (
63
+ algorithm != null &&
64
+ baseBundleId != null &&
65
+ baseFileHash != null &&
66
+ patchFileHash != null &&
67
+ patchUrl != null
68
+ ) {
69
+ BsdiffPatchDescriptor(
70
+ algorithm = algorithm,
71
+ baseBundleId = baseBundleId,
72
+ baseFileHash = baseFileHash,
73
+ patchFileHash = patchFileHash,
74
+ patchUrl = patchUrl,
75
+ )
76
+ } else {
77
+ null
78
+ }
79
+ } else {
80
+ null
81
+ }
82
+ val fileMap =
83
+ if (assetMap.hasKey("file") && !assetMap.isNull("file")) {
84
+ assetMap.getMap("file")
85
+ } else {
86
+ null
87
+ }
88
+ val assetUrl = fileMap?.getString("url")
89
+ val fileCompression =
90
+ if (fileMap != null && fileMap.hasKey("compression") && !fileMap.isNull("compression")) {
91
+ fileMap.getString("compression")
92
+ } else {
93
+ null
94
+ }
95
+ if (assetUrl == null && patch == null) {
96
+ continue
97
+ }
98
+ parsedAssets[assetPath] =
99
+ ChangedAssetDescriptor(
100
+ fileUrl = assetUrl,
101
+ fileHash = assetHash,
102
+ fileCompression = fileCompression,
103
+ patch = patch,
104
+ )
105
+ }
106
+
107
+ return parsedAssets
108
+ }
109
+
40
110
  @ReactMethod
41
111
  override fun reload(promise: Promise) {
42
112
  moduleScope.launch {
@@ -91,6 +161,9 @@ class HotUpdaterModule internal constructor(
91
161
  }
92
162
 
93
163
  val fileHash = params.getString("fileHash")
164
+ val manifestUrl = params.getString("manifestUrl")
165
+ val manifestFileHash = params.getString("manifestFileHash")
166
+ val changedAssets = parseChangedAssets(params)
94
167
  val channel = params.getString("channel")
95
168
 
96
169
  val impl = getInstance()
@@ -99,14 +172,45 @@ class HotUpdaterModule internal constructor(
99
172
  bundleId,
100
173
  fileUrl,
101
174
  fileHash,
175
+ manifestUrl,
176
+ manifestFileHash,
177
+ changedAssets,
102
178
  channel,
103
179
  ) { progress ->
104
180
  // Post to Main thread for React Native event emission
105
181
  Handler(Looper.getMainLooper()).post {
106
182
  try {
107
183
  val progressParams =
108
- WritableNativeMap().apply {
109
- putDouble("progress", progress)
184
+ Arguments.createMap().apply {
185
+ putDouble("progress", progress.progress)
186
+ putString("artifactType", progress.artifactType)
187
+ progress.downloadedBytes?.let { putDouble("downloadedBytes", it.toDouble()) }
188
+ progress.totalBytes?.let { putDouble("totalBytes", it.toDouble()) }
189
+ progress.details?.let { details ->
190
+ val files = Arguments.createArray()
191
+ details.files.forEach { file ->
192
+ files.pushMap(
193
+ Arguments.createMap().apply {
194
+ putString("path", file.path)
195
+ putString("downloadPath", file.downloadPath)
196
+ putString("status", file.status)
197
+ putDouble("progress", file.progress)
198
+ putInt("order", file.order)
199
+ file.downloadedBytes?.let { putDouble("downloadedBytes", it.toDouble()) }
200
+ file.totalBytes?.let { putDouble("totalBytes", it.toDouble()) }
201
+ },
202
+ )
203
+ }
204
+
205
+ putMap(
206
+ "details",
207
+ Arguments.createMap().apply {
208
+ putInt("totalFilesCount", details.totalFilesCount)
209
+ putInt("completedFilesCount", details.completedFilesCount)
210
+ putArray("files", files)
211
+ },
212
+ )
213
+ }
110
214
  }
111
215
 
112
216
  this@HotUpdaterModule