@hot-updater/react-native 0.10.2 → 0.12.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.
@@ -26,26 +26,6 @@ class HotUpdater : ReactPackage {
26
26
  listOf(HotUpdaterModule(context)).toMutableList()
27
27
 
28
28
  companion object {
29
- private fun convertFileSystemPathFromBasePath(
30
- context: Context,
31
- basePath: String,
32
- ): String {
33
- val documentsDir =
34
- context.getExternalFilesDir(null)?.absolutePath ?: context.filesDir.absolutePath
35
- val separator = if (basePath.startsWith("/")) "" else "/"
36
- return "$documentsDir$separator$basePath"
37
- }
38
-
39
- private fun stripPrefixFromPath(
40
- prefix: String,
41
- path: String,
42
- ): String =
43
- if (path.startsWith("/$prefix/")) {
44
- path.replaceFirst("/$prefix/", "")
45
- } else {
46
- path
47
- }
48
-
49
29
  fun getAppVersion(context: Context): String? {
50
30
  val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
51
31
  return packageInfo.versionName
@@ -146,17 +126,46 @@ class HotUpdater : ReactPackage {
146
126
  return true
147
127
  }
148
128
 
149
- val downloadUrl = URL(zipUrl)
150
- val basePath = stripPrefixFromPath(bundleId, downloadUrl.path)
151
- val path = convertFileSystemPathFromBasePath(context, basePath)
129
+ val baseDir = context.getExternalFilesDir(null)
130
+ val bundleStoreDir = File(baseDir, "bundle-store")
131
+ if (!bundleStoreDir.exists()) {
132
+ bundleStoreDir.mkdirs()
133
+ }
134
+
135
+ val finalBundleDir = File(bundleStoreDir, bundleId)
136
+ if (finalBundleDir.exists()) {
137
+ Log.d("HotUpdater", "Bundle for bundleId $bundleId already exists. Using cached bundle.")
138
+ val existingIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
139
+ if (existingIndexFile != null) {
140
+ // Update directory modification time to current time after update
141
+ finalBundleDir.setLastModified(System.currentTimeMillis())
142
+ setBundleURL(context, existingIndexFile.absolutePath)
143
+ cleanupOldBundles(bundleStoreDir)
144
+ return true
145
+ } else {
146
+ finalBundleDir.deleteRecursively()
147
+ }
148
+ }
149
+
150
+ val tempDir = File(baseDir, "bundle-temp")
151
+ if (tempDir.exists()) {
152
+ tempDir.deleteRecursively()
153
+ }
154
+ tempDir.mkdirs()
155
+
156
+ val tempZipFile = File(tempDir, "build.zip")
157
+ val extractedDir = File(tempDir, "extracted")
158
+ extractedDir.mkdirs()
152
159
 
153
160
  val isSuccess =
154
161
  withContext(Dispatchers.IO) {
162
+ val downloadUrl = URL(zipUrl)
155
163
  val conn =
156
164
  try {
157
165
  downloadUrl.openConnection() as HttpURLConnection
158
166
  } catch (e: Exception) {
159
167
  Log.d("HotUpdater", "Failed to open connection: ${e.message}")
168
+ tempDir.deleteRecursively()
160
169
  return@withContext false
161
170
  }
162
171
 
@@ -165,14 +174,11 @@ class HotUpdater : ReactPackage {
165
174
  val totalSize = conn.contentLength
166
175
  if (totalSize <= 0) {
167
176
  Log.d("HotUpdater", "Invalid content length: $totalSize")
177
+ tempDir.deleteRecursively()
168
178
  return@withContext false
169
179
  }
170
-
171
- val file = File(path)
172
- file.parentFile?.mkdirs()
173
-
174
180
  conn.inputStream.use { input ->
175
- file.outputStream().use { output ->
181
+ tempZipFile.outputStream().use { output ->
176
182
  val buffer = ByteArray(8 * 1024)
177
183
  var bytesRead: Int
178
184
  var totalRead = 0L
@@ -182,46 +188,91 @@ class HotUpdater : ReactPackage {
182
188
  output.write(buffer, 0, bytesRead)
183
189
  totalRead += bytesRead
184
190
  val currentTime = System.currentTimeMillis()
185
- if (currentTime - lastProgressTime >= 100) { // Check every 100ms
191
+ if (currentTime - lastProgressTime >= 100) {
186
192
  val progress = totalRead.toDouble() / totalSize
187
193
  progressCallback.invoke(progress)
188
194
  lastProgressTime = currentTime
189
195
  }
190
196
  }
191
- // Send final progress (100%) after download completes
192
197
  progressCallback.invoke(1.0)
193
198
  }
194
199
  }
195
200
  } catch (e: Exception) {
196
201
  Log.d("HotUpdater", "Failed to download data from URL: $zipUrl, Error: ${e.message}")
202
+ tempDir.deleteRecursively()
197
203
  return@withContext false
198
204
  } finally {
199
205
  conn.disconnect()
200
206
  }
201
207
 
202
- val extractedPath = File(path).parentFile?.path ?: return@withContext false
203
-
204
- if (!extractZipFileAtPath(path, extractedPath)) {
208
+ if (!extractZipFileAtPath(tempZipFile.absolutePath, extractedDir.absolutePath)) {
205
209
  Log.d("HotUpdater", "Failed to extract zip file.")
210
+ tempDir.deleteRecursively()
206
211
  return@withContext false
207
212
  }
213
+ true
214
+ }
208
215
 
209
- val extractedDirectory = File(extractedPath)
210
- val indexFile = extractedDirectory.walk().find { it.name == "index.android.bundle" }
216
+ if (!isSuccess) {
217
+ tempDir.deleteRecursively()
218
+ return false
219
+ }
211
220
 
212
- if (indexFile != null) {
213
- val bundlePath = indexFile.path
214
- Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
215
- setBundleURL(context, bundlePath)
216
- } else {
217
- Log.d("HotUpdater", "index.android.bundle not found.")
218
- return@withContext false
219
- }
221
+ val indexFileExtracted = extractedDir.walk().find { it.name == "index.android.bundle" }
222
+ if (indexFileExtracted == null) {
223
+ Log.d("HotUpdater", "index.android.bundle not found in extracted files.")
224
+ tempDir.deleteRecursively()
225
+ return false
226
+ }
220
227
 
221
- Log.d("HotUpdater", "Downloaded and extracted file successfully.")
222
- true
228
+ // Move (or copy) contents from temp folder to finalBundleDir
229
+ if (finalBundleDir.exists()) {
230
+ finalBundleDir.deleteRecursively()
231
+ }
232
+ if (!extractedDir.renameTo(finalBundleDir)) {
233
+ extractedDir.copyRecursively(finalBundleDir, overwrite = true)
234
+ extractedDir.deleteRecursively()
235
+ }
236
+
237
+ val finalIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
238
+ if (finalIndexFile == null) {
239
+ Log.d("HotUpdater", "index.android.bundle not found in final directory.")
240
+ tempDir.deleteRecursively()
241
+ return false
242
+ }
243
+
244
+ // Update final bundle directory modification time to current time after bundle update
245
+ finalBundleDir.setLastModified(System.currentTimeMillis())
246
+
247
+ val bundlePath = finalIndexFile.absolutePath
248
+ Log.d("HotUpdater", "Setting bundle URL: $bundlePath")
249
+ setBundleURL(context, bundlePath)
250
+
251
+ // Clean up old bundles in the bundle store to keep only up to 2 bundles
252
+ cleanupOldBundles(bundleStoreDir)
253
+
254
+ // Clean up temp directory
255
+ tempDir.deleteRecursively()
256
+
257
+ Log.d("HotUpdater", "Downloaded and extracted file successfully.")
258
+ return true
259
+ }
260
+
261
+ // Helper function to delete old bundles, keeping only up to 2 bundles in the bundle-store folder
262
+ private fun cleanupOldBundles(bundleStoreDir: File) {
263
+ // Get list of all directories in bundle-store folder
264
+ val bundles = bundleStoreDir.listFiles { file -> file.isDirectory }?.toList() ?: return
265
+
266
+ // Sort by last modified time in descending order to keep most recently updated bundles at the top
267
+ val sortedBundles = bundles.sortedByDescending { it.lastModified() }
268
+
269
+ // Delete all bundles except the top 2
270
+ if (sortedBundles.size > 2) {
271
+ sortedBundles.drop(2).forEach { oldBundle ->
272
+ Log.d("HotUpdater", "Removing old bundle: ${oldBundle.name}")
273
+ oldBundle.deleteRecursively()
223
274
  }
224
- return isSuccess
275
+ }
225
276
  }
226
277
  }
227
278
  }
package/dist/index.js CHANGED
@@ -1689,14 +1689,13 @@ var __webpack_exports__ = {};
1689
1689
  const updateBundle = (bundleId, zipUrl)=>HotUpdaterNative.updateBundle(bundleId, zipUrl);
1690
1690
  const getAppVersion = ()=>HotUpdaterNative.getAppVersion();
1691
1691
  const reload = ()=>{
1692
- HotUpdaterNative.reload();
1692
+ requestAnimationFrame(()=>{
1693
+ HotUpdaterNative.reload();
1694
+ });
1693
1695
  };
1694
1696
  const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID;
1695
1697
  async function checkForUpdate(config) {
1696
- if (__DEV__) {
1697
- console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
1698
- return null;
1699
- }
1698
+ if (__DEV__) return null;
1700
1699
  if (![
1701
1700
  "ios",
1702
1701
  "android"
package/dist/index.mjs CHANGED
@@ -1658,14 +1658,13 @@ const addListener = (eventName, listener)=>{
1658
1658
  const updateBundle = (bundleId, zipUrl)=>HotUpdaterNative.updateBundle(bundleId, zipUrl);
1659
1659
  const getAppVersion = ()=>HotUpdaterNative.getAppVersion();
1660
1660
  const reload = ()=>{
1661
- HotUpdaterNative.reload();
1661
+ requestAnimationFrame(()=>{
1662
+ HotUpdaterNative.reload();
1663
+ });
1662
1664
  };
1663
1665
  const getBundleId = ()=>HotUpdater.HOT_UPDATER_BUNDLE_ID;
1664
1666
  async function checkForUpdate(config) {
1665
- if (__DEV__) {
1666
- console.warn("[HotUpdater] __DEV__ is true, HotUpdater is only supported in production");
1667
- return null;
1668
- }
1667
+ if (__DEV__) return null;
1669
1668
  if (![
1670
1669
  "ios",
1671
1670
  "android"
package/dist/native.d.ts CHANGED
@@ -7,7 +7,7 @@ export declare const addListener: <T extends keyof HotUpdaterEvent>(eventName: T
7
7
  /**
8
8
  * Downloads files from given URLs.
9
9
  *
10
- * @param {string} bundleId - identifier for the bundle version.
10
+ * @param {string} bundleId - identifier for the bundle id.
11
11
  * @param {string | null} zipUrl - zip file URL. If null, it means rolling back to the built-in bundle
12
12
  * @returns {Promise<boolean>} Resolves with true if download was successful, otherwise rejects with an error.
13
13
  */
@@ -7,6 +7,9 @@
7
7
  bool hasListeners;
8
8
  }
9
9
 
10
+ + (BOOL)requiresMainQueueSetup {
11
+ return YES;
12
+ }
10
13
 
11
14
  - (instancetype)init {
12
15
  self = [super init];
@@ -20,7 +23,6 @@ RCT_EXPORT_MODULE();
20
23
 
21
24
  #pragma mark - Bundle URL Management
22
25
 
23
-
24
26
  - (NSString *)getAppVersion {
25
27
  NSString *appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
26
28
  return appVersion;
@@ -47,7 +49,6 @@ RCT_EXPORT_MODULE();
47
49
  }
48
50
 
49
51
  + (NSURL *)fallbackURL {
50
- // This Support React Native 0.72.6
51
52
  #if DEBUG
52
53
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
53
54
  #else
@@ -61,17 +62,6 @@ RCT_EXPORT_MODULE();
61
62
 
62
63
  #pragma mark - Utility Methods
63
64
 
64
- - (NSString *)convertFileSystemPathFromBasePath:(NSString *)basePath {
65
- return [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:basePath];
66
- }
67
-
68
- - (NSString *)stripPrefixFromPath:(NSString *)prefix path:(NSString *)path {
69
- if ([path hasPrefix:[NSString stringWithFormat:@"/%@/", prefix]]) {
70
- return [path stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"/%@/", prefix] withString:@""];
71
- }
72
- return path;
73
- }
74
-
75
65
  - (BOOL)extractZipFileAtPath:(NSString *)filePath toDestination:(NSString *)destinationPath {
76
66
  NSError *error = nil;
77
67
  BOOL success = [SSZipArchive unzipFileAtPath:filePath toDestination:destinationPath overwrite:YES password:nil error:&error];
@@ -81,6 +71,50 @@ RCT_EXPORT_MODULE();
81
71
  return success;
82
72
  }
83
73
 
74
+ #pragma mark - Cleanup Old Bundles
75
+
76
+ - (void)cleanupOldBundlesAtDirectory:(NSString *)bundleStoreDir {
77
+ NSFileManager *fileManager = [NSFileManager defaultManager];
78
+ NSError *error = nil;
79
+ NSArray *contents = [fileManager contentsOfDirectoryAtPath:bundleStoreDir error:&error];
80
+ if (error) {
81
+ NSLog(@"Failed to list bundle store directory: %@", error);
82
+ return;
83
+ }
84
+
85
+ NSMutableArray *bundleDirs = [NSMutableArray array];
86
+ for (NSString *item in contents) {
87
+ NSString *fullPath = [bundleStoreDir stringByAppendingPathComponent:item];
88
+ BOOL isDir = NO;
89
+ if ([fileManager fileExistsAtPath:fullPath isDirectory:&isDir] && isDir) {
90
+ [bundleDirs addObject:fullPath];
91
+ }
92
+ }
93
+
94
+ // Sort in descending order by modification time (keep latest 2)
95
+ [bundleDirs sortUsingComparator:^NSComparisonResult(NSString *path1, NSString *path2) {
96
+ NSDictionary *attr1 = [fileManager attributesOfItemAtPath:path1 error:nil];
97
+ NSDictionary *attr2 = [fileManager attributesOfItemAtPath:path2 error:nil];
98
+ NSDate *date1 = attr1[NSFileModificationDate] ?: [NSDate dateWithTimeIntervalSince1970:0];
99
+ NSDate *date2 = attr2[NSFileModificationDate] ?: [NSDate dateWithTimeIntervalSince1970:0];
100
+ return [date2 compare:date1];
101
+ }];
102
+
103
+ if (bundleDirs.count > 2) {
104
+ NSArray *oldBundles = [bundleDirs subarrayWithRange:NSMakeRange(2, bundleDirs.count - 2)];
105
+ for (NSString *oldBundle in oldBundles) {
106
+ NSError *delError = nil;
107
+ if ([fileManager removeItemAtPath:oldBundle error:&delError]) {
108
+ NSLog(@"Removed old bundle: %@", oldBundle);
109
+ } else {
110
+ NSLog(@"Failed to remove old bundle %@: %@", oldBundle, delError);
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ #pragma mark - Update Bundle Method
117
+
84
118
  - (void)updateBundle:(NSString *)bundleId zipUrl:(NSURL *)zipUrl completion:(void (^)(BOOL success))completion {
85
119
  if (!zipUrl) {
86
120
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -89,105 +123,182 @@ RCT_EXPORT_MODULE();
89
123
  });
90
124
  return;
91
125
  }
92
-
93
- NSString *basePath = [self stripPrefixFromPath:bundleId path:[zipUrl path]];
94
- NSString *path = [self convertFileSystemPathFromBasePath:basePath];
95
-
126
+
127
+ // Set document directory path and bundle store path
128
+ NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
129
+ NSString *bundleStoreDir = [documentsPath stringByAppendingPathComponent:@"bundle-store"];
130
+
131
+ NSFileManager *fileManager = [NSFileManager defaultManager];
132
+ if (![fileManager fileExistsAtPath:bundleStoreDir]) {
133
+ [fileManager createDirectoryAtPath:bundleStoreDir withIntermediateDirectories:YES attributes:nil error:nil];
134
+ }
135
+
136
+ // Final bundle path (bundle-store/<bundleId>)
137
+ NSString *finalBundleDir = [bundleStoreDir stringByAppendingPathComponent:bundleId];
138
+
139
+ // Check if cached bundle exists
140
+ if ([fileManager fileExistsAtPath:finalBundleDir]) {
141
+ NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:finalBundleDir];
142
+ NSString *foundBundle = nil;
143
+ for (NSString *file in enumerator) {
144
+ if ([file isEqualToString:@"index.ios.bundle"]) {
145
+ foundBundle = file;
146
+ break;
147
+ }
148
+ }
149
+ if (foundBundle) {
150
+ // Update modification time of final bundle
151
+ NSDictionary *attributes = @{NSFileModificationDate: [NSDate date]};
152
+ [fileManager setAttributes:attributes ofItemAtPath:finalBundleDir error:nil];
153
+ NSString *bundlePath = [finalBundleDir stringByAppendingPathComponent:foundBundle];
154
+ NSLog(@"Using cached bundle at path: %@", bundlePath);
155
+ [self setBundleURL:bundlePath];
156
+ [self cleanupOldBundlesAtDirectory:bundleStoreDir];
157
+ dispatch_async(dispatch_get_main_queue(), ^{
158
+ if (completion) completion(YES);
159
+ });
160
+ return;
161
+ } else {
162
+ [fileManager removeItemAtPath:finalBundleDir error:nil];
163
+ }
164
+ }
165
+
166
+ // Set up temporary folder (for download and extraction)
167
+ NSString *tempDir = [documentsPath stringByAppendingPathComponent:@"bundle-temp"];
168
+ if ([fileManager fileExistsAtPath:tempDir]) {
169
+ [fileManager removeItemAtPath:tempDir error:nil];
170
+ }
171
+ [fileManager createDirectoryAtPath:tempDir withIntermediateDirectories:YES attributes:nil error:nil];
172
+
173
+ NSString *tempZipFile = [tempDir stringByAppendingPathComponent:@"build.zip"];
174
+ NSString *extractedDir = [tempDir stringByAppendingPathComponent:@"extracted"];
175
+ [fileManager createDirectoryAtPath:extractedDir withIntermediateDirectories:YES attributes:nil error:nil];
176
+
96
177
  NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
97
178
  NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
98
-
99
- NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:zipUrl
100
- completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
179
+
180
+ NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:zipUrl completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
101
181
  if (error) {
102
182
  NSLog(@"Failed to download data from URL: %@, error: %@", zipUrl, error);
103
183
  if (completion) completion(NO);
104
184
  return;
105
185
  }
106
-
107
- NSFileManager *fileManager = [NSFileManager defaultManager];
108
- NSError *folderError;
109
-
110
- // Ensure directory exists
111
- if (![fileManager createDirectoryAtPath:[path stringByDeletingLastPathComponent]
112
- withIntermediateDirectories:YES
113
- attributes:nil
114
- error:&folderError]) {
115
- NSLog(@"Failed to create folder: %@", folderError);
116
- if (completion) completion(NO);
117
- return;
118
- }
119
-
120
- // Check if file already exists and remove it
121
- if ([fileManager fileExistsAtPath:path]) {
122
- NSError *removeError;
123
- if (![fileManager removeItemAtPath:path error:&removeError]) {
124
- NSLog(@"Failed to remove existing file: %@", removeError);
125
- if (completion) completion(NO);
126
- return;
127
- }
186
+
187
+ // Save temporary zip file
188
+ if ([fileManager fileExistsAtPath:tempZipFile]) {
189
+ [fileManager removeItemAtPath:tempZipFile error:nil];
128
190
  }
129
-
130
- NSError *moveError;
131
- if (![fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:path] error:&moveError]) {
132
- NSLog(@"Failed to save data: %@", moveError);
191
+
192
+ NSError *moveError = nil;
193
+ if (![fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:tempZipFile] error:&moveError]) {
194
+ NSLog(@"Failed to save downloaded file: %@", moveError);
133
195
  if (completion) completion(NO);
134
196
  return;
135
197
  }
136
-
137
- NSString *extractedPath = [path stringByDeletingLastPathComponent];
138
- if (![self extractZipFileAtPath:path toDestination:extractedPath]) {
198
+
199
+ // Extract zip
200
+ if (![self extractZipFileAtPath:tempZipFile toDestination:extractedDir]) {
139
201
  NSLog(@"Failed to extract zip file.");
140
202
  if (completion) completion(NO);
141
203
  return;
142
204
  }
143
-
144
- NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:extractedPath];
145
- NSString *filename = nil;
205
+
206
+ // Search for index.ios.bundle in extracted folder
207
+ NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:extractedDir];
208
+ NSString *foundBundle = nil;
146
209
  for (NSString *file in enumerator) {
147
210
  if ([file isEqualToString:@"index.ios.bundle"]) {
148
- filename = file;
211
+ foundBundle = file;
149
212
  break;
150
213
  }
151
214
  }
152
-
153
- if (filename) {
154
- NSString *bundlePath = [extractedPath stringByAppendingPathComponent:filename];
155
- NSLog(@"Setting bundle URL: %@", bundlePath);
156
- dispatch_async(dispatch_get_main_queue(), ^{
157
- [self setBundleURL:bundlePath];
158
- if (completion) completion(YES);
159
- });
160
- } else {
161
- NSLog(@"index.ios.bundle not found.");
215
+
216
+ if (!foundBundle) {
217
+ NSLog(@"index.ios.bundle not found in extracted files.");
162
218
  if (completion) completion(NO);
219
+ return;
220
+ }
221
+
222
+ // Move extracted folder to final bundle folder
223
+ if ([fileManager fileExistsAtPath:finalBundleDir]) {
224
+ [fileManager removeItemAtPath:finalBundleDir error:nil];
225
+ }
226
+ NSError *moveFinalError = nil;
227
+ BOOL moved = [fileManager moveItemAtPath:extractedDir toPath:finalBundleDir error:&moveFinalError];
228
+ if (!moved) {
229
+ // Try copy and delete if move fails
230
+ BOOL copied = [fileManager copyItemAtPath:extractedDir toPath:finalBundleDir error:&moveFinalError];
231
+ if (copied) {
232
+ [fileManager removeItemAtPath:extractedDir error:nil];
233
+ } else {
234
+ NSLog(@"Failed to move or copy extracted bundle: %@", moveFinalError);
235
+ if (completion) completion(NO);
236
+ return;
237
+ }
163
238
  }
239
+
240
+ // Recheck index.ios.bundle in final folder
241
+ NSDirectoryEnumerator *finalEnum = [fileManager enumeratorAtPath:finalBundleDir];
242
+ NSString *finalFoundBundle = nil;
243
+ for (NSString *file in finalEnum) {
244
+ if ([file isEqualToString:@"index.ios.bundle"]) {
245
+ finalFoundBundle = file;
246
+ break;
247
+ }
248
+ }
249
+
250
+ if (!finalFoundBundle) {
251
+ NSLog(@"index.ios.bundle not found in final directory.");
252
+ if (completion) completion(NO);
253
+ return;
254
+ }
255
+
256
+ // Update modification time of final bundle
257
+ NSDictionary *attributes = @{NSFileModificationDate: [NSDate date]};
258
+ [fileManager setAttributes:attributes ofItemAtPath:finalBundleDir error:nil];
259
+
260
+ NSString *bundlePath = [finalBundleDir stringByAppendingPathComponent:finalFoundBundle];
261
+ NSLog(@"Setting bundle URL: %@", bundlePath);
262
+ dispatch_async(dispatch_get_main_queue(), ^{
263
+ [self setBundleURL:bundlePath];
264
+ [self cleanupOldBundlesAtDirectory:bundleStoreDir];
265
+ [fileManager removeItemAtPath:tempDir error:nil];
266
+ if (completion) completion(YES);
267
+ });
164
268
  }];
165
-
166
269
 
167
- // Add observer for progress updates
168
- [downloadTask addObserver:self
169
- forKeyPath:@"countOfBytesReceived"
170
- options:NSKeyValueObservingOptionNew
171
- context:nil];
172
- [downloadTask addObserver:self
173
- forKeyPath:@"countOfBytesExpectedToReceive"
174
- options:NSKeyValueObservingOptionNew
175
- context:nil];
176
-
177
- __block HotUpdater *weakSelf = self;
270
+ // Register KVO for progress updates
271
+ [downloadTask addObserver:self forKeyPath:@"countOfBytesReceived" options:NSKeyValueObservingOptionNew context:nil];
272
+ [downloadTask addObserver:self forKeyPath:@"countOfBytesExpectedToReceive" options:NSKeyValueObservingOptionNew context:nil];
273
+
274
+ __weak HotUpdater *weakSelf = self;
178
275
  [[NSNotificationCenter defaultCenter] addObserverForName:@"NSURLSessionDownloadTaskDidFinishDownloading"
179
- object:downloadTask
180
- queue:[NSOperationQueue mainQueue]
181
- usingBlock:^(NSNotification * _Nonnull note) {
276
+ object:downloadTask
277
+ queue:[NSOperationQueue mainQueue]
278
+ usingBlock:^(NSNotification * _Nonnull note) {
182
279
  [weakSelf removeObserversForTask:downloadTask];
183
280
  }];
281
+
184
282
  [downloadTask resume];
283
+ }
284
+
285
+ #pragma mark - Folder Deletion Utility
185
286
 
287
+ - (void)deleteFolderIfExists:(NSString *)path {
288
+ NSFileManager *fileManager = [NSFileManager defaultManager];
289
+ if ([fileManager fileExistsAtPath:path]) {
290
+ NSError *error;
291
+ [fileManager removeItemAtPath:path error:&error];
292
+ if (error) {
293
+ NSLog(@"Failed to delete existing folder: %@", error);
294
+ } else {
295
+ NSLog(@"Successfully deleted existing folder: %@", path);
296
+ }
297
+ }
186
298
  }
187
299
 
188
300
  #pragma mark - Progress Updates
189
301
 
190
-
191
302
  - (void)removeObserversForTask:(NSURLSessionDownloadTask *)task {
192
303
  @try {
193
304
  if ([task observationInfo]) {
@@ -200,53 +311,41 @@ RCT_EXPORT_MODULE();
200
311
  }
201
312
  }
202
313
 
203
-
204
314
  - (void)observeValueForKeyPath:(NSString *)keyPath
205
315
  ofObject:(id)object
206
316
  change:(NSDictionary<NSKeyValueChangeKey, id> *)change
207
317
  context:(void *)context {
208
318
  if ([keyPath isEqualToString:@"countOfBytesReceived"] || [keyPath isEqualToString:@"countOfBytesExpectedToReceive"]) {
209
319
  NSURLSessionDownloadTask *task = (NSURLSessionDownloadTask *)object;
210
-
211
320
  if (task.countOfBytesExpectedToReceive > 0) {
212
321
  double progress = (double)task.countOfBytesReceived / (double)task.countOfBytesExpectedToReceive;
213
-
214
- // Get current timestamp
215
- NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970] * 1000; // Convert to milliseconds
216
-
217
- // Send event only if 100ms has passed OR progress is 100%
322
+ NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970] * 1000; // In milliseconds
218
323
  if ((currentTime - self.lastUpdateTime) >= 100 || progress >= 1.0) {
219
- self.lastUpdateTime = currentTime; // Update last event timestamp
220
-
221
- // Send progress to React Native
324
+ self.lastUpdateTime = currentTime;
222
325
  [self sendEventWithName:@"onProgress" body:@{@"progress": @(progress)}];
223
326
  }
224
327
  }
225
328
  }
226
329
  }
227
330
 
228
-
229
331
  #pragma mark - React Native Events
332
+
230
333
  - (NSArray<NSString *> *)supportedEvents {
231
334
  return @[@"onProgress"];
232
335
  }
233
336
 
234
- - (void)startObserving
235
- {
337
+ - (void)startObserving {
236
338
  hasListeners = YES;
237
339
  }
238
340
 
239
- - (void)stopObserving
240
- {
341
+ - (void)stopObserving {
241
342
  hasListeners = NO;
242
343
  }
243
344
 
244
-
245
345
  - (void)sendEventWithName:(NSString * _Nonnull)name result:(NSDictionary *)result {
246
346
  [self sendEventWithName:name body:result];
247
347
  }
248
348
 
249
-
250
349
  #pragma mark - React Native Exports
251
350
 
252
351
  RCT_EXPORT_METHOD(reload) {
@@ -257,8 +356,7 @@ RCT_EXPORT_METHOD(reload) {
257
356
  });
258
357
  }
259
358
 
260
- RCT_EXPORT_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve
261
- reject:(RCTPromiseRejectBlock)reject) {
359
+ RCT_EXPORT_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
262
360
  NSString *version = [self getAppVersion];
263
361
  resolve(version ?: [NSNull null]);
264
362
  }
@@ -268,7 +366,6 @@ RCT_EXPORT_METHOD(updateBundle:(NSString *)bundleId zipUrl:(NSString *)zipUrlStr
268
366
  if (![zipUrlString isEqualToString:@""]) {
269
367
  zipUrl = [NSURL URLWithString:zipUrlString];
270
368
  }
271
-
272
369
  [self updateBundle:bundleId zipUrl:zipUrl completion:^(BOOL success) {
273
370
  dispatch_async(dispatch_get_main_queue(), ^{
274
371
  resolve(@[@(success)]);
@@ -276,14 +373,10 @@ RCT_EXPORT_METHOD(updateBundle:(NSString *)bundleId zipUrl:(NSString *)zipUrlStr
276
373
  }];
277
374
  }
278
375
 
279
-
280
- // Don't compile this code when we build for the old architecture.
281
376
  #ifdef RCT_NEW_ARCH_ENABLED
282
- - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
283
- (const facebook::react::ObjCTurboModule::InitParams &)params
284
- {
377
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
285
378
  return std::make_shared<facebook::react::NativeHotUpdaterSpecJSI>(params);
286
379
  }
287
380
  #endif
288
381
 
289
- @end
382
+ @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/react-native",
3
- "version": "0.10.2",
3
+ "version": "0.12.0",
4
4
  "description": "React Native OTA solution for self-hosted",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -78,8 +78,8 @@
78
78
  "react-native-builder-bob": "^0.33.1"
79
79
  },
80
80
  "dependencies": {
81
- "@hot-updater/js": "0.10.2",
82
- "@hot-updater/core": "0.10.2"
81
+ "@hot-updater/js": "0.12.0",
82
+ "@hot-updater/core": "0.12.0"
83
83
  },
84
84
  "scripts": {
85
85
  "build": "rslib build",
@@ -12,9 +12,6 @@ export interface CheckForUpdateConfig {
12
12
 
13
13
  export async function checkForUpdate(config: CheckForUpdateConfig) {
14
14
  if (__DEV__) {
15
- console.warn(
16
- "[HotUpdater] __DEV__ is true, HotUpdater is only supported in production",
17
- );
18
15
  return null;
19
16
  }
20
17
 
package/src/native.ts CHANGED
@@ -52,7 +52,7 @@ export const addListener = <T extends keyof HotUpdaterEvent>(
52
52
  /**
53
53
  * Downloads files from given URLs.
54
54
  *
55
- * @param {string} bundleId - identifier for the bundle version.
55
+ * @param {string} bundleId - identifier for the bundle id.
56
56
  * @param {string | null} zipUrl - zip file URL. If null, it means rolling back to the built-in bundle
57
57
  * @returns {Promise<boolean>} Resolves with true if download was successful, otherwise rejects with an error.
58
58
  */
@@ -74,7 +74,9 @@ export const getAppVersion = (): Promise<string | null> => {
74
74
  * Reloads the app.
75
75
  */
76
76
  export const reload = () => {
77
- HotUpdaterNative.reload();
77
+ requestAnimationFrame(() => {
78
+ HotUpdaterNative.reload();
79
+ });
78
80
  };
79
81
 
80
82
  /**