react-native-ota-hot-update 2.3.6 → 2.4.0-rc.2
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.
- package/android/generated/java/com/otahotupdate/NativeOtaHotUpdateSpec.java +14 -1
- package/android/generated/jni/RNOtaHotUpdateSpec-generated.cpp +20 -2
- package/android/generated/jni/react/renderer/components/RNOtaHotUpdateSpec/RNOtaHotUpdateSpecJSI-generated.cpp +26 -2
- package/android/generated/jni/react/renderer/components/RNOtaHotUpdateSpec/RNOtaHotUpdateSpecJSI.h +32 -5
- package/android/src/main/java/com/otahotupdate/OtaHotUpdateModule.kt +365 -36
- package/android/src/main/java/com/otahotupdate/SharedPrefs.kt +12 -0
- package/android/src/main/java/com/otahotupdate/Utils.kt +9 -3
- package/android/src/oldarch/OtaHotUpdateSpec.kt +4 -1
- package/ios/OtaHotUpdate.mm +383 -42
- package/ios/generated/RNOtaHotUpdateSpec/RNOtaHotUpdateSpec-generated.mm +23 -2
- package/ios/generated/RNOtaHotUpdateSpec/RNOtaHotUpdateSpec.h +12 -0
- package/ios/generated/RNOtaHotUpdateSpecJSI-generated.cpp +26 -2
- package/ios/generated/RNOtaHotUpdateSpecJSI.h +32 -5
- package/lib/commonjs/NativeOtaHotUpdate.js.map +1 -1
- package/lib/commonjs/index.js +26 -3
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/NativeOtaHotUpdate.js.map +1 -1
- package/lib/module/index.js +26 -3
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/commonjs/src/NativeOtaHotUpdate.d.ts +4 -1
- package/lib/typescript/commonjs/src/NativeOtaHotUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +8 -2
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/type.d.ts +36 -0
- package/lib/typescript/commonjs/src/type.d.ts.map +1 -1
- package/lib/typescript/module/src/NativeOtaHotUpdate.d.ts +4 -1
- package/lib/typescript/module/src/NativeOtaHotUpdate.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +8 -2
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/type.d.ts +36 -0
- package/lib/typescript/module/src/type.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeOtaHotUpdate.ts +4 -1
- package/src/index.d.ts +25 -2
- package/src/index.tsx +36 -5
- package/src/type.ts +44 -1
|
@@ -19,6 +19,16 @@ class SharedPrefs internal constructor(context: Context) {
|
|
|
19
19
|
editor.apply()
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
fun getInt(key: String?, defaultValue: Int): Int {
|
|
23
|
+
return mSharedPreferences.getInt(key, defaultValue)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fun putInt(key: String?, value: Int) {
|
|
27
|
+
val editor = mSharedPreferences.edit()
|
|
28
|
+
editor.putInt(key, value)
|
|
29
|
+
editor.apply()
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
fun clear() {
|
|
23
33
|
mSharedPreferences.edit().clear().apply()
|
|
24
34
|
}
|
|
@@ -32,4 +42,6 @@ object Common {
|
|
|
32
42
|
val SHARED_PREFERENCE_NAME = "HOT-UPDATE-REACT_NATIVE"
|
|
33
43
|
val DEFAULT_BUNDLE = "assets://index.android.bundle"
|
|
34
44
|
val METADATA = "METADATA"
|
|
45
|
+
val BUNDLE_HISTORY = "BUNDLE_HISTORY"
|
|
46
|
+
const val DEFAULT_MAX_BUNDLE_VERSIONS = 2
|
|
35
47
|
}
|
|
@@ -47,11 +47,11 @@ class Utils internal constructor(private val context: Context) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
fun extractZipFile(
|
|
50
|
-
zipFile: File,extension: String
|
|
50
|
+
zipFile: File,extension: String, version: Int? = null
|
|
51
51
|
): String? {
|
|
52
52
|
return try {
|
|
53
53
|
val outputDir = zipFile.parentFile
|
|
54
|
-
val timestamp = SimpleDateFormat("
|
|
54
|
+
val timestamp = SimpleDateFormat("yyyy_MM_dd_HH_mm", Locale.getDefault()).format(Date())
|
|
55
55
|
var topLevelFolder: String? = null
|
|
56
56
|
var bundlePath: String? = null
|
|
57
57
|
ZipFile(zipFile).use { zip ->
|
|
@@ -83,7 +83,13 @@ class Utils internal constructor(private val context: Context) {
|
|
|
83
83
|
// Rename the detected top-level folder
|
|
84
84
|
if (topLevelFolder != null) {
|
|
85
85
|
val extractedFolder = File(outputDir, topLevelFolder)
|
|
86
|
-
|
|
86
|
+
// Include version in folder name if provided, otherwise use timestamp only
|
|
87
|
+
val folderName = if (version != null) {
|
|
88
|
+
"output_v${version}_$timestamp"
|
|
89
|
+
} else {
|
|
90
|
+
"output_$timestamp"
|
|
91
|
+
}
|
|
92
|
+
val renamedFolder = File(outputDir, folderName)
|
|
87
93
|
if (extractedFolder.exists()) {
|
|
88
94
|
extractedFolder.renameTo(renamedFolder)
|
|
89
95
|
// Update bundlePath if the file was inside the renamed folder
|
|
@@ -7,7 +7,7 @@ import com.facebook.react.bridge.Promise
|
|
|
7
7
|
abstract class OtaHotUpdateSpec internal constructor(context: ReactApplicationContext) :
|
|
8
8
|
ReactContextBaseJavaModule(context) {
|
|
9
9
|
|
|
10
|
-
abstract fun setupBundlePath(path: String?, extension: String?, promise: Promise)
|
|
10
|
+
abstract fun setupBundlePath(path: String?, extension: String?, version: Double?, maxVersions: Double?, metadata: String?, promise: Promise)
|
|
11
11
|
abstract fun deleteBundle(i: Double, promise: Promise)
|
|
12
12
|
abstract fun restart()
|
|
13
13
|
abstract fun getCurrentVersion(a: Double, promise: Promise)
|
|
@@ -16,4 +16,7 @@ abstract class OtaHotUpdateSpec internal constructor(context: ReactApplicationCo
|
|
|
16
16
|
abstract fun rollbackToPreviousBundle(a: Double, promise: Promise)
|
|
17
17
|
abstract fun getUpdateMetadata(a: Double, promise: Promise)
|
|
18
18
|
abstract fun setUpdateMetadata(metadata: String?, promise: Promise)
|
|
19
|
+
abstract fun getBundleList(a: Double, promise: Promise)
|
|
20
|
+
abstract fun deleteBundleById(id: String, promise: Promise)
|
|
21
|
+
abstract fun clearAllBundles(a: Double, promise: Promise)
|
|
19
22
|
}
|
package/ios/OtaHotUpdate.mm
CHANGED
|
@@ -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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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 *
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
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 ¶ms)
|
|
66
78
|
: ObjCTurboModule(params) {
|
|
67
79
|
|
|
68
|
-
methodMap_["setupBundlePath"] = MethodMetadata {
|
|
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
|
|