@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.
- package/LICENSE +20 -0
- package/README.md +61 -0
- package/android/build.gradle +129 -0
- package/android/generated/java/com/vexorpush/codepush/NativeVexorCodePushSpec.java +91 -0
- package/android/generated/jni/CMakeLists.txt +36 -0
- package/android/generated/jni/RNVexorCodePushSpec-generated.cpp +110 -0
- package/android/generated/jni/RNVexorCodePushSpec.h +31 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ComponentDescriptors.cpp +22 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ComponentDescriptors.h +24 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/EventEmitters.cpp +16 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/EventEmitters.h +17 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/Props.cpp +19 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/Props.h +18 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/RNVexorCodePushSpecJSI-generated.cpp +129 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/RNVexorCodePushSpecJSI.h +184 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ShadowNodes.cpp +17 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/ShadowNodes.h +23 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/States.cpp +16 -0
- package/android/generated/jni/react/renderer/components/RNVexorCodePushSpec/States.h +19 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/vexorpush/codepush/CrashHandler.kt +78 -0
- package/android/src/main/java/com/vexorpush/codepush/SharedPrefs.kt +60 -0
- package/android/src/main/java/com/vexorpush/codepush/Utils.kt +105 -0
- package/android/src/main/java/com/vexorpush/codepush/VexorCodePush.kt +100 -0
- package/android/src/main/java/com/vexorpush/codepush/VexorCodePushModule.kt +698 -0
- package/android/src/newarch/VexorCodePushSpec.kt +7 -0
- package/android/src/oldarch/VexorCodePushSpec.kt +24 -0
- package/app.plugin.js +1 -0
- package/current-version.js +27 -0
- package/deployment-state.js +33 -0
- package/index.d.ts +101 -0
- package/index.js +535 -0
- package/ios/VexorCodePush.h +13 -0
- package/ios/VexorCodePush.mm +949 -0
- package/ios/generated/RNVexorCodePushSpec/ComponentDescriptors.cpp +22 -0
- package/ios/generated/RNVexorCodePushSpec/ComponentDescriptors.h +24 -0
- package/ios/generated/RNVexorCodePushSpec/EventEmitters.cpp +16 -0
- package/ios/generated/RNVexorCodePushSpec/EventEmitters.h +17 -0
- package/ios/generated/RNVexorCodePushSpec/Props.cpp +19 -0
- package/ios/generated/RNVexorCodePushSpec/Props.h +18 -0
- package/ios/generated/RNVexorCodePushSpec/RCTComponentViewHelpers.h +18 -0
- package/ios/generated/RNVexorCodePushSpec/RNVexorCodePushSpec-generated.mm +130 -0
- package/ios/generated/RNVexorCodePushSpec/RNVexorCodePushSpec.h +109 -0
- package/ios/generated/RNVexorCodePushSpec/ShadowNodes.cpp +17 -0
- package/ios/generated/RNVexorCodePushSpec/ShadowNodes.h +23 -0
- package/ios/generated/RNVexorCodePushSpec/States.cpp +16 -0
- package/ios/generated/RNVexorCodePushSpec/States.h +19 -0
- package/ios/generated/RNVexorCodePushSpecJSI-generated.cpp +129 -0
- package/ios/generated/RNVexorCodePushSpecJSI.h +184 -0
- package/package.json +51 -0
- package/plugin/src/index.ts +92 -0
- package/plugin/tsconfig.json +9 -0
- package/react-native-code-push.podspec +48 -0
- package/react-native.config.js +13 -0
- 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
|