@vexor-push/react-native-code-push 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +61 -0
  3. package/android/build.gradle +129 -0
  4. package/android/generated/java/com/vexorpush/codepush/NativeVexorCodePushSpec.java +91 -0
  5. package/android/generated/jni/CMakeLists.txt +36 -0
  6. package/android/generated/jni/RNVexorCodePushSpec-generated.cpp +110 -0
  7. package/android/generated/jni/RNVexorCodePushSpec.h +31 -0
  8. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ComponentDescriptors.cpp +22 -0
  9. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ComponentDescriptors.h +24 -0
  10. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/EventEmitters.cpp +16 -0
  11. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/EventEmitters.h +17 -0
  12. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/Props.cpp +19 -0
  13. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/Props.h +18 -0
  14. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/RNVexorCodePushSpecJSI-generated.cpp +129 -0
  15. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/RNVexorCodePushSpecJSI.h +184 -0
  16. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ShadowNodes.cpp +17 -0
  17. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ShadowNodes.h +23 -0
  18. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/States.cpp +16 -0
  19. package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/States.h +19 -0
  20. package/android/gradle.properties +5 -0
  21. package/android/src/main/AndroidManifest.xml +3 -0
  22. package/android/src/main/AndroidManifestNew.xml +2 -0
  23. package/android/src/main/java/com/vexorpush/codepush/CrashHandler.kt +78 -0
  24. package/android/src/main/java/com/vexorpush/codepush/SharedPrefs.kt +60 -0
  25. package/android/src/main/java/com/vexorpush/codepush/Utils.kt +105 -0
  26. package/android/src/main/java/com/vexorpush/codepush/VexorCodePush.kt +100 -0
  27. package/android/src/main/java/com/vexorpush/codepush/VexorCodePushModule.kt +698 -0
  28. package/android/src/newarch/VexorCodePushSpec.kt +7 -0
  29. package/android/src/oldarch/VexorCodePushSpec.kt +24 -0
  30. package/app.plugin.js +1 -0
  31. package/current-version.js +27 -0
  32. package/deployment-state.js +33 -0
  33. package/index.d.ts +101 -0
  34. package/index.js +535 -0
  35. package/ios/VexorCodePush.h +13 -0
  36. package/ios/VexorCodePush.mm +949 -0
  37. package/ios/generated/RNVexorCodePushSpec/ComponentDescriptors.cpp +22 -0
  38. package/ios/generated/RNVexorCodePushSpec/ComponentDescriptors.h +24 -0
  39. package/ios/generated/RNVexorCodePushSpec/EventEmitters.cpp +16 -0
  40. package/ios/generated/RNVexorCodePushSpec/EventEmitters.h +17 -0
  41. package/ios/generated/RNVexorCodePushSpec/Props.cpp +19 -0
  42. package/ios/generated/RNVexorCodePushSpec/Props.h +18 -0
  43. package/ios/generated/RNVexorCodePushSpec/RCTComponentViewHelpers.h +18 -0
  44. package/ios/generated/RNVexorCodePushSpec/RNVexorCodePushSpec-generated.mm +130 -0
  45. package/ios/generated/RNVexorCodePushSpec/RNVexorCodePushSpec.h +109 -0
  46. package/ios/generated/RNVexorCodePushSpec/ShadowNodes.cpp +17 -0
  47. package/ios/generated/RNVexorCodePushSpec/ShadowNodes.h +23 -0
  48. package/ios/generated/RNVexorCodePushSpec/States.cpp +16 -0
  49. package/ios/generated/RNVexorCodePushSpec/States.h +19 -0
  50. package/ios/generated/RNVexorCodePushSpecJSI-generated.cpp +129 -0
  51. package/ios/generated/RNVexorCodePushSpecJSI.h +184 -0
  52. package/package.json +51 -0
  53. package/plugin/src/index.ts +92 -0
  54. package/plugin/tsconfig.json +9 -0
  55. package/react-native-code-push.podspec +48 -0
  56. package/react-native.config.js +13 -0
  57. package/src/NativeVexorCodePush.ts +21 -0
@@ -0,0 +1,949 @@
1
+ #import "VexorCodePush.h"
2
+ #import <SSZipArchive/SSZipArchive.h>
3
+ #include <signal.h>
4
+
5
+ @interface VexorCodePush ()
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
+
12
+ static NSUncaughtExceptionHandler *previousHandler = NULL;
13
+ static BOOL isBeginning = YES;
14
+ @implementation VexorCodePush
15
+ RCT_EXPORT_MODULE()
16
+
17
+ + (BOOL)requiresMainQueueSetup {
18
+ return NO;
19
+ }
20
+
21
+ - (instancetype)init {
22
+ self = [super init];
23
+ if (self) {
24
+ previousHandler = NSGetUncaughtExceptionHandler();
25
+ NSSetUncaughtExceptionHandler(&VexorCodePushExceptionHandler);
26
+ signal(SIGABRT, VexorCodePushSignalHandler);
27
+ signal(SIGILL, VexorCodePushSignalHandler);
28
+ signal(SIGSEGV, VexorCodePushSignalHandler);
29
+ signal(SIGFPE, VexorCodePushSignalHandler);
30
+ signal(SIGBUS, VexorCodePushSignalHandler);
31
+ signal(SIGPIPE, VexorCodePushSignalHandler);
32
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
33
+ isBeginning = NO;
34
+ });
35
+ }
36
+ return self;
37
+ }
38
+
39
+ void VexorCodePushSignalHandler(int sig) {
40
+ if (isBeginning) {
41
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
42
+ // Use PREVIOUS_BUNDLE_PATH (simple key, written by saveBundleVersion before each update)
43
+ NSString *previousPath = [defaults stringForKey:@"PREVIOUS_BUNDLE_PATH"];
44
+ if (previousPath && previousPath.length > 0) {
45
+ BOOL isDeleted = [VexorCodePush removeBundleIfNeeded:@"PATH"];
46
+ if (isDeleted) {
47
+ [defaults setObject:previousPath forKey:@"PATH"];
48
+ NSString *previousVersion = [defaults stringForKey:@"PREVIOUS_BUNDLE_VERSION"];
49
+ if (previousVersion) {
50
+ [defaults setObject:previousVersion forKey:@"VERSION"];
51
+ }
52
+ } else {
53
+ [defaults removeObjectForKey:@"PATH"];
54
+ }
55
+ } else {
56
+ [defaults removeObjectForKey:@"PATH"];
57
+ }
58
+ [defaults removeObjectForKey:@"PREVIOUS_BUNDLE_PATH"];
59
+ [defaults removeObjectForKey:@"PREVIOUS_BUNDLE_VERSION"];
60
+ [defaults synchronize];
61
+ }
62
+
63
+ signal(sig, SIG_DFL);
64
+ raise(sig);
65
+ }
66
+ void VexorCodePushExceptionHandler(NSException *exception) {
67
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
68
+ if (isBeginning) {
69
+ NSString *previousPath = [defaults stringForKey:@"PREVIOUS_BUNDLE_PATH"];
70
+ if (previousPath && previousPath.length > 0) {
71
+ BOOL isDeleted = [VexorCodePush removeBundleIfNeeded:@"PATH"];
72
+ if (isDeleted) {
73
+ [defaults setObject:previousPath forKey:@"PATH"];
74
+ NSString *previousVersion = [defaults stringForKey:@"PREVIOUS_BUNDLE_VERSION"];
75
+ if (previousVersion) {
76
+ [defaults setObject:previousVersion forKey:@"VERSION"];
77
+ }
78
+ } else {
79
+ [defaults removeObjectForKey:@"PATH"];
80
+ }
81
+ } else {
82
+ [defaults removeObjectForKey:@"PATH"];
83
+ }
84
+ [defaults removeObjectForKey:@"PREVIOUS_BUNDLE_PATH"];
85
+ [defaults removeObjectForKey:@"PREVIOUS_BUNDLE_VERSION"];
86
+ [defaults synchronize];
87
+ } else if (previousHandler) {
88
+ previousHandler(exception);
89
+ }
90
+ }
91
+
92
+ // Check if a file path is valid
93
+ + (BOOL)isFilePathValid:(NSString *)path {
94
+ NSFileManager *fileManager = [NSFileManager defaultManager];
95
+ return [fileManager fileExistsAtPath:path];
96
+ }
97
+
98
+ // Delete a file at the specified path
99
+ - (BOOL)deleteFileAtPath:(NSString *)path {
100
+ NSFileManager *fileManager = [NSFileManager defaultManager];
101
+ NSError *error = nil;
102
+ BOOL success = [fileManager removeItemAtPath:path error:&error];
103
+ if (!success) {
104
+ NSLog(@"Error deleting file: %@", [error localizedDescription]);
105
+ }
106
+ return success;
107
+ }
108
+ + (BOOL)deleteAllContentsOfParentDirectoryOfFile:(NSString *)filePath error:(NSError **)error {
109
+ NSFileManager *fileManager = [NSFileManager defaultManager];
110
+
111
+ // Get the parent directory of the file
112
+ NSString *parentDirectory = [filePath stringByDeletingLastPathComponent];
113
+
114
+ // Ensure the parent directory exists
115
+ BOOL isDirectory;
116
+ if (![fileManager fileExistsAtPath:parentDirectory isDirectory:&isDirectory] || !isDirectory) {
117
+ if (error) {
118
+ *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoSuchFileError userInfo:@{NSLocalizedDescriptionKey: @"Parent directory does not exist or is not a directory."}];
119
+ }
120
+ return NO;
121
+ }
122
+
123
+ // Get the contents of the parent directory
124
+ NSArray *contents = [fileManager contentsOfDirectoryAtPath:parentDirectory error:error];
125
+ if (error && *error) {
126
+ return NO;
127
+ }
128
+
129
+ BOOL success = YES;
130
+ for (NSString *fileName in contents) {
131
+ NSString *filePathInDirectory = [parentDirectory stringByAppendingPathComponent:fileName];
132
+
133
+ BOOL isDirectory;
134
+ if ([fileManager fileExistsAtPath:filePathInDirectory isDirectory:&isDirectory]) {
135
+ NSError *removeError = nil;
136
+ if (isDirectory) {
137
+ // Recursively delete directory contents
138
+ if (![fileManager removeItemAtPath:filePathInDirectory error:&removeError]) {
139
+ NSLog(@"Failed to delete directory at path: %@", filePathInDirectory);
140
+ success = NO;
141
+ }
142
+ } else {
143
+ // Delete file
144
+ if (![fileManager removeItemAtPath:filePathInDirectory error:&removeError]) {
145
+ NSLog(@"Failed to delete file at path: %@", filePathInDirectory);
146
+ success = NO;
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ return success;
153
+ }
154
+
155
+ + (BOOL)deleteBundleAtPath:(NSString *)path {
156
+ if (!path || path.length == 0) {
157
+ return NO;
158
+ }
159
+ NSError *error = nil;
160
+ if ([self isFilePathValid:path]) {
161
+ BOOL isDeleted = [self deleteAllContentsOfParentDirectoryOfFile:path error:&error];
162
+ return isDeleted;
163
+ }
164
+ return NO;
165
+ }
166
+
167
+ + (BOOL)removeBundleIfNeeded:(NSString *)pathKey {
168
+ NSString *keyToUse = pathKey ? pathKey : @"OLD_PATH";
169
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
170
+ NSString *retrievedString = [defaults stringForKey:keyToUse];
171
+ NSError *error = nil;
172
+ if (retrievedString && [self isFilePathValid:retrievedString]) {
173
+ BOOL isDeleted = [self deleteAllContentsOfParentDirectoryOfFile:retrievedString error:&error];
174
+ [defaults removeObjectForKey:keyToUse];
175
+ [defaults synchronize];
176
+ return isDeleted;
177
+ } else {
178
+ return NO;
179
+ }
180
+ }
181
+
182
+ + (BOOL)isFilePathExist:(NSString *)path {
183
+ NSFileManager *fileManager = [NSFileManager defaultManager];
184
+ return [fileManager fileExistsAtPath:path];
185
+ }
186
+
187
+ + (NSURL *)getBundle {
188
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
189
+ NSString *retrievedString = [defaults stringForKey:@"PATH"];
190
+ NSString *currentVersionName = [defaults stringForKey:@"VERSION_NAME"];
191
+ NSString *versionName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
192
+
193
+ if (retrievedString && [self isFilePathExist:retrievedString] && [currentVersionName isEqualToString:versionName]) {
194
+ NSURL *fileURL = [NSURL fileURLWithPath:retrievedString];
195
+ return fileURL;
196
+ } else {
197
+ // reset version number because bundle is wrong version, need download from new version
198
+ [defaults removeObjectForKey:@"VERSION"];
199
+ [defaults removeObjectForKey:@"PATH"];
200
+ [defaults synchronize];
201
+ return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
202
+ }
203
+ }
204
+
205
+ - (NSString *)searchForJsBundleInDirectory:(NSString *)directoryPath extension:(NSString *)extension {
206
+ NSFileManager *fileManager = [NSFileManager defaultManager];
207
+ NSError *error;
208
+
209
+ // Get contents of the directory
210
+ NSArray *contents = [fileManager contentsOfDirectoryAtPath:directoryPath error:&error];
211
+ if (error) {
212
+ NSLog(@"Error reading directory contents: %@", error.localizedDescription);
213
+ return nil;
214
+ }
215
+
216
+ for (NSString *file in contents) {
217
+ NSString *filePath = [directoryPath stringByAppendingPathComponent:file];
218
+ BOOL isDirectory;
219
+ if ([fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
220
+ if (isDirectory) {
221
+ // Recursively search in subdirectories
222
+ NSString *foundPath = [self searchForJsBundleInDirectory:filePath extension:extension];
223
+ if (foundPath) {
224
+ return foundPath;
225
+ }
226
+ } else if ([filePath hasSuffix:extension]) {
227
+ // Return the path if it's a .jsbundle file
228
+ return filePath;
229
+ }
230
+ }
231
+ }
232
+
233
+ return nil;
234
+ }
235
+ - (NSString *)renameExtractedFolderInDirectory:(NSString *)directoryPath version:(NSNumber *)version {
236
+ NSFileManager *fileManager = [NSFileManager defaultManager];
237
+ NSError *error = nil;
238
+
239
+ // Get the contents of the extracted directory
240
+ NSArray *contents = [fileManager contentsOfDirectoryAtPath:directoryPath error:&error];
241
+ if (error || contents.count != 1) {
242
+ NSLog(@"Error retrieving extracted folder or unexpected structure: %@", error.localizedDescription);
243
+ return nil;
244
+ }
245
+
246
+ // Get the original extracted folder name (assuming only one folder exists)
247
+ NSString *originalFolderName = contents.firstObject;
248
+ NSString *originalFolderPath = [directoryPath stringByAppendingPathComponent:originalFolderName];
249
+
250
+ // Generate new folder name with version and timestamp
251
+ NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
252
+ [dateFormatter setDateFormat:@"yyyy_MM_dd_HH_mm"];
253
+ [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
254
+ NSString *timestamp = [dateFormatter stringFromDate:[NSDate date]];
255
+ NSString *folderName = version != nil
256
+ ? [NSString stringWithFormat:@"output_v%@_%@", version, timestamp]
257
+ : [NSString stringWithFormat:@"output_%@", timestamp];
258
+ NSString *newFolderPath = [directoryPath stringByAppendingPathComponent:folderName];
259
+
260
+ // Rename the extracted folder
261
+ if (![fileManager moveItemAtPath:originalFolderPath toPath:newFolderPath error:&error]) {
262
+ NSLog(@"Failed to rename folder: %@", error.localizedDescription);
263
+ return nil;
264
+ }
265
+
266
+ NSLog(@"Renamed extracted folder to: %@", newFolderPath);
267
+ return newFolderPath;
268
+ }
269
+ - (NSString *)unzipFileAtPath:(NSString *)zipFilePath extension:(NSString *)extension version:(NSNumber *)version {
270
+ // Define the directory where the files will be extracted
271
+ NSString *extractedFolderPath = [[zipFilePath stringByDeletingPathExtension] stringByAppendingPathExtension:@"unzip"];
272
+
273
+ // Create the directory if it does not exist
274
+ NSFileManager *fileManager = [NSFileManager defaultManager];
275
+ if (![fileManager fileExistsAtPath:extractedFolderPath]) {
276
+ NSError *error = nil;
277
+ [fileManager createDirectoryAtPath:extractedFolderPath withIntermediateDirectories:YES attributes:nil error:&error];
278
+ if (error) {
279
+ [self deleteFileAtPath:zipFilePath];
280
+ NSLog(@"Failed to create directory: %@", error.localizedDescription);
281
+ return nil;
282
+ }
283
+ }
284
+
285
+ // Unzip the file
286
+ BOOL success = [SSZipArchive unzipFileAtPath:zipFilePath toDestination:extractedFolderPath];
287
+ if (!success) {
288
+ [self deleteFileAtPath:zipFilePath];
289
+ NSLog(@"Failed to unzip file");
290
+ return nil;
291
+ }
292
+ // Try renaming the extracted folder
293
+ NSString *renamedFolderPath = [self renameExtractedFolderInDirectory:extractedFolderPath version:version];
294
+
295
+ // If renaming fails, use the original extracted folder path
296
+ NSString *finalFolderPath = renamedFolderPath ? renamedFolderPath : extractedFolderPath;
297
+
298
+ // Find .jsbundle files in the extracted directory
299
+ NSString *jsbundleFilePath = [self searchForJsBundleInDirectory:finalFolderPath extension:extension];
300
+
301
+ // Delete the zip file after extraction
302
+ NSError *removeError = nil;
303
+ [fileManager removeItemAtPath:zipFilePath error:&removeError];
304
+ if (removeError) {
305
+ NSLog(@"Failed to delete zip file: %@", removeError.localizedDescription);
306
+ }
307
+ return jsbundleFilePath;
308
+ }
309
+
310
+ // Expose setupBundlePath method to JavaScript
311
+ RCT_EXPORT_METHOD(downloadAndInstallBundle:(NSString *)url
312
+ headersJson:(NSString *)headersJson
313
+ extension:(NSString *)extension
314
+ version:(NSNumber * _Nonnull)version
315
+ maxVersions:(NSNumber * _Nonnull)maxVersions
316
+ metadata:(NSString *)metadata
317
+ resolve:(RCTPromiseResolveBlock)resolve
318
+ reject:(RCTPromiseRejectBlock)reject) {
319
+ if (url == nil || url.length == 0) {
320
+ reject(@"E_INVALID_URL", @"Download URL is required", nil);
321
+ return;
322
+ }
323
+
324
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
325
+ @try {
326
+ NSURL *downloadURL = [NSURL URLWithString:url];
327
+ if (downloadURL == nil) {
328
+ reject(@"E_INVALID_URL", @"Download URL is invalid", nil);
329
+ return;
330
+ }
331
+
332
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:downloadURL];
333
+ request.HTTPMethod = @"GET";
334
+ request.timeoutInterval = 120.0;
335
+
336
+ if (headersJson != nil && headersJson.length > 0) {
337
+ NSData *headersData = [headersJson dataUsingEncoding:NSUTF8StringEncoding];
338
+ NSError *headersError = nil;
339
+ NSDictionary *headers = [NSJSONSerialization JSONObjectWithData:headersData options:0 error:&headersError];
340
+ if (headersError == nil && [headers isKindOfClass:[NSDictionary class]]) {
341
+ for (id key in headers) {
342
+ id value = headers[key];
343
+ if ([key isKindOfClass:[NSString class]] && [value isKindOfClass:[NSString class]]) {
344
+ [request setValue:value forHTTPHeaderField:key];
345
+ }
346
+ }
347
+ }
348
+ }
349
+
350
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
351
+ __block NSData *downloadedData = nil;
352
+ __block NSURLResponse *downloadResponse = nil;
353
+ __block NSError *downloadError = nil;
354
+
355
+ NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
356
+ downloadedData = data;
357
+ downloadResponse = response;
358
+ downloadError = error;
359
+ dispatch_semaphore_signal(semaphore);
360
+ }];
361
+ [task resume];
362
+ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
363
+
364
+ if (downloadError != nil) {
365
+ reject(@"E_DOWNLOAD_FAIL", downloadError.localizedDescription, downloadError);
366
+ return;
367
+ }
368
+
369
+ if ([downloadResponse isKindOfClass:[NSHTTPURLResponse class]]) {
370
+ NSInteger statusCode = [(NSHTTPURLResponse *)downloadResponse statusCode];
371
+ if (statusCode < 200 || statusCode > 299) {
372
+ reject(@"E_DOWNLOAD_STATUS", [NSString stringWithFormat:@"Download failed with HTTP status %ld", (long)statusCode], nil);
373
+ return;
374
+ }
375
+ }
376
+
377
+ if (downloadedData == nil || downloadedData.length == 0) {
378
+ reject(@"E_DOWNLOAD_EMPTY", @"Downloaded bundle is empty", nil);
379
+ return;
380
+ }
381
+
382
+ NSString *cacheDir = [NSTemporaryDirectory() stringByAppendingPathComponent:@"vexor-code-push"];
383
+ NSFileManager *fileManager = [NSFileManager defaultManager];
384
+ if (![fileManager fileExistsAtPath:cacheDir]) {
385
+ NSError *createError = nil;
386
+ if (![fileManager createDirectoryAtPath:cacheDir withIntermediateDirectories:YES attributes:nil error:&createError]) {
387
+ reject(@"E_CREATE_DIR", createError.localizedDescription, createError);
388
+ return;
389
+ }
390
+ }
391
+
392
+ NSString *zipPath = [cacheDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%lld-update.zip", (long long)([[NSDate date] timeIntervalSince1970] * 1000)]];
393
+ NSError *writeError = nil;
394
+ if (![downloadedData writeToFile:zipPath options:NSDataWritingAtomic error:&writeError]) {
395
+ reject(@"E_WRITE_FILE", writeError.localizedDescription, writeError);
396
+ return;
397
+ }
398
+
399
+ NSString *extractedFilePath = [self unzipFileAtPath:zipPath extension:(extension != nil) ? extension : @".jsbundle" version:version];
400
+ if (extractedFilePath == nil) {
401
+ reject(@"E_UNZIP_FAIL", @"Unzipping failed, zip file invalid", nil);
402
+ return;
403
+ }
404
+
405
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
406
+ if (version != nil) {
407
+ NSInteger maxVersionsToKeep = maxVersions != nil ? [maxVersions integerValue] : 2;
408
+ [self saveBundleVersion:extractedFilePath version:[version integerValue] maxVersions:maxVersionsToKeep metadata:metadata];
409
+ } else {
410
+ [defaults setObject:extractedFilePath forKey:@"PATH"];
411
+ }
412
+ [defaults setObject:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"] forKey:@"VERSION_NAME"];
413
+ [defaults synchronize];
414
+ isBeginning = YES;
415
+ resolve(@(YES));
416
+ } @catch (NSException *exception) {
417
+ reject(@"E_DOWNLOAD_INSTALL", exception.reason, nil);
418
+ }
419
+ });
420
+ }
421
+
422
+ RCT_EXPORT_METHOD(setupBundlePath:(NSString *)path extension:(NSString *)extension version:(NSNumber * _Nonnull)version maxVersions:(NSNumber * _Nonnull)maxVersions metadata:(NSString *)metadata
423
+ resolve:(RCTPromiseResolveBlock)resolve
424
+ reject:(RCTPromiseRejectBlock)reject) {
425
+ if ([VexorCodePush isFilePathValid:path]) {
426
+ //Unzip file
427
+ NSString *extractedFilePath = [self unzipFileAtPath:path extension:(extension != nil) ? extension : @".jsbundle" version:version];
428
+ if (extractedFilePath) {
429
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
430
+
431
+ // If version is provided, save to history system
432
+ if (version != nil) {
433
+ // Default maxVersions to 2 if not provided (new product default)
434
+ NSInteger defaultMaxVersions = 2;
435
+ NSInteger maxVersionsToKeep = maxVersions != nil ? [maxVersions integerValue] : defaultMaxVersions;
436
+ [self saveBundleVersion:extractedFilePath version:[version integerValue] maxVersions:maxVersionsToKeep metadata:metadata];
437
+ } else {
438
+ // No version (e.g., Git update) - just set path, no history
439
+ [defaults setObject:extractedFilePath forKey:@"PATH"];
440
+ }
441
+
442
+ [defaults setObject:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"] forKey:@"VERSION_NAME"];
443
+ [defaults synchronize];
444
+ isBeginning = YES;
445
+ resolve(@(YES));
446
+ } else {
447
+ reject(@"E_UNZIP_FAIL", @"Unzipping failed, zip file invalid", nil);
448
+ }
449
+ } else {
450
+ reject(@"E_INVALID_PATH", @"Invalid or missing file path", nil);
451
+ }
452
+ }
453
+ RCT_EXPORT_METHOD(deleteBundle:(double)i
454
+ resolve:(RCTPromiseResolveBlock)resolve
455
+ reject:(RCTPromiseRejectBlock)reject) {
456
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
457
+ NSString *currentPath = [defaults stringForKey:@"PATH"];
458
+
459
+ // Delete current bundle from file system
460
+ BOOL isDeleted = [VexorCodePush removeBundleIfNeeded:@"PATH"];
461
+
462
+ // Remove current bundle from history if exists
463
+ if (currentPath && currentPath.length > 0) {
464
+ NSArray *history = [self loadBundleHistory];
465
+ NSMutableArray *updatedHistory = [NSMutableArray array];
466
+ for (NSDictionary *bundle in history) {
467
+ if (![bundle[@"path"] isEqualToString:currentPath]) {
468
+ [updatedHistory addObject:bundle];
469
+ }
470
+ }
471
+ [self saveBundleHistory:updatedHistory];
472
+ }
473
+
474
+ // Clear paths and version
475
+ [defaults removeObjectForKey:@"PATH"];
476
+ [defaults setObject:@"0" forKey:@"VERSION"];
477
+ [defaults synchronize];
478
+
479
+ resolve(@(isDeleted));
480
+ }
481
+ // Expose deleteBundle method to JavaScript
482
+ RCT_EXPORT_METHOD(rollbackToPreviousBundle:(double)i
483
+ resolve:(RCTPromiseResolveBlock)resolve
484
+ reject:(RCTPromiseRejectBlock)reject) {
485
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
486
+ NSString *currentPath = [defaults stringForKey:@"PATH"];
487
+
488
+ // Use history to find previous version
489
+ NSArray *history = [self loadBundleHistory];
490
+ if (history.count > 0 && currentPath && currentPath.length > 0) {
491
+ // Find current bundle in history
492
+ NSDictionary *currentBundle = nil;
493
+ for (NSDictionary *bundle in history) {
494
+ if ([bundle[@"path"] isEqualToString:currentPath]) {
495
+ currentBundle = bundle;
496
+ break;
497
+ }
498
+ }
499
+
500
+ if (currentBundle) {
501
+ // Find previous version (older than current, max version)
502
+ NSDictionary *previousBundle = nil;
503
+ NSInteger currentVersion = [currentBundle[@"version"] integerValue];
504
+ for (NSDictionary *bundle in history) {
505
+ NSInteger bundleVersion = [bundle[@"version"] integerValue];
506
+ if (bundleVersion < currentVersion) {
507
+ if (!previousBundle || [bundle[@"version"] integerValue] > [previousBundle[@"version"] integerValue]) {
508
+ previousBundle = bundle;
509
+ }
510
+ }
511
+ }
512
+
513
+ if (previousBundle && [VexorCodePush isFilePathValid:previousBundle[@"path"]]) {
514
+ // Rollback to previous bundle from history
515
+ BOOL isDeleted = [VexorCodePush removeBundleIfNeeded:@"PATH"];
516
+ if (isDeleted) {
517
+ [defaults setObject:previousBundle[@"path"] forKey:@"PATH"];
518
+ [defaults setObject:[NSString stringWithFormat:@"%@", previousBundle[@"version"]] forKey:@"VERSION"];
519
+ [defaults synchronize];
520
+ resolve(@(YES));
521
+ return;
522
+ }
523
+ }
524
+ }
525
+ }
526
+
527
+ resolve(@(NO));
528
+ }
529
+
530
+ RCT_EXPORT_METHOD(getCurrentVersion:(double)a
531
+ resolve:(RCTPromiseResolveBlock)resolve
532
+ reject:(RCTPromiseRejectBlock)reject)
533
+ {
534
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
535
+ NSString *version = [defaults stringForKey:@"VERSION"];
536
+ if (version) {
537
+ resolve(version);
538
+ } else {
539
+ resolve(@"0");
540
+ }
541
+ }
542
+
543
+ RCT_EXPORT_METHOD(setCurrentVersion:(NSString *)version
544
+ resolve:(RCTPromiseResolveBlock)resolve
545
+ reject:(RCTPromiseRejectBlock)reject) {
546
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
547
+ // No longer save PREVIOUS_VERSION, use history instead
548
+ if (version) {
549
+ [defaults setObject:version forKey:@"VERSION"];
550
+ [defaults synchronize];
551
+ resolve(@(YES));
552
+ } else {
553
+ resolve(@(NO));
554
+ }
555
+ }
556
+
557
+ RCT_EXPORT_METHOD(setUpdateMetadata:(NSString *)metadataString
558
+ resolve:(RCTPromiseResolveBlock)resolve
559
+ reject:(RCTPromiseRejectBlock)reject) {
560
+ if (metadataString) {
561
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
562
+ [defaults setObject:metadataString forKey:@"METADATA"];
563
+ [defaults synchronize];
564
+ resolve(@(YES));
565
+ } else {
566
+ resolve(@(NO));
567
+ }
568
+ }
569
+
570
+ RCT_EXPORT_METHOD(getUpdateMetadata:(double)a
571
+ resolve:(RCTPromiseResolveBlock)resolve
572
+ reject:(RCTPromiseRejectBlock)reject)
573
+ {
574
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
575
+ NSString *metadata = [defaults stringForKey:@"METADATA"];
576
+
577
+ if (metadata) {
578
+ resolve(metadata);
579
+ } else {
580
+ resolve(nil);
581
+ }
582
+ }
583
+
584
+ RCT_EXPORT_METHOD(setExactBundlePath:(NSString *)path
585
+ resolve:(RCTPromiseResolveBlock)resolve
586
+ reject:(RCTPromiseRejectBlock)reject) {
587
+ if ([VexorCodePush isFilePathValid:path]) {
588
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
589
+ [defaults setObject:path forKey:@"PATH"];
590
+ [defaults setObject:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"] forKey:@"VERSION_NAME"];
591
+ [defaults synchronize];
592
+ resolve(@(YES));
593
+ } else {
594
+ resolve(@(NO));
595
+ }
596
+ }
597
+
598
+ - (void)loadBundle
599
+ {
600
+ RCTTriggerReloadCommandListeners(@"VexorCodePush: Restart");
601
+ }
602
+ - (NSArray *)loadBundleHistory {
603
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
604
+ NSString *historyJson = [defaults stringForKey:@"BUNDLE_HISTORY"];
605
+
606
+ // If history exists, load it
607
+ if (historyJson && historyJson.length > 0) {
608
+ NSData *data = [historyJson dataUsingEncoding:NSUTF8StringEncoding];
609
+ NSError *error = nil;
610
+ NSArray *history = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
611
+ if (!error && [history isKindOfClass:[NSArray class]]) {
612
+ return history;
613
+ }
614
+ }
615
+
616
+ // Bootstrap bundle history from the current and previous bundle pointers
617
+ NSString *currentPath = [defaults stringForKey:@"PATH"];
618
+ NSString *currentVersion = [defaults stringForKey:@"VERSION"];
619
+ NSString *previousPath = [defaults stringForKey:@"OLD_PATH"];
620
+ NSString *previousVersion = [defaults stringForKey:@"PREVIOUS_VERSION"];
621
+
622
+ if (!currentPath || currentPath.length == 0) {
623
+ return @[];
624
+ }
625
+
626
+ // Migrate current bundle
627
+ NSMutableArray *migratedHistory = [NSMutableArray array];
628
+
629
+ // Add current bundle if has version
630
+ if (currentVersion && currentVersion.length > 0) {
631
+ NSInteger version = [currentVersion integerValue];
632
+ if (version > 0 && [VexorCodePush isFilePathValid:currentPath]) {
633
+ NSFileManager *fileManager = [NSFileManager defaultManager];
634
+ NSDictionary *attributes = [fileManager attributesOfItemAtPath:currentPath error:nil];
635
+ NSDate *modificationDate = attributes[NSFileModificationDate];
636
+ long long timestamp = modificationDate ? (long long)([modificationDate timeIntervalSince1970] * 1000) : (long long)([[NSDate date] timeIntervalSince1970] * 1000);
637
+
638
+ NSMutableDictionary *bundle = [NSMutableDictionary dictionary];
639
+ bundle[@"version"] = @(version);
640
+ bundle[@"path"] = currentPath;
641
+ bundle[@"timestamp"] = @(timestamp);
642
+ bundle[@"metadata"] = [NSNull null];
643
+ [migratedHistory addObject:bundle];
644
+ }
645
+ }
646
+
647
+ // Add previous bundle if exists
648
+ if (previousPath && previousPath.length > 0 && previousVersion && previousVersion.length > 0) {
649
+ NSInteger version = [previousVersion integerValue];
650
+ if (version > 0 && [VexorCodePush isFilePathValid:previousPath]) {
651
+ NSFileManager *fileManager = [NSFileManager defaultManager];
652
+ NSDictionary *attributes = [fileManager attributesOfItemAtPath:previousPath error:nil];
653
+ NSDate *modificationDate = attributes[NSFileModificationDate];
654
+ long long timestamp = modificationDate ? (long long)([modificationDate timeIntervalSince1970] * 1000) : (long long)([[NSDate date] timeIntervalSince1970] * 1000);
655
+
656
+ NSMutableDictionary *bundle = [NSMutableDictionary dictionary];
657
+ bundle[@"version"] = @(version);
658
+ bundle[@"path"] = previousPath;
659
+ bundle[@"timestamp"] = @(timestamp);
660
+ bundle[@"metadata"] = [NSNull null];
661
+ [migratedHistory addObject:bundle];
662
+ }
663
+ }
664
+
665
+ // Save migrated history if any
666
+ if (migratedHistory.count > 0) {
667
+ // Sort by version descending
668
+ NSArray *sortedHistory = [migratedHistory sortedArrayUsingComparator:^NSComparisonResult(NSDictionary *obj1, NSDictionary *obj2) {
669
+ NSInteger v1 = [obj1[@"version"] integerValue];
670
+ NSInteger v2 = [obj2[@"version"] integerValue];
671
+ if (v1 > v2) return NSOrderedAscending;
672
+ if (v1 < v2) return NSOrderedDescending;
673
+ return NSOrderedSame;
674
+ }];
675
+ [self saveBundleHistory:sortedHistory];
676
+ return sortedHistory;
677
+ }
678
+
679
+ return @[];
680
+ }
681
+
682
+ - (void)saveBundleHistory:(NSArray *)history {
683
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
684
+ NSError *error = nil;
685
+ NSData *data = [NSJSONSerialization dataWithJSONObject:history options:0 error:&error];
686
+ if (!error && data) {
687
+ NSString *historyJson = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
688
+ [defaults setObject:historyJson forKey:@"BUNDLE_HISTORY"];
689
+ [defaults synchronize];
690
+ }
691
+ }
692
+
693
+ - (NSString *)extractFolderName:(NSString *)path {
694
+ return [[path stringByDeletingLastPathComponent] lastPathComponent];
695
+ }
696
+
697
+ - (void)saveBundleVersion:(NSString *)path version:(NSInteger)version maxVersions:(NSInteger)maxVersions metadata:(NSString *)metadata {
698
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
699
+ NSArray *history = [self loadBundleHistory];
700
+
701
+ // Create new bundle entry
702
+ NSMutableDictionary *newBundle = [NSMutableDictionary dictionary];
703
+ newBundle[@"version"] = @(version);
704
+ newBundle[@"path"] = path;
705
+ newBundle[@"timestamp"] = @((long long)([[NSDate date] timeIntervalSince1970] * 1000));
706
+ if (metadata) {
707
+ newBundle[@"metadata"] = metadata;
708
+ }
709
+
710
+ // Combine with existing history
711
+ NSMutableArray *updatedHistory = [NSMutableArray arrayWithObject:newBundle];
712
+ [updatedHistory addObjectsFromArray:history];
713
+
714
+ // Sort by version descending and remove duplicates
715
+ [updatedHistory sortUsingComparator:^NSComparisonResult(NSDictionary *obj1, NSDictionary *obj2) {
716
+ NSInteger v1 = [obj1[@"version"] integerValue];
717
+ NSInteger v2 = [obj2[@"version"] integerValue];
718
+ if (v1 > v2) return NSOrderedAscending;
719
+ if (v1 < v2) return NSOrderedDescending;
720
+ return NSOrderedSame;
721
+ }];
722
+
723
+ // Remove duplicates by version
724
+ NSMutableArray *uniqueHistory = [NSMutableArray array];
725
+ NSMutableSet *seenVersions = [NSMutableSet set];
726
+ for (NSDictionary *bundle in updatedHistory) {
727
+ NSInteger v = [bundle[@"version"] integerValue];
728
+ if (![seenVersions containsObject:@(v)]) {
729
+ [seenVersions addObject:@(v)];
730
+ [uniqueHistory addObject:bundle];
731
+ }
732
+ }
733
+
734
+ // Keep only maxVersions most recent
735
+ NSArray *finalHistory = [uniqueHistory subarrayWithRange:NSMakeRange(0, MIN(maxVersions, uniqueHistory.count))];
736
+
737
+ // Delete old versions beyond limit
738
+ NSMutableSet *versionsToKeep = [NSMutableSet set];
739
+ for (NSDictionary *bundle in finalHistory) {
740
+ [versionsToKeep addObject:bundle[@"version"]];
741
+ }
742
+
743
+ for (NSDictionary *bundle in uniqueHistory) {
744
+ if (![versionsToKeep containsObject:bundle[@"version"]]) {
745
+ [VexorCodePush deleteBundleAtPath:bundle[@"path"]];
746
+ }
747
+ }
748
+
749
+ // Save updated history
750
+ [self saveBundleHistory:finalHistory];
751
+
752
+ // Before updating current path, save it as fallback for crash handler
753
+ NSString *currentPath = [defaults stringForKey:@"PATH"];
754
+ NSString *currentVersion = [defaults stringForKey:@"VERSION"];
755
+ if (currentPath && currentPath.length > 0) {
756
+ [defaults setObject:currentPath forKey:@"PREVIOUS_BUNDLE_PATH"];
757
+ if (currentVersion) {
758
+ [defaults setObject:currentVersion forKey:@"PREVIOUS_BUNDLE_VERSION"];
759
+ }
760
+ }
761
+
762
+ // Set current path and version
763
+ [defaults setObject:path forKey:@"PATH"];
764
+ [defaults setObject:[NSString stringWithFormat:@"%ld", (long)version] forKey:@"VERSION"];
765
+ [defaults synchronize];
766
+ }
767
+
768
+ RCT_EXPORT_METHOD(restart) {
769
+ if ([NSThread isMainThread]) {
770
+ [self loadBundle];
771
+ } else {
772
+ dispatch_sync(dispatch_get_main_queue(), ^{
773
+ [self loadBundle];
774
+ });
775
+ }
776
+ return;
777
+ }
778
+
779
+
780
+ RCT_EXPORT_METHOD(getBundleList:(double)a
781
+ resolve:(RCTPromiseResolveBlock)resolve
782
+ reject:(RCTPromiseRejectBlock)reject) {
783
+ NSArray *history = [self loadBundleHistory];
784
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
785
+ NSString *activePath = [defaults stringForKey:@"PATH"];
786
+
787
+ NSMutableArray *bundleList = [NSMutableArray array];
788
+ for (NSDictionary *bundle in history) {
789
+ NSString *path = bundle[@"path"];
790
+ NSString *folderName = [self extractFolderName:path];
791
+ NSMutableDictionary *bundleInfo = [NSMutableDictionary dictionary];
792
+ bundleInfo[@"id"] = folderName;
793
+ bundleInfo[@"version"] = bundle[@"version"];
794
+ bundleInfo[@"date"] = bundle[@"timestamp"];
795
+ bundleInfo[@"path"] = path;
796
+ bundleInfo[@"isActive"] = @([path isEqualToString:activePath]);
797
+ if (bundle[@"metadata"]) {
798
+ NSError *error = nil;
799
+ id metadata = [NSJSONSerialization JSONObjectWithData:[bundle[@"metadata"] dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error];
800
+ bundleInfo[@"metadata"] = error ? bundle[@"metadata"] : metadata;
801
+ } else {
802
+ bundleInfo[@"metadata"] = [NSNull null];
803
+ }
804
+ [bundleList addObject:bundleInfo];
805
+ }
806
+
807
+ NSError *error = nil;
808
+ NSData *data = [NSJSONSerialization dataWithJSONObject:bundleList options:0 error:&error];
809
+ if (!error && data) {
810
+ NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
811
+ resolve(jsonString);
812
+ } else {
813
+ reject(@"GET_BUNDLE_LIST_ERROR", @"Failed to serialize bundle list", error);
814
+ }
815
+ }
816
+
817
+ RCT_EXPORT_METHOD(deleteBundleById:(NSString *)id
818
+ resolve:(RCTPromiseResolveBlock)resolve
819
+ reject:(RCTPromiseRejectBlock)reject) {
820
+ NSArray *history = [self loadBundleHistory];
821
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
822
+ NSString *activePath = [defaults stringForKey:@"PATH"];
823
+
824
+ NSDictionary *bundleToDelete = nil;
825
+ for (NSDictionary *bundle in history) {
826
+ NSString *folderName = [self extractFolderName:bundle[@"path"]];
827
+ if ([folderName isEqualToString:id]) {
828
+ bundleToDelete = bundle;
829
+ break;
830
+ }
831
+ }
832
+
833
+ if (!bundleToDelete) {
834
+ resolve(@(NO));
835
+ return;
836
+ }
837
+
838
+ // If deleting active bundle, rollback to oldest remaining bundle or clear
839
+ if ([bundleToDelete[@"path"] isEqualToString:activePath]) {
840
+ NSMutableArray *remainingBundles = [NSMutableArray array];
841
+ for (NSDictionary *bundle in history) {
842
+ if (![bundle[@"path"] isEqualToString:bundleToDelete[@"path"]]) {
843
+ [remainingBundles addObject:bundle];
844
+ }
845
+ }
846
+ if (remainingBundles.count > 0) {
847
+ NSDictionary *oldestBundle = [remainingBundles sortedArrayUsingComparator:^NSComparisonResult(NSDictionary *obj1, NSDictionary *obj2) {
848
+ NSInteger v1 = [obj1[@"version"] integerValue];
849
+ NSInteger v2 = [obj2[@"version"] integerValue];
850
+ if (v1 < v2) return NSOrderedAscending;
851
+ if (v1 > v2) return NSOrderedDescending;
852
+ return NSOrderedSame;
853
+ }].firstObject;
854
+ [defaults setObject:oldestBundle[@"path"] forKey:@"PATH"];
855
+ [defaults setObject:[NSString stringWithFormat:@"%@", oldestBundle[@"version"]] forKey:@"VERSION"];
856
+ } else {
857
+ [defaults removeObjectForKey:@"PATH"];
858
+ [defaults removeObjectForKey:@"VERSION"];
859
+ }
860
+ [defaults synchronize];
861
+ }
862
+
863
+ // Delete bundle folder
864
+ BOOL isDeleted = [VexorCodePush deleteBundleAtPath:bundleToDelete[@"path"]];
865
+
866
+ // Remove from history
867
+ NSMutableArray *updatedHistory = [NSMutableArray array];
868
+ for (NSDictionary *bundle in history) {
869
+ if (![bundle[@"path"] isEqualToString:bundleToDelete[@"path"]]) {
870
+ [updatedHistory addObject:bundle];
871
+ }
872
+ }
873
+ [self saveBundleHistory:updatedHistory];
874
+
875
+ resolve(@(isDeleted));
876
+ }
877
+
878
+ RCT_EXPORT_METHOD(clearAllBundles:(double)a
879
+ resolve:(RCTPromiseResolveBlock)resolve
880
+ reject:(RCTPromiseRejectBlock)reject) {
881
+ NSArray *history = [self loadBundleHistory];
882
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
883
+
884
+ // Delete all bundle folders
885
+ for (NSDictionary *bundle in history) {
886
+ [VexorCodePush deleteBundleAtPath:bundle[@"path"]];
887
+ }
888
+
889
+ // Clear history
890
+ [self saveBundleHistory:@[]];
891
+
892
+ // Clear current path and version
893
+ [defaults removeObjectForKey:@"PATH"];
894
+ [defaults removeObjectForKey:@"VERSION"];
895
+ [defaults synchronize];
896
+
897
+ resolve(@(YES));
898
+ }
899
+
900
+ RCT_EXPORT_METHOD(writeFile:(NSString *)path
901
+ base64Content:(NSString *)base64Content
902
+ encoding:(NSString *)encoding
903
+ resolve:(RCTPromiseResolveBlock)resolve
904
+ reject:(RCTPromiseRejectBlock)reject) {
905
+ // Run on background queue to avoid blocking JS thread
906
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
907
+ @try {
908
+ // Decode base64 to NSData
909
+ NSData *data = [[NSData alloc] initWithBase64EncodedString:base64Content options:0];
910
+ if (!data) {
911
+ reject(@"DECODE_ERROR", @"Failed to decode base64 content", nil);
912
+ return;
913
+ }
914
+
915
+ // Ensure parent directory exists
916
+ NSString *parentDir = [path stringByDeletingLastPathComponent];
917
+ NSFileManager *fileManager = [NSFileManager defaultManager];
918
+ if (![fileManager fileExistsAtPath:parentDir]) {
919
+ NSError *error;
920
+ if (![fileManager createDirectoryAtPath:parentDir withIntermediateDirectories:YES attributes:nil error:&error]) {
921
+ reject(@"CREATE_DIR_ERROR", [NSString stringWithFormat:@"Failed to create directory: %@", error.localizedDescription], error);
922
+ return;
923
+ }
924
+ }
925
+
926
+ // Write file on background thread
927
+ NSError *error;
928
+ if (![data writeToFile:path options:NSDataWritingAtomic error:&error]) {
929
+ reject(@"WRITE_ERROR", [NSString stringWithFormat:@"Failed to write file: %@", error.localizedDescription], error);
930
+ return;
931
+ }
932
+
933
+ resolve(@(YES));
934
+ } @catch (NSException *exception) {
935
+ reject(@"WRITE_ERROR", [NSString stringWithFormat:@"Unexpected error: %@", exception.reason], nil);
936
+ }
937
+ });
938
+ }
939
+
940
+ // Don't compile this code when we build for the old architecture.
941
+ #ifdef RCT_NEW_ARCH_ENABLED
942
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
943
+ (const facebook::react::ObjCTurboModule::InitParams &)params
944
+ {
945
+ return std::make_shared<facebook::react::NativeVexorCodePushSpecJSI>(params);
946
+ }
947
+ #endif
948
+
949
+ @end