@hot-updater/react-native 0.20.14 → 0.21.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 (56) hide show
  1. package/HotUpdater.podspec +7 -4
  2. package/android/build.gradle +3 -0
  3. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +78 -21
  4. package/android/src/main/java/com/hotupdater/DecompressService.kt +83 -0
  5. package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +26 -0
  6. package/android/src/main/java/com/hotupdater/HashUtils.kt +47 -0
  7. package/android/src/main/java/com/hotupdater/HotUpdater.kt +3 -0
  8. package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +3 -3
  9. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +3 -1
  10. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +293 -0
  11. package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +105 -0
  12. package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +117 -0
  13. package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +175 -0
  14. package/android/src/newarch/HotUpdaterModule.kt +2 -0
  15. package/android/src/oldarch/HotUpdaterModule.kt +2 -0
  16. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +133 -60
  17. package/ios/HotUpdater/Internal/DecompressService.swift +89 -0
  18. package/ios/HotUpdater/Internal/DecompressionStrategy.swift +22 -0
  19. package/ios/HotUpdater/Internal/HashUtils.swift +63 -0
  20. package/ios/HotUpdater/Internal/HotUpdater.mm +5 -2
  21. package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +4 -4
  22. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +23 -10
  23. package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +229 -0
  24. package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +177 -0
  25. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +73 -7
  26. package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +165 -0
  27. package/lib/commonjs/checkForUpdate.js +1 -0
  28. package/lib/commonjs/checkForUpdate.js.map +1 -1
  29. package/lib/commonjs/fetchUpdateInfo.js +1 -1
  30. package/lib/commonjs/fetchUpdateInfo.js.map +1 -1
  31. package/lib/commonjs/native.js +3 -1
  32. package/lib/commonjs/native.js.map +1 -1
  33. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  34. package/lib/module/checkForUpdate.js +1 -0
  35. package/lib/module/checkForUpdate.js.map +1 -1
  36. package/lib/module/fetchUpdateInfo.js +1 -1
  37. package/lib/module/fetchUpdateInfo.js.map +1 -1
  38. package/lib/module/native.js +3 -1
  39. package/lib/module/native.js.map +1 -1
  40. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  41. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +5 -0
  44. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  45. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  46. package/lib/typescript/module/native.d.ts.map +1 -1
  47. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +5 -0
  48. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  49. package/package.json +5 -5
  50. package/src/checkForUpdate.ts +1 -0
  51. package/src/fetchUpdateInfo.ts +1 -1
  52. package/src/native.ts +6 -0
  53. package/src/specs/NativeHotUpdater.ts +5 -0
  54. package/android/src/main/java/com/hotupdater/HttpDownloadService.kt +0 -98
  55. package/android/src/main/java/com/hotupdater/ZipFileUnzipService.kt +0 -74
  56. package/ios/HotUpdater/Internal/SSZipArchiveUnzipService.swift +0 -25
@@ -11,18 +11,21 @@ Pod::Spec.new do |s|
11
11
  s.license = package["license"]
12
12
  s.authors = package["author"]
13
13
 
14
- s.platforms = { :ios => min_ios_version_supported }
14
+ s.platforms = { :ios => "13.4" }
15
15
  s.source = { :git => "https://github.com/gronxb/hot-updater.git", :tag => "#{s.version}" }
16
16
  s.source_files = "ios/**/*.{h,m,mm,swift}"
17
17
  s.public_header_files = "ios/HotUpdater/Public/*.h"
18
18
  s.private_header_files = "ios/HotUpdater/Internal/*.h"
19
- s.exclude_files = "ios/HotUpdater/Package.swift", "ios/HotUpdater/Test/**/*.{swift,h,m,mm}"
19
+ s.exclude_files = ["ios/HotUpdater/Package.swift", "ios/HotUpdater/Test/**/*.{swift,h,m,mm}"]
20
20
 
21
21
  s.pod_target_xcconfig = {
22
22
  "DEFINES_MODULE" => "YES",
23
23
  "OTHER_SWIFT_FLAGS" => "-enable-experimental-feature AccessLevelOnImport"
24
24
  }
25
- s.dependency "SSZipArchive", "~> 2.2.2"
25
+
26
+ # SWCompression dependency for ZIP/TAR/GZIP/Brotli extraction support
27
+ # Native Compression framework is used for GZIP and Brotli decompression
28
+ s.dependency "SWCompression", "~> 4.8.0"
26
29
 
27
30
  # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
28
31
  # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
@@ -45,5 +48,5 @@ Pod::Spec.new do |s|
45
48
  s.dependency "RCTTypeSafety"
46
49
  s.dependency "ReactCommon/turbomodule/core"
47
50
  end
48
- end
51
+ end
49
52
  end
@@ -130,6 +130,9 @@ dependencies {
130
130
  implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
131
131
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
132
132
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
133
+ implementation "com.squareup.okhttp3:okhttp:4.12.0"
134
+ implementation "org.brotli:dec:0.1.2"
135
+ implementation "org.apache.commons:commons-compress:1.24.0"
133
136
  }
134
137
 
135
138
  if (isNewArchitectureEnabled()) {
@@ -1,5 +1,6 @@
1
1
  package com.hotupdater
2
2
 
3
+ import android.os.StatFs
3
4
  import android.util.Log
4
5
  import kotlinx.coroutines.Dispatchers
5
6
  import kotlinx.coroutines.withContext
@@ -39,12 +40,14 @@ interface BundleStorageService {
39
40
  * Updates the bundle from the specified URL
40
41
  * @param bundleId ID of the bundle to update
41
42
  * @param fileUrl URL of the bundle file to download (or null to reset)
43
+ * @param fileHash SHA256 hash of the bundle file for verification (nullable)
42
44
  * @param progressCallback Callback for download progress updates
43
45
  * @return true if the update was successful
44
46
  */
45
47
  suspend fun updateBundle(
46
48
  bundleId: String,
47
49
  fileUrl: String?,
50
+ fileHash: String?,
48
51
  progressCallback: (Double) -> Unit,
49
52
  ): Boolean
50
53
  }
@@ -55,7 +58,7 @@ interface BundleStorageService {
55
58
  class BundleFileStorageService(
56
59
  private val fileSystem: FileSystemService,
57
60
  private val downloadService: DownloadService,
58
- private val unzipService: UnzipService,
61
+ private val decompressService: DecompressService,
59
62
  private val preferences: PreferencesService,
60
63
  ) : BundleStorageService {
61
64
  override fun setBundleURL(localPath: String?): Boolean {
@@ -84,9 +87,10 @@ class BundleFileStorageService(
84
87
  override suspend fun updateBundle(
85
88
  bundleId: String,
86
89
  fileUrl: String?,
90
+ fileHash: String?,
87
91
  progressCallback: (Double) -> Unit,
88
92
  ): Boolean {
89
- Log.d("BundleStorage", "updateBundle bundleId $bundleId fileUrl $fileUrl")
93
+ Log.d("BundleStorage", "updateBundle bundleId $bundleId fileUrl $fileUrl fileHash $fileHash")
90
94
 
91
95
  // If no URL is provided, reset to fallback
92
96
  if (fileUrl.isNullOrEmpty()) {
@@ -131,18 +135,46 @@ class BundleFileStorageService(
131
135
  }
132
136
  tempDir.mkdirs()
133
137
 
134
- val tempZipFile = File(tempDir, "bundle.zip")
135
-
136
138
  return withContext(Dispatchers.IO) {
137
139
  val downloadUrl = URL(fileUrl)
138
140
 
139
- // Download the file
141
+ // Determine bundle filename from URL
142
+ val bundleFileName =
143
+ if (downloadUrl.path.isNotEmpty()) {
144
+ File(downloadUrl.path).name.ifEmpty { "bundle.zip" }
145
+ } else {
146
+ "bundle.zip"
147
+ }
148
+ val tempBundleFile = File(tempDir, bundleFileName)
149
+
150
+ // Check file size before downloading
151
+ val fileSize = downloadService.getFileSize(downloadUrl)
152
+ if (fileSize > 0 && baseDir != null) {
153
+ // Check available disk space
154
+ val stat = StatFs(baseDir.absolutePath)
155
+ val availableBytes = stat.availableBlocksLong * stat.blockSizeLong
156
+ val requiredSpace = fileSize * 2 // ZIP + extracted files
157
+
158
+ Log.d("BundleStorage", "File size: $fileSize bytes, Available: $availableBytes bytes, Required: $requiredSpace bytes")
159
+
160
+ if (availableBytes < requiredSpace) {
161
+ val errorMsg = "Insufficient disk space: need $requiredSpace bytes, available $availableBytes bytes"
162
+ Log.d("BundleStorage", errorMsg)
163
+ return@withContext false
164
+ }
165
+ } else {
166
+ Log.d("BundleStorage", "Unable to determine file size, proceeding with download")
167
+ }
168
+
169
+ // Download the file (0% - 80%)
140
170
  val downloadResult =
141
171
  downloadService.downloadFile(
142
172
  downloadUrl,
143
- tempZipFile,
144
- progressCallback,
145
- )
173
+ tempBundleFile,
174
+ ) { downloadProgress ->
175
+ // Map download progress to 0.0 - 0.8
176
+ progressCallback(downloadProgress * 0.8)
177
+ }
146
178
 
147
179
  when (downloadResult) {
148
180
  is DownloadResult.Error -> {
@@ -151,23 +183,43 @@ class BundleFileStorageService(
151
183
  return@withContext false
152
184
  }
153
185
  is DownloadResult.Success -> {
154
- // 1) Create a .tmp directory under bundle-store (to avoid colliding with an existing bundleId folder)
186
+ Log.d("BundleStorage", "Download successful")
187
+ // 1) Verify file hash if provided
188
+ if (!fileHash.isNullOrEmpty()) {
189
+ Log.d("BundleStorage", "Verifying file hash...")
190
+ if (!HashUtils.verifyHash(tempBundleFile, fileHash)) {
191
+ Log.d("BundleStorage", "Hash mismatch! Deleting and aborting.")
192
+ tempDir.deleteRecursively()
193
+ tempBundleFile.delete()
194
+ return@withContext false
195
+ }
196
+ Log.d("BundleStorage", "Hash verification passed")
197
+ }
198
+
199
+ // 2) Create a .tmp directory under bundle-store (to avoid colliding with an existing bundleId folder)
155
200
  val tmpDir = File(bundleStoreDir, "$bundleId.tmp")
156
201
  if (tmpDir.exists()) {
157
202
  tmpDir.deleteRecursively()
158
203
  }
159
204
  tmpDir.mkdirs()
160
205
 
161
- // 2) Unzip into tmpDir
162
- Log.d("BundleStorage", "Unzipping $tempZipFile → $tmpDir")
163
- if (!unzipService.extractZipFile(tempZipFile.absolutePath, tmpDir.absolutePath)) {
164
- Log.d("BundleStorage", "Failed to extract zip into tmpDir.")
206
+ // 3) Extract archive into tmpDir (80% - 100%)
207
+ Log.d("BundleStorage", "Extracting $tempBundleFile → $tmpDir")
208
+ if (!decompressService.extractZipFile(
209
+ tempBundleFile.absolutePath,
210
+ tmpDir.absolutePath,
211
+ ) { unzipProgress ->
212
+ // Map unzip progress (0.0 - 1.0) to overall progress (0.8 - 1.0)
213
+ progressCallback(0.8 + (unzipProgress * 0.2))
214
+ }
215
+ ) {
216
+ Log.d("BundleStorage", "Failed to extract archive into tmpDir.")
165
217
  tempDir.deleteRecursively()
166
218
  tmpDir.deleteRecursively()
167
219
  return@withContext false
168
220
  }
169
221
 
170
- // 3) Find index.android.bundle inside tmpDir
222
+ // 4) Find index.android.bundle inside tmpDir
171
223
  val extractedIndex = tmpDir.walk().find { it.name == "index.android.bundle" }
172
224
  if (extractedIndex == null) {
173
225
  Log.d("BundleStorage", "index.android.bundle not found in tmpDir.")
@@ -176,12 +228,16 @@ class BundleFileStorageService(
176
228
  return@withContext false
177
229
  }
178
230
 
179
- // 4) If the realDir (bundle-store/<bundleId>) exists, delete it
231
+ // 5) Log extracted bundle file size
232
+ val bundleSize = extractedIndex.length()
233
+ Log.d("BundleStorage", "Extracted bundle size: $bundleSize bytes")
234
+
235
+ // 6) If the realDir (bundle-store/<bundleId>) exists, delete it
180
236
  if (finalBundleDir.exists()) {
181
237
  finalBundleDir.deleteRecursively()
182
238
  }
183
239
 
184
- // 5) Attempt to rename tmpDir → finalBundleDir (atomic within the same parent folder)
240
+ // 7) Attempt to rename tmpDir → finalBundleDir (atomic within the same parent folder)
185
241
  val renamed = tmpDir.renameTo(finalBundleDir)
186
242
  if (!renamed) {
187
243
  // If rename fails, use moveItem or copyItem
@@ -191,7 +247,7 @@ class BundleFileStorageService(
191
247
  }
192
248
  }
193
249
 
194
- // 6) Verify index.android.bundle exists inside finalBundleDir
250
+ // 8) Verify index.android.bundle exists inside finalBundleDir
195
251
  val finalIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
196
252
  if (finalIndexFile == null) {
197
253
  Log.d("BundleStorage", "index.android.bundle not found in realDir.")
@@ -200,21 +256,22 @@ class BundleFileStorageService(
200
256
  return@withContext false
201
257
  }
202
258
 
203
- // 7) Update finalBundleDir's last modified time
259
+ // 9) Update finalBundleDir's last modified time
204
260
  finalBundleDir.setLastModified(System.currentTimeMillis())
205
261
 
206
- // 8) Save the new bundle path in Preferences
262
+ // 10) Save the new bundle path in Preferences
207
263
  val bundlePath = finalIndexFile.absolutePath
208
264
  Log.d("BundleStorage", "Setting bundle URL: $bundlePath")
209
265
  setBundleURL(bundlePath)
210
266
 
211
- // 9) Clean up temporary and download folders
267
+ // 11) Clean up temporary and download folders
212
268
  tempDir.deleteRecursively()
213
269
 
214
- // 10) Remove old bundles
270
+ // 12) Remove old bundles
215
271
  cleanupOldBundles(bundleStoreDir, currentBundleId, bundleId)
216
272
 
217
273
  Log.d("BundleStorage", "Downloaded and activated bundle successfully.")
274
+ // Progress already at 1.0 from unzip completion
218
275
  return@withContext true
219
276
  }
220
277
  }
@@ -0,0 +1,83 @@
1
+ package com.hotupdater
2
+
3
+ import android.util.Log
4
+ import java.io.File
5
+
6
+ /**
7
+ * Unified decompression service that uses Strategy pattern to handle multiple compression formats.
8
+ * Automatically detects format by trying each strategy's validation and delegates to appropriate decompression strategy.
9
+ */
10
+ class DecompressService {
11
+ companion object {
12
+ private const val TAG = "DecompressService"
13
+ }
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 =
18
+ listOf(
19
+ ZipDecompressionStrategy(),
20
+ TarGzDecompressionStrategy(),
21
+ TarBrDecompressionStrategy(),
22
+ )
23
+
24
+ /**
25
+ * Extracts a compressed file to the destination directory.
26
+ * Automatically detects compression format by trying each strategy's validation.
27
+ * @param filePath Path to the compressed file
28
+ * @param destinationPath Path to the destination directory
29
+ * @param progressCallback Callback for progress updates (0.0 - 1.0)
30
+ * @return true if extraction was successful, false otherwise
31
+ */
32
+ fun extractZipFile(
33
+ filePath: String,
34
+ destinationPath: String,
35
+ progressCallback: (Double) -> Unit,
36
+ ): Boolean {
37
+ // Collect file information for better error messages
38
+ val file = File(filePath)
39
+ val fileName = file.name
40
+ val fileSize = if (file.exists()) file.length() else 0L
41
+
42
+ // Try each strategy's validation
43
+ for (strategy in strategies) {
44
+ if (strategy.isValid(filePath)) {
45
+ Log.d(TAG, "Using strategy for $fileName")
46
+ return strategy.decompress(filePath, destinationPath, progressCallback)
47
+ }
48
+ }
49
+
50
+ // No valid strategy found - provide detailed error message
51
+ val errorMessage =
52
+ """
53
+ Failed to decompress file: $fileName ($fileSize bytes)
54
+
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()
64
+
65
+ Log.e(TAG, errorMessage)
66
+ return false
67
+ }
68
+
69
+ /**
70
+ * Validates if a file is a valid compressed archive.
71
+ * @param filePath Path to the file to validate
72
+ * @return true if the file is a valid compressed archive
73
+ */
74
+ fun isValidZipFile(filePath: String): Boolean {
75
+ for (strategy in strategies) {
76
+ if (strategy.isValid(filePath)) {
77
+ return true
78
+ }
79
+ }
80
+ Log.d(TAG, "No valid strategy found for file: $filePath")
81
+ return false
82
+ }
83
+ }
@@ -0,0 +1,26 @@
1
+ package com.hotupdater
2
+
3
+ /**
4
+ * Interface for decompression strategies
5
+ */
6
+ interface DecompressionStrategy {
7
+ /**
8
+ * Validates if a file can be decompressed by this strategy
9
+ * @param filePath Path to the file to validate
10
+ * @return true if the file is valid for this strategy
11
+ */
12
+ fun isValid(filePath: String): Boolean
13
+
14
+ /**
15
+ * Decompresses a file to the destination directory
16
+ * @param filePath Path to the compressed file
17
+ * @param destinationPath Path to the destination directory
18
+ * @param progressCallback Callback for progress updates (0.0 - 1.0)
19
+ * @return true if decompression was successful, false otherwise
20
+ */
21
+ fun decompress(
22
+ filePath: String,
23
+ destinationPath: String,
24
+ progressCallback: (Double) -> Unit,
25
+ ): Boolean
26
+ }
@@ -0,0 +1,47 @@
1
+ package com.hotupdater
2
+
3
+ import android.util.Log
4
+ import java.io.File
5
+ import java.security.MessageDigest
6
+
7
+ /**
8
+ * Utility class for file hash operations
9
+ */
10
+ object HashUtils {
11
+ /**
12
+ * Calculates SHA256 hash of a file
13
+ * @param file The file to hash
14
+ * @return Hex string of the hash (lowercase)
15
+ */
16
+ fun calculateSHA256(file: File): String {
17
+ val digest = MessageDigest.getInstance("SHA-256")
18
+ file.inputStream().use { input ->
19
+ val buffer = ByteArray(8192)
20
+ var bytesRead: Int
21
+ while (input.read(buffer).also { bytesRead = it } != -1) {
22
+ digest.update(buffer, 0, bytesRead)
23
+ }
24
+ }
25
+ return digest.digest().joinToString("") { "%02x".format(it) }
26
+ }
27
+
28
+ /**
29
+ * Verifies file hash
30
+ * @param file File to verify
31
+ * @param expectedHash Expected SHA256 hash (hex string, case-insensitive)
32
+ * @return true if hash matches
33
+ */
34
+ fun verifyHash(
35
+ file: File,
36
+ expectedHash: String,
37
+ ): Boolean {
38
+ val actualHash = calculateSHA256(file)
39
+ val matches = actualHash.equals(expectedHash, ignoreCase = true)
40
+
41
+ if (!matches) {
42
+ Log.d("HashUtils", "Hash mismatch - Expected: $expectedHash, Actual: $actualHash")
43
+ }
44
+
45
+ return matches
46
+ }
47
+ }
@@ -49,6 +49,7 @@ class HotUpdater {
49
49
  * @param context Application context
50
50
  * @param bundleId ID of the bundle to update
51
51
  * @param fileUrl URL of the bundle file to download (or null to reset)
52
+ * @param fileHash SHA256 hash of the bundle file for verification (nullable)
52
53
  * @param progressCallback Callback for download progress updates
53
54
  * @return true if the update was successful
54
55
  */
@@ -56,11 +57,13 @@ class HotUpdater {
56
57
  context: Context,
57
58
  bundleId: String,
58
59
  fileUrl: String?,
60
+ fileHash: String?,
59
61
  progressCallback: (Double) -> Unit,
60
62
  ): Boolean =
61
63
  HotUpdaterFactory.getInstance(context).updateBundle(
62
64
  bundleId,
63
65
  fileUrl,
66
+ fileHash,
64
67
  progressCallback,
65
68
  )
66
69
 
@@ -33,15 +33,15 @@ object HotUpdaterFactory {
33
33
  // Create services
34
34
  val fileSystem = FileManagerService(appContext)
35
35
  val preferences = VersionedPreferencesService(appContext, isolationKey)
36
- val downloadService = HttpDownloadService()
37
- val unzipService = ZipFileUnzipService()
36
+ val downloadService = OkHttpDownloadService()
37
+ val decompressService = DecompressService()
38
38
 
39
39
  // Create bundle storage with dependencies
40
40
  val bundleStorage =
41
41
  BundleFileStorageService(
42
42
  fileSystem,
43
43
  downloadService,
44
- unzipService,
44
+ decompressService,
45
45
  preferences,
46
46
  )
47
47
 
@@ -185,14 +185,16 @@ class HotUpdaterImpl(
185
185
  * Updates the bundle from the specified URL
186
186
  * @param bundleId ID of the bundle to update
187
187
  * @param fileUrl URL of the bundle file to download (or null to reset)
188
+ * @param fileHash SHA256 hash of the bundle file for verification (nullable)
188
189
  * @param progressCallback Callback for download progress updates
189
190
  * @return true if the update was successful
190
191
  */
191
192
  suspend fun updateBundle(
192
193
  bundleId: String,
193
194
  fileUrl: String?,
195
+ fileHash: String?,
194
196
  progressCallback: (Double) -> Unit,
195
- ): Boolean = bundleStorage.updateBundle(bundleId, fileUrl, progressCallback)
197
+ ): Boolean = bundleStorage.updateBundle(bundleId, fileUrl, fileHash, progressCallback)
196
198
 
197
199
  /**
198
200
  * Reloads the React Native application