react-native-ota-hot-update 2.3.5 → 2.4.0-rc.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 (38) hide show
  1. package/android/generated/java/com/otahotupdate/NativeOtaHotUpdateSpec.java +14 -1
  2. package/android/generated/jni/RNOtaHotUpdateSpec-generated.cpp +20 -2
  3. package/android/generated/jni/react/renderer/components/RNOtaHotUpdateSpec/RNOtaHotUpdateSpecJSI-generated.cpp +26 -2
  4. package/android/generated/jni/react/renderer/components/RNOtaHotUpdateSpec/RNOtaHotUpdateSpecJSI.h +32 -5
  5. package/android/gradle.properties +4 -0
  6. package/android/src/main/java/com/otahotupdate/OtaHotUpdate.kt +6 -1
  7. package/android/src/main/java/com/otahotupdate/OtaHotUpdateModule.kt +365 -36
  8. package/android/src/main/java/com/otahotupdate/SharedPrefs.kt +12 -0
  9. package/android/src/main/java/com/otahotupdate/Utils.kt +9 -3
  10. package/android/src/oldarch/OtaHotUpdateSpec.kt +4 -1
  11. package/ios/OtaHotUpdate.mm +383 -42
  12. package/ios/generated/RNOtaHotUpdateSpec/RNOtaHotUpdateSpec-generated.mm +23 -2
  13. package/ios/generated/RNOtaHotUpdateSpec/RNOtaHotUpdateSpec.h +12 -0
  14. package/ios/generated/RNOtaHotUpdateSpecJSI-generated.cpp +26 -2
  15. package/ios/generated/RNOtaHotUpdateSpecJSI.h +32 -5
  16. package/lib/commonjs/NativeOtaHotUpdate.js.map +1 -1
  17. package/lib/commonjs/index.js +26 -3
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/module/NativeOtaHotUpdate.js.map +1 -1
  20. package/lib/module/index.js +26 -3
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/typescript/commonjs/src/NativeOtaHotUpdate.d.ts +4 -1
  23. package/lib/typescript/commonjs/src/NativeOtaHotUpdate.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/src/index.d.ts +8 -2
  25. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/src/type.d.ts +36 -0
  27. package/lib/typescript/commonjs/src/type.d.ts.map +1 -1
  28. package/lib/typescript/module/src/NativeOtaHotUpdate.d.ts +4 -1
  29. package/lib/typescript/module/src/NativeOtaHotUpdate.d.ts.map +1 -1
  30. package/lib/typescript/module/src/index.d.ts +8 -2
  31. package/lib/typescript/module/src/index.d.ts.map +1 -1
  32. package/lib/typescript/module/src/type.d.ts +36 -0
  33. package/lib/typescript/module/src/type.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/NativeOtaHotUpdate.ts +4 -1
  36. package/src/index.d.ts +25 -2
  37. package/src/index.tsx +36 -5
  38. package/src/type.ts +44 -1
@@ -2,6 +2,13 @@
2
2
  #import <SSZipArchive/SSZipArchive.h>
3
3
  #include <signal.h>
4
4
 
5
+ @interface OtaHotUpdate ()
6
+ - (NSArray *)loadBundleHistory;
7
+ - (void)saveBundleHistory:(NSArray *)history;
8
+ - (void)saveBundleVersion:(NSString *)path version:(NSInteger)version maxVersions:(NSInteger)maxVersions metadata:(NSString *)metadata;
9
+ - (NSString *)extractFolderName:(NSString *)path;
10
+ @end
11
+
5
12
  static NSUncaughtExceptionHandler *previousHandler = NULL;
6
13
  static BOOL isBeginning = YES;
7
14
  @implementation OtaHotUpdate
@@ -130,6 +137,18 @@ void OTAExceptionHandler(NSException *exception) {
130
137
  return success;
131
138
  }
132
139
 
140
+ + (BOOL)deleteBundleAtPath:(NSString *)path {
141
+ if (!path || path.length == 0) {
142
+ return NO;
143
+ }
144
+ NSError *error = nil;
145
+ if ([self isFilePathValid:path]) {
146
+ BOOL isDeleted = [self deleteAllContentsOfParentDirectoryOfFile:path error:&error];
147
+ return isDeleted;
148
+ }
149
+ return NO;
150
+ }
151
+
133
152
  + (BOOL)removeBundleIfNeeded:(NSString *)pathKey {
134
153
  NSString *keyToUse = pathKey ? pathKey : @"OLD_PATH";
135
154
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
@@ -198,7 +217,7 @@ void OTAExceptionHandler(NSException *exception) {
198
217
 
199
218
  return nil;
200
219
  }
201
- - (NSString *)renameExtractedFolderInDirectory:(NSString *)directoryPath {
220
+ - (NSString *)renameExtractedFolderInDirectory:(NSString *)directoryPath version:(NSNumber *)version {
202
221
  NSFileManager *fileManager = [NSFileManager defaultManager];
203
222
  NSError *error = nil;
204
223
 
@@ -213,9 +232,15 @@ void OTAExceptionHandler(NSException *exception) {
213
232
  NSString *originalFolderName = contents.firstObject;
214
233
  NSString *originalFolderPath = [directoryPath stringByAppendingPathComponent:originalFolderName];
215
234
 
216
- // Generate new folder name with timestamp
217
- NSString *timestamp = [NSString stringWithFormat:@"output_%ld", (long)[[NSDate date] timeIntervalSince1970]];
218
- NSString *newFolderPath = [directoryPath stringByAppendingPathComponent:timestamp];
235
+ // Generate new folder name with version and timestamp
236
+ NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
237
+ [dateFormatter setDateFormat:@"yyyy_MM_dd_HH_mm"];
238
+ [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
239
+ NSString *timestamp = [dateFormatter stringFromDate:[NSDate date]];
240
+ NSString *folderName = version != nil
241
+ ? [NSString stringWithFormat:@"output_v%@_%@", version, timestamp]
242
+ : [NSString stringWithFormat:@"output_%@", timestamp];
243
+ NSString *newFolderPath = [directoryPath stringByAppendingPathComponent:folderName];
219
244
 
220
245
  // Rename the extracted folder
221
246
  if (![fileManager moveItemAtPath:originalFolderPath toPath:newFolderPath error:&error]) {
@@ -226,7 +251,7 @@ void OTAExceptionHandler(NSException *exception) {
226
251
  NSLog(@"Renamed extracted folder to: %@", newFolderPath);
227
252
  return newFolderPath;
228
253
  }
229
- - (NSString *)unzipFileAtPath:(NSString *)zipFilePath extension:(NSString *)extension {
254
+ - (NSString *)unzipFileAtPath:(NSString *)zipFilePath extension:(NSString *)extension version:(NSNumber *)version {
230
255
  // Define the directory where the files will be extracted
231
256
  NSString *extractedFolderPath = [[zipFilePath stringByDeletingPathExtension] stringByAppendingPathExtension:@"unzip"];
232
257
 
@@ -250,7 +275,7 @@ void OTAExceptionHandler(NSException *exception) {
250
275
  return nil;
251
276
  }
252
277
  // Try renaming the extracted folder
253
- NSString *renamedFolderPath = [self renameExtractedFolderInDirectory:extractedFolderPath];
278
+ NSString *renamedFolderPath = [self renameExtractedFolderInDirectory:extractedFolderPath version:version];
254
279
 
255
280
  // If renaming fails, use the original extracted folder path
256
281
  NSString *finalFolderPath = renamedFolderPath ? renamedFolderPath : extractedFolderPath;
@@ -268,20 +293,28 @@ void OTAExceptionHandler(NSException *exception) {
268
293
  }
269
294
 
270
295
  // Expose setupBundlePath method to JavaScript
271
- RCT_EXPORT_METHOD(setupBundlePath:(NSString *)path extension:(NSString *)extension
296
+ RCT_EXPORT_METHOD(setupBundlePath:(NSString *)path extension:(NSString *)extension version:(NSNumber *)version maxVersions:(NSNumber *)maxVersions metadata:(NSString *)metadata
272
297
  resolve:(RCTPromiseResolveBlock)resolve
273
298
  reject:(RCTPromiseRejectBlock)reject) {
274
299
  if ([OtaHotUpdate isFilePathValid:path]) {
275
300
  [OtaHotUpdate removeBundleIfNeeded:nil];
276
301
  //Unzip file
277
- NSString *extractedFilePath = [self unzipFileAtPath:path extension:(extension != nil) ? extension : @".jsbundle"];
302
+ NSString *extractedFilePath = [self unzipFileAtPath:path extension:(extension != nil) ? extension : @".jsbundle" version:version];
278
303
  if (extractedFilePath) {
279
304
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
280
305
  NSString *oldPath = [defaults stringForKey:@"PATH"];
281
- if (oldPath) {
282
- [defaults setObject:oldPath forKey:@"OLD_PATH"];
306
+
307
+ // If version is provided, save to history system
308
+ if (version != nil) {
309
+ // Default maxVersions to 2 if not provided (backward compatible)
310
+ NSInteger defaultMaxVersions = 2;
311
+ NSInteger maxVersionsToKeep = maxVersions != nil ? [maxVersions integerValue] : defaultMaxVersions;
312
+ [self saveBundleVersion:extractedFilePath version:[version integerValue] maxVersions:maxVersionsToKeep metadata:metadata];
313
+ } else {
314
+ // No version (e.g., Git update) - just set path, no history
315
+ [defaults setObject:extractedFilePath forKey:@"PATH"];
283
316
  }
284
- [defaults setObject:extractedFilePath forKey:@"PATH"];
317
+
285
318
  [defaults setObject:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"] forKey:@"VERSION_NAME"];
286
319
  [defaults synchronize];
287
320
  isBeginning = YES;
@@ -296,41 +329,78 @@ RCT_EXPORT_METHOD(setupBundlePath:(NSString *)path extension:(NSString *)extensi
296
329
  RCT_EXPORT_METHOD(deleteBundle:(double)i
297
330
  resolve:(RCTPromiseResolveBlock)resolve
298
331
  reject:(RCTPromiseRejectBlock)reject) {
332
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
333
+ NSString *currentPath = [defaults stringForKey:@"PATH"];
334
+
335
+ // Delete current bundle from file system
299
336
  BOOL isDeleted = [OtaHotUpdate removeBundleIfNeeded:@"PATH"];
300
- BOOL isDeletedOld = [OtaHotUpdate removeBundleIfNeeded:nil];
301
- if (isDeleted && isDeletedOld) {
302
- resolve(@(YES));
303
- } else {
304
- resolve(@(NO));
305
- }
337
+
338
+ // Remove current bundle from history if exists
339
+ if (currentPath && currentPath.length > 0) {
340
+ NSArray *history = [self loadBundleHistory];
341
+ NSMutableArray *updatedHistory = [NSMutableArray array];
342
+ for (NSDictionary *bundle in history) {
343
+ if (![bundle[@"path"] isEqualToString:currentPath]) {
344
+ [updatedHistory addObject:bundle];
345
+ }
346
+ }
347
+ [self saveBundleHistory:updatedHistory];
348
+ }
349
+
350
+ // Clear paths and version
351
+ [defaults removeObjectForKey:@"PATH"];
352
+ [defaults setObject:@"0" forKey:@"VERSION"];
353
+ [defaults synchronize];
354
+
355
+ resolve(@(isDeleted));
306
356
  }
307
357
  // Expose deleteBundle method to JavaScript
308
358
  RCT_EXPORT_METHOD(rollbackToPreviousBundle:(double)i
309
359
  resolve:(RCTPromiseResolveBlock)resolve
310
360
  reject:(RCTPromiseRejectBlock)reject) {
311
361
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
312
- NSString *oldPath = [defaults stringForKey:@"OLD_PATH"];
313
- if (oldPath && [OtaHotUpdate isFilePathValid:oldPath]) {
314
- BOOL isDeleted = [OtaHotUpdate removeBundleIfNeeded:@"PATH"];
315
- if (isDeleted) {
316
- NSString *previousVersion = [defaults stringForKey:@"PREVIOUS_VERSION"];
317
- if (previousVersion) {
318
- [defaults setObject:previousVersion forKey:@"VERSION"];
319
- [defaults removeObjectForKey:@"PREVIOUS_VERSION"];
320
- } else {
321
- [defaults removeObjectForKey:@"VERSION"];
362
+ NSString *currentPath = [defaults stringForKey:@"PATH"];
363
+
364
+ // Use history to find previous version
365
+ NSArray *history = [self loadBundleHistory];
366
+ if (history.count > 0 && currentPath && currentPath.length > 0) {
367
+ // Find current bundle in history
368
+ NSDictionary *currentBundle = nil;
369
+ for (NSDictionary *bundle in history) {
370
+ if ([bundle[@"path"] isEqualToString:currentPath]) {
371
+ currentBundle = bundle;
372
+ break;
373
+ }
374
+ }
375
+
376
+ if (currentBundle) {
377
+ // Find previous version (older than current, max version)
378
+ NSDictionary *previousBundle = nil;
379
+ NSInteger currentVersion = [currentBundle[@"version"] integerValue];
380
+ for (NSDictionary *bundle in history) {
381
+ NSInteger bundleVersion = [bundle[@"version"] integerValue];
382
+ if (bundleVersion < currentVersion) {
383
+ if (!previousBundle || [bundle[@"version"] integerValue] > [previousBundle[@"version"] integerValue]) {
384
+ previousBundle = bundle;
385
+ }
322
386
  }
323
-
324
- [defaults setObject:oldPath forKey:@"PATH"];
325
- [defaults removeObjectForKey:@"OLD_PATH"];
326
- [defaults synchronize];
327
- resolve(@(YES));
328
- } else {
329
- resolve(@(NO));
387
+ }
388
+
389
+ if (previousBundle && [OtaHotUpdate isFilePathValid:previousBundle[@"path"]]) {
390
+ // Rollback to previous bundle from history
391
+ BOOL isDeleted = [OtaHotUpdate removeBundleIfNeeded:@"PATH"];
392
+ if (isDeleted) {
393
+ [defaults setObject:previousBundle[@"path"] forKey:@"PATH"];
394
+ [defaults setObject:[NSString stringWithFormat:@"%@", previousBundle[@"version"]] forKey:@"VERSION"];
395
+ [defaults synchronize];
396
+ resolve(@(YES));
397
+ return;
330
398
  }
331
- } else {
332
- resolve(@(NO));
333
399
  }
400
+ }
401
+ }
402
+
403
+ resolve(@(NO));
334
404
  }
335
405
 
336
406
  RCT_EXPORT_METHOD(getCurrentVersion:(double)a
@@ -350,13 +420,8 @@ RCT_EXPORT_METHOD(setCurrentVersion:(NSString *)version
350
420
  resolve:(RCTPromiseResolveBlock)resolve
351
421
  reject:(RCTPromiseRejectBlock)reject) {
352
422
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
353
- NSString *currentVersion = [defaults stringForKey:@"VERSION"];
354
-
423
+ // No longer save PREVIOUS_VERSION, use history instead
355
424
  if (version) {
356
- if (currentVersion && currentVersion != version) {
357
- [defaults setObject:currentVersion forKey:@"PREVIOUS_VERSION"];
358
- }
359
-
360
425
  [defaults setObject:version forKey:@"VERSION"];
361
426
  [defaults synchronize];
362
427
  resolve(@(YES));
@@ -410,6 +475,162 @@ RCT_EXPORT_METHOD(setExactBundlePath:(NSString *)path
410
475
  {
411
476
  RCTTriggerReloadCommandListeners(@"react-native-ota-hot-update: Restart");
412
477
  }
478
+ - (NSArray *)loadBundleHistory {
479
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
480
+ NSString *historyJson = [defaults stringForKey:@"BUNDLE_HISTORY"];
481
+
482
+ // If history exists, load it
483
+ if (historyJson && historyJson.length > 0) {
484
+ NSData *data = [historyJson dataUsingEncoding:NSUTF8StringEncoding];
485
+ NSError *error = nil;
486
+ NSArray *history = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
487
+ if (!error && [history isKindOfClass:[NSArray class]]) {
488
+ return history;
489
+ }
490
+ }
491
+
492
+ // Migration: If history is empty but PATH exists, migrate from old system
493
+ NSString *currentPath = [defaults stringForKey:@"PATH"];
494
+ NSString *currentVersion = [defaults stringForKey:@"VERSION"];
495
+ NSString *previousPath = [defaults stringForKey:@"OLD_PATH"];
496
+ NSString *previousVersion = [defaults stringForKey:@"PREVIOUS_VERSION"];
497
+
498
+ if (!currentPath || currentPath.length == 0) {
499
+ return @[];
500
+ }
501
+
502
+ // Migrate current bundle
503
+ NSMutableArray *migratedHistory = [NSMutableArray array];
504
+
505
+ // Add current bundle if has version
506
+ if (currentVersion && currentVersion.length > 0) {
507
+ NSInteger version = [currentVersion integerValue];
508
+ if (version > 0 && [OtaHotUpdate isFilePathValid:currentPath]) {
509
+ NSFileManager *fileManager = [NSFileManager defaultManager];
510
+ NSDictionary *attributes = [fileManager attributesOfItemAtPath:currentPath error:nil];
511
+ NSDate *modificationDate = attributes[NSFileModificationDate];
512
+ long long timestamp = modificationDate ? (long long)([modificationDate timeIntervalSince1970] * 1000) : (long long)([[NSDate date] timeIntervalSince1970] * 1000);
513
+
514
+ NSMutableDictionary *bundle = [NSMutableDictionary dictionary];
515
+ bundle[@"version"] = @(version);
516
+ bundle[@"path"] = currentPath;
517
+ bundle[@"timestamp"] = @(timestamp);
518
+ bundle[@"metadata"] = [NSNull null];
519
+ [migratedHistory addObject:bundle];
520
+ }
521
+ }
522
+
523
+ // Add previous bundle if exists
524
+ if (previousPath && previousPath.length > 0 && previousVersion && previousVersion.length > 0) {
525
+ NSInteger version = [previousVersion integerValue];
526
+ if (version > 0 && [OtaHotUpdate isFilePathValid:previousPath]) {
527
+ NSFileManager *fileManager = [NSFileManager defaultManager];
528
+ NSDictionary *attributes = [fileManager attributesOfItemAtPath:previousPath error:nil];
529
+ NSDate *modificationDate = attributes[NSFileModificationDate];
530
+ long long timestamp = modificationDate ? (long long)([modificationDate timeIntervalSince1970] * 1000) : (long long)([[NSDate date] timeIntervalSince1970] * 1000);
531
+
532
+ NSMutableDictionary *bundle = [NSMutableDictionary dictionary];
533
+ bundle[@"version"] = @(version);
534
+ bundle[@"path"] = previousPath;
535
+ bundle[@"timestamp"] = @(timestamp);
536
+ bundle[@"metadata"] = [NSNull null];
537
+ [migratedHistory addObject:bundle];
538
+ }
539
+ }
540
+
541
+ // Save migrated history if any
542
+ if (migratedHistory.count > 0) {
543
+ // Sort by version descending
544
+ NSArray *sortedHistory = [migratedHistory sortedArrayUsingComparator:^NSComparisonResult(NSDictionary *obj1, NSDictionary *obj2) {
545
+ NSInteger v1 = [obj1[@"version"] integerValue];
546
+ NSInteger v2 = [obj2[@"version"] integerValue];
547
+ if (v1 > v2) return NSOrderedAscending;
548
+ if (v1 < v2) return NSOrderedDescending;
549
+ return NSOrderedSame;
550
+ }];
551
+ [self saveBundleHistory:sortedHistory];
552
+ return sortedHistory;
553
+ }
554
+
555
+ return @[];
556
+ }
557
+
558
+ - (void)saveBundleHistory:(NSArray *)history {
559
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
560
+ NSError *error = nil;
561
+ NSData *data = [NSJSONSerialization dataWithJSONObject:history options:0 error:&error];
562
+ if (!error && data) {
563
+ NSString *historyJson = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
564
+ [defaults setObject:historyJson forKey:@"BUNDLE_HISTORY"];
565
+ [defaults synchronize];
566
+ }
567
+ }
568
+
569
+ - (NSString *)extractFolderName:(NSString *)path {
570
+ return [[path stringByDeletingLastPathComponent] lastPathComponent];
571
+ }
572
+
573
+ - (void)saveBundleVersion:(NSString *)path version:(NSInteger)version maxVersions:(NSInteger)maxVersions metadata:(NSString *)metadata {
574
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
575
+ NSArray *history = [self loadBundleHistory];
576
+
577
+ // Create new bundle entry
578
+ NSMutableDictionary *newBundle = [NSMutableDictionary dictionary];
579
+ newBundle[@"version"] = @(version);
580
+ newBundle[@"path"] = path;
581
+ newBundle[@"timestamp"] = @((long long)([[NSDate date] timeIntervalSince1970] * 1000));
582
+ if (metadata) {
583
+ newBundle[@"metadata"] = metadata;
584
+ }
585
+
586
+ // Combine with existing history
587
+ NSMutableArray *updatedHistory = [NSMutableArray arrayWithObject:newBundle];
588
+ [updatedHistory addObjectsFromArray:history];
589
+
590
+ // Sort by version descending and remove duplicates
591
+ [updatedHistory sortUsingComparator:^NSComparisonResult(NSDictionary *obj1, NSDictionary *obj2) {
592
+ NSInteger v1 = [obj1[@"version"] integerValue];
593
+ NSInteger v2 = [obj2[@"version"] integerValue];
594
+ if (v1 > v2) return NSOrderedAscending;
595
+ if (v1 < v2) return NSOrderedDescending;
596
+ return NSOrderedSame;
597
+ }];
598
+
599
+ // Remove duplicates by version
600
+ NSMutableArray *uniqueHistory = [NSMutableArray array];
601
+ NSMutableSet *seenVersions = [NSMutableSet set];
602
+ for (NSDictionary *bundle in updatedHistory) {
603
+ NSInteger v = [bundle[@"version"] integerValue];
604
+ if (![seenVersions containsObject:@(v)]) {
605
+ [seenVersions addObject:@(v)];
606
+ [uniqueHistory addObject:bundle];
607
+ }
608
+ }
609
+
610
+ // Keep only maxVersions most recent
611
+ NSArray *finalHistory = [uniqueHistory subarrayWithRange:NSMakeRange(0, MIN(maxVersions, uniqueHistory.count))];
612
+
613
+ // Delete old versions beyond limit
614
+ NSMutableSet *versionsToKeep = [NSMutableSet set];
615
+ for (NSDictionary *bundle in finalHistory) {
616
+ [versionsToKeep addObject:bundle[@"version"]];
617
+ }
618
+
619
+ for (NSDictionary *bundle in uniqueHistory) {
620
+ if (![versionsToKeep containsObject:bundle[@"version"]]) {
621
+ [OtaHotUpdate deleteBundleAtPath:bundle[@"path"]];
622
+ }
623
+ }
624
+
625
+ // Save updated history
626
+ [self saveBundleHistory:finalHistory];
627
+
628
+ // Set current path and version
629
+ [defaults setObject:path forKey:@"PATH"];
630
+ [defaults setObject:[NSString stringWithFormat:@"%ld", (long)version] forKey:@"VERSION"];
631
+ [defaults synchronize];
632
+ }
633
+
413
634
  RCT_EXPORT_METHOD(restart) {
414
635
  if ([NSThread isMainThread]) {
415
636
  [self loadBundle];
@@ -422,6 +643,126 @@ RCT_EXPORT_METHOD(restart) {
422
643
  }
423
644
 
424
645
 
646
+ RCT_EXPORT_METHOD(getBundleList:(double)a
647
+ resolve:(RCTPromiseResolveBlock)resolve
648
+ reject:(RCTPromiseRejectBlock)reject) {
649
+ NSArray *history = [self loadBundleHistory];
650
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
651
+ NSString *activePath = [defaults stringForKey:@"PATH"];
652
+
653
+ NSMutableArray *bundleList = [NSMutableArray array];
654
+ for (NSDictionary *bundle in history) {
655
+ NSString *path = bundle[@"path"];
656
+ NSString *folderName = [self extractFolderName:path];
657
+ NSMutableDictionary *bundleInfo = [NSMutableDictionary dictionary];
658
+ bundleInfo[@"id"] = folderName;
659
+ bundleInfo[@"version"] = bundle[@"version"];
660
+ bundleInfo[@"date"] = bundle[@"timestamp"];
661
+ bundleInfo[@"path"] = path;
662
+ bundleInfo[@"isActive"] = @([path isEqualToString:activePath]);
663
+ if (bundle[@"metadata"]) {
664
+ NSError *error = nil;
665
+ id metadata = [NSJSONSerialization JSONObjectWithData:[bundle[@"metadata"] dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error];
666
+ bundleInfo[@"metadata"] = error ? bundle[@"metadata"] : metadata;
667
+ } else {
668
+ bundleInfo[@"metadata"] = [NSNull null];
669
+ }
670
+ [bundleList addObject:bundleInfo];
671
+ }
672
+
673
+ NSError *error = nil;
674
+ NSData *data = [NSJSONSerialization dataWithJSONObject:bundleList options:0 error:&error];
675
+ if (!error && data) {
676
+ NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
677
+ resolve(jsonString);
678
+ } else {
679
+ reject(@"GET_BUNDLE_LIST_ERROR", @"Failed to serialize bundle list", error);
680
+ }
681
+ }
682
+
683
+ RCT_EXPORT_METHOD(deleteBundleById:(NSString *)id
684
+ resolve:(RCTPromiseResolveBlock)resolve
685
+ reject:(RCTPromiseRejectBlock)reject) {
686
+ NSArray *history = [self loadBundleHistory];
687
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
688
+ NSString *activePath = [defaults stringForKey:@"PATH"];
689
+
690
+ NSDictionary *bundleToDelete = nil;
691
+ for (NSDictionary *bundle in history) {
692
+ NSString *folderName = [self extractFolderName:bundle[@"path"]];
693
+ if ([folderName isEqualToString:id]) {
694
+ bundleToDelete = bundle;
695
+ break;
696
+ }
697
+ }
698
+
699
+ if (!bundleToDelete) {
700
+ resolve(@(NO));
701
+ return;
702
+ }
703
+
704
+ // If deleting active bundle, rollback to oldest remaining bundle or clear
705
+ if ([bundleToDelete[@"path"] isEqualToString:activePath]) {
706
+ NSMutableArray *remainingBundles = [NSMutableArray array];
707
+ for (NSDictionary *bundle in history) {
708
+ if (![bundle[@"path"] isEqualToString:bundleToDelete[@"path"]]) {
709
+ [remainingBundles addObject:bundle];
710
+ }
711
+ }
712
+ if (remainingBundles.count > 0) {
713
+ NSDictionary *oldestBundle = [remainingBundles sortedArrayUsingComparator:^NSComparisonResult(NSDictionary *obj1, NSDictionary *obj2) {
714
+ NSInteger v1 = [obj1[@"version"] integerValue];
715
+ NSInteger v2 = [obj2[@"version"] integerValue];
716
+ if (v1 < v2) return NSOrderedAscending;
717
+ if (v1 > v2) return NSOrderedDescending;
718
+ return NSOrderedSame;
719
+ }].firstObject;
720
+ [defaults setObject:oldestBundle[@"path"] forKey:@"PATH"];
721
+ [defaults setObject:[NSString stringWithFormat:@"%@", oldestBundle[@"version"]] forKey:@"VERSION"];
722
+ } else {
723
+ [defaults removeObjectForKey:@"PATH"];
724
+ [defaults removeObjectForKey:@"VERSION"];
725
+ }
726
+ [defaults synchronize];
727
+ }
728
+
729
+ // Delete bundle folder
730
+ BOOL isDeleted = [OtaHotUpdate deleteBundleAtPath:bundleToDelete[@"path"]];
731
+
732
+ // Remove from history
733
+ NSMutableArray *updatedHistory = [NSMutableArray array];
734
+ for (NSDictionary *bundle in history) {
735
+ if (![bundle[@"path"] isEqualToString:bundleToDelete[@"path"]]) {
736
+ [updatedHistory addObject:bundle];
737
+ }
738
+ }
739
+ [self saveBundleHistory:updatedHistory];
740
+
741
+ resolve(@(isDeleted));
742
+ }
743
+
744
+ RCT_EXPORT_METHOD(clearAllBundles:(double)a
745
+ resolve:(RCTPromiseResolveBlock)resolve
746
+ reject:(RCTPromiseRejectBlock)reject) {
747
+ NSArray *history = [self loadBundleHistory];
748
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
749
+
750
+ // Delete all bundle folders
751
+ for (NSDictionary *bundle in history) {
752
+ [OtaHotUpdate deleteBundleAtPath:bundle[@"path"]];
753
+ }
754
+
755
+ // Clear history
756
+ [self saveBundleHistory:@[]];
757
+
758
+ // Clear current path and version
759
+ [defaults removeObjectForKey:@"PATH"];
760
+ [defaults removeObjectForKey:@"VERSION"];
761
+ [defaults synchronize];
762
+
763
+ resolve(@(YES));
764
+ }
765
+
425
766
  // Don't compile this code when we build for the old architecture.
426
767
  #ifdef RCT_NEW_ARCH_ENABLED
427
768
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
@@ -27,7 +27,7 @@
27
27
  namespace facebook::react {
28
28
 
29
29
  static facebook::jsi::Value __hostFunction_NativeOtaHotUpdateSpecJSI_setupBundlePath(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
30
- return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "setupBundlePath", @selector(setupBundlePath:extension:resolve:reject:), args, count);
30
+ return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "setupBundlePath", @selector(setupBundlePath:extension:version:maxVersions:metadata:resolve:reject:), args, count);
31
31
  }
32
32
 
33
33
  static facebook::jsi::Value __hostFunction_NativeOtaHotUpdateSpecJSI_setExactBundlePath(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
@@ -62,10 +62,22 @@ namespace facebook::react {
62
62
  return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "rollbackToPreviousBundle", @selector(rollbackToPreviousBundle:resolve:reject:), args, count);
63
63
  }
64
64
 
65
+ static facebook::jsi::Value __hostFunction_NativeOtaHotUpdateSpecJSI_getBundleList(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
66
+ return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "getBundleList", @selector(getBundleList:resolve:reject:), args, count);
67
+ }
68
+
69
+ static facebook::jsi::Value __hostFunction_NativeOtaHotUpdateSpecJSI_deleteBundleById(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
70
+ return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "deleteBundleById", @selector(deleteBundleById:resolve:reject:), args, count);
71
+ }
72
+
73
+ static facebook::jsi::Value __hostFunction_NativeOtaHotUpdateSpecJSI_clearAllBundles(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
74
+ return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "clearAllBundles", @selector(clearAllBundles:resolve:reject:), args, count);
75
+ }
76
+
65
77
  NativeOtaHotUpdateSpecJSI::NativeOtaHotUpdateSpecJSI(const ObjCTurboModule::InitParams &params)
66
78
  : ObjCTurboModule(params) {
67
79
 
68
- methodMap_["setupBundlePath"] = MethodMetadata {2, __hostFunction_NativeOtaHotUpdateSpecJSI_setupBundlePath};
80
+ methodMap_["setupBundlePath"] = MethodMetadata {5, __hostFunction_NativeOtaHotUpdateSpecJSI_setupBundlePath};
69
81
 
70
82
 
71
83
  methodMap_["setExactBundlePath"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_setExactBundlePath};
@@ -91,5 +103,14 @@ namespace facebook::react {
91
103
 
92
104
  methodMap_["rollbackToPreviousBundle"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_rollbackToPreviousBundle};
93
105
 
106
+
107
+ methodMap_["getBundleList"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_getBundleList};
108
+
109
+
110
+ methodMap_["deleteBundleById"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_deleteBundleById};
111
+
112
+
113
+ methodMap_["clearAllBundles"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateSpecJSI_clearAllBundles};
114
+
94
115
  }
95
116
  } // namespace facebook::react
@@ -35,6 +35,9 @@
35
35
 
36
36
  - (void)setupBundlePath:(NSString *)path
37
37
  extension:(NSString *)extension
38
+ version:(NSNumber *)version
39
+ maxVersions:(NSNumber *)maxVersions
40
+ metadata:(NSString *)metadata
38
41
  resolve:(RCTPromiseResolveBlock)resolve
39
42
  reject:(RCTPromiseRejectBlock)reject;
40
43
  - (void)setExactBundlePath:(NSString *)path
@@ -59,6 +62,15 @@
59
62
  - (void)rollbackToPreviousBundle:(double)a
60
63
  resolve:(RCTPromiseResolveBlock)resolve
61
64
  reject:(RCTPromiseRejectBlock)reject;
65
+ - (void)getBundleList:(double)a
66
+ resolve:(RCTPromiseResolveBlock)resolve
67
+ reject:(RCTPromiseRejectBlock)reject;
68
+ - (void)deleteBundleById:(NSString *)id
69
+ resolve:(RCTPromiseResolveBlock)resolve
70
+ reject:(RCTPromiseRejectBlock)reject;
71
+ - (void)clearAllBundles:(double)a
72
+ resolve:(RCTPromiseResolveBlock)resolve
73
+ reject:(RCTPromiseRejectBlock)reject;
62
74
 
63
75
  @end
64
76
 
@@ -15,7 +15,10 @@ static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setupBundlePath(js
15
15
  return static_cast<NativeOtaHotUpdateCxxSpecJSI *>(&turboModule)->setupBundlePath(
16
16
  rt,
17
17
  count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt),
18
- count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt)
18
+ count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asString(rt),
19
+ count <= 2 || args[2].isUndefined() ? std::nullopt : std::make_optional(args[2].asNumber()),
20
+ count <= 3 || args[3].isUndefined() ? std::nullopt : std::make_optional(args[3].asNumber()),
21
+ count <= 4 || args[4].isUndefined() ? std::nullopt : std::make_optional(args[4].asString(rt))
19
22
  );
20
23
  }
21
24
  static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setExactBundlePath(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
@@ -66,10 +69,28 @@ static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_rollbackToPrevious
66
69
  count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asNumber()
67
70
  );
68
71
  }
72
+ static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_getBundleList(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
73
+ return static_cast<NativeOtaHotUpdateCxxSpecJSI *>(&turboModule)->getBundleList(
74
+ rt,
75
+ count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asNumber()
76
+ );
77
+ }
78
+ static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_deleteBundleById(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
79
+ return static_cast<NativeOtaHotUpdateCxxSpecJSI *>(&turboModule)->deleteBundleById(
80
+ rt,
81
+ count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asString(rt)
82
+ );
83
+ }
84
+ static jsi::Value __hostFunction_NativeOtaHotUpdateCxxSpecJSI_clearAllBundles(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
85
+ return static_cast<NativeOtaHotUpdateCxxSpecJSI *>(&turboModule)->clearAllBundles(
86
+ rt,
87
+ count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asNumber()
88
+ );
89
+ }
69
90
 
70
91
  NativeOtaHotUpdateCxxSpecJSI::NativeOtaHotUpdateCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
71
92
  : TurboModule("OtaHotUpdate", jsInvoker) {
72
- methodMap_["setupBundlePath"] = MethodMetadata {2, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setupBundlePath};
93
+ methodMap_["setupBundlePath"] = MethodMetadata {5, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setupBundlePath};
73
94
  methodMap_["setExactBundlePath"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setExactBundlePath};
74
95
  methodMap_["deleteBundle"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_deleteBundle};
75
96
  methodMap_["restart"] = MethodMetadata {0, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_restart};
@@ -78,6 +99,9 @@ NativeOtaHotUpdateCxxSpecJSI::NativeOtaHotUpdateCxxSpecJSI(std::shared_ptr<CallI
78
99
  methodMap_["setCurrentVersion"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setCurrentVersion};
79
100
  methodMap_["setUpdateMetadata"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_setUpdateMetadata};
80
101
  methodMap_["rollbackToPreviousBundle"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_rollbackToPreviousBundle};
102
+ methodMap_["getBundleList"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_getBundleList};
103
+ methodMap_["deleteBundleById"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_deleteBundleById};
104
+ methodMap_["clearAllBundles"] = MethodMetadata {1, __hostFunction_NativeOtaHotUpdateCxxSpecJSI_clearAllBundles};
81
105
  }
82
106
 
83
107