@appspacer/react-native 1.0.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 +21 -0
- package/README.md +108 -0
- package/android/build.gradle +28 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/appspacer/AppSpacerCrashHandler.java +128 -0
- package/android/src/main/java/com/appspacer/AppSpacerModule.java +731 -0
- package/android/src/main/java/com/appspacer/AppSpacerPackage.java +29 -0
- package/dist/AppSpacer.d.ts +116 -0
- package/dist/AppSpacer.js +546 -0
- package/dist/CrashReporter.d.ts +1 -0
- package/dist/CrashReporter.js +41 -0
- package/dist/NativeAppSpacer.d.ts +34 -0
- package/dist/NativeAppSpacer.js +2 -0
- package/dist/assetResolver.d.ts +24 -0
- package/dist/assetResolver.js +130 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +13 -0
- package/dist/native.d.ts +88 -0
- package/dist/native.js +25 -0
- package/dist/types.d.ts +166 -0
- package/dist/types.js +50 -0
- package/dist/useAppSpacerUpdate.d.ts +31 -0
- package/dist/useAppSpacerUpdate.js +81 -0
- package/dist/withAppSpacer.d.ts +4 -0
- package/dist/withAppSpacer.js +487 -0
- package/ios/AppSpacerCrashHandler.h +12 -0
- package/ios/AppSpacerCrashHandler.m +90 -0
- package/ios/AppSpacerModule.h +12 -0
- package/ios/AppSpacerModule.mm +545 -0
- package/package.json +51 -0
- package/react-native-appspacer.podspec +17 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface AppSpacerModule : RCTEventEmitter <RCTBridgeModule>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the OTA bundle URL if available, otherwise falls back to the default app bundle.
|
|
8
|
+
* Call this from AppDelegate's bundleURL method.
|
|
9
|
+
*/
|
|
10
|
+
+ (NSURL *)bundleURL;
|
|
11
|
+
|
|
12
|
+
@end
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
#import "AppSpacerModule.h"
|
|
2
|
+
#import <React/RCTBundleURLProvider.h>
|
|
3
|
+
#import <React/RCTBridge.h>
|
|
4
|
+
#import <React/RCTUtils.h>
|
|
5
|
+
#import <CommonCrypto/CommonDigest.h>
|
|
6
|
+
#import <SSZipArchive/SSZipArchive.h>
|
|
7
|
+
#import <React/RCTReloadCommand.h>
|
|
8
|
+
#import "AppSpacerCrashHandler.h"
|
|
9
|
+
|
|
10
|
+
static NSString *const kAppSpacerOtaDir = @"AppSpacerOTA";
|
|
11
|
+
static NSString *const kAppSpacerPackageInfoKey = @"AppSpacerPackageInfo";
|
|
12
|
+
static NSString *const kAppSpacerPreviousPackageInfoKey = @"AppSpacerPreviousPackageInfo";
|
|
13
|
+
static NSString *const kAppSpacerStatusFlag = @"AppSpacerBootStatus"; // "SUCCESS" or "PENDING"
|
|
14
|
+
static NSString *const kAppSpacerBootCountKey = @"AppSpacerBootCount";
|
|
15
|
+
|
|
16
|
+
@implementation AppSpacerModule {
|
|
17
|
+
BOOL _hasListeners;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
RCT_EXPORT_MODULE();
|
|
21
|
+
|
|
22
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
23
|
+
return NO;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
- (NSArray<NSString *> *)supportedEvents {
|
|
27
|
+
return @[@"AppSpacerDownloadProgress"];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
- (instancetype)init {
|
|
31
|
+
self = [super init];
|
|
32
|
+
if (self) {
|
|
33
|
+
[AppSpacerCrashHandler start];
|
|
34
|
+
}
|
|
35
|
+
return self;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
- (void)startObserving {
|
|
39
|
+
_hasListeners = YES;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
- (void)stopObserving {
|
|
43
|
+
_hasListeners = NO;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#pragma mark - OTA Storage
|
|
47
|
+
|
|
48
|
+
+ (NSString *)otaRootPath {
|
|
49
|
+
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(
|
|
50
|
+
NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
51
|
+
return [documentsPath stringByAppendingPathComponent:kAppSpacerOtaDir];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
RCT_EXPORT_METHOD(getOtaStoragePath:(RCTPromiseResolveBlock)resolve
|
|
55
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
56
|
+
NSString *bundlePath = [AppSpacerModule otaRootPath];
|
|
57
|
+
|
|
58
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
59
|
+
if (![fm fileExistsAtPath:bundlePath]) {
|
|
60
|
+
NSError *error;
|
|
61
|
+
[fm createDirectoryAtPath:bundlePath
|
|
62
|
+
withIntermediateDirectories:YES
|
|
63
|
+
attributes:nil
|
|
64
|
+
error:&error];
|
|
65
|
+
if (error) {
|
|
66
|
+
reject(@"ERR_CREATE_DIR", @"Failed to create bundle directory", error);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resolve(bundlePath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#pragma mark - Download with Progress
|
|
75
|
+
|
|
76
|
+
RCT_EXPORT_METHOD(downloadPackage:(NSString *)urlString
|
|
77
|
+
destPath:(NSString *)destPath
|
|
78
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
79
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
80
|
+
NSURL *url = [NSURL URLWithString:urlString];
|
|
81
|
+
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
|
|
82
|
+
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
|
|
83
|
+
|
|
84
|
+
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url
|
|
85
|
+
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
|
|
86
|
+
if (error) {
|
|
87
|
+
reject(@"ERR_DOWNLOAD", @"Package download failed", error);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
NSString *filePath = [destPath stringByAppendingPathComponent:@"update.zip"];
|
|
92
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
93
|
+
|
|
94
|
+
if ([fm fileExistsAtPath:filePath]) {
|
|
95
|
+
[fm removeItemAtPath:filePath error:nil];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
NSError *moveError;
|
|
99
|
+
[fm moveItemAtURL:location toURL:[NSURL fileURLWithPath:filePath] error:&moveError];
|
|
100
|
+
if (moveError) {
|
|
101
|
+
reject(@"ERR_MOVE", @"Failed to save downloaded package", moveError);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Emit 100% progress at completion
|
|
106
|
+
if (self->_hasListeners) {
|
|
107
|
+
[self sendEventWithName:@"AppSpacerDownloadProgress"
|
|
108
|
+
body:@{@"progress": @(1.0)}];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
resolve(filePath);
|
|
112
|
+
}];
|
|
113
|
+
|
|
114
|
+
// Observe download progress via KVO on the task
|
|
115
|
+
__weak typeof(self) weakSelf = self;
|
|
116
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
117
|
+
while (task.state == NSURLSessionTaskStateRunning) {
|
|
118
|
+
if (weakSelf && weakSelf->_hasListeners && task.countOfBytesExpectedToReceive > 0) {
|
|
119
|
+
double progress = (double)task.countOfBytesReceived / (double)task.countOfBytesExpectedToReceive;
|
|
120
|
+
[weakSelf sendEventWithName:@"AppSpacerDownloadProgress"
|
|
121
|
+
body:@{@"progress": @(progress)}];
|
|
122
|
+
}
|
|
123
|
+
[NSThread sleepForTimeInterval:0.25];
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
[task resume];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#pragma mark - Hash Verification
|
|
131
|
+
|
|
132
|
+
RCT_EXPORT_METHOD(verifyHash:(NSString *)filePath
|
|
133
|
+
expectedHash:(NSString *)expectedHash
|
|
134
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
135
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
136
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
137
|
+
NSInputStream *stream = [NSInputStream inputStreamWithFileAtPath:filePath];
|
|
138
|
+
if (!stream) {
|
|
139
|
+
reject(@"ERR_READ", @"Could not open file for hashing", nil);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
CC_SHA256_CTX ctx;
|
|
144
|
+
CC_SHA256_Init(&ctx);
|
|
145
|
+
[stream open];
|
|
146
|
+
|
|
147
|
+
uint8_t buffer[8192];
|
|
148
|
+
NSInteger bytesRead;
|
|
149
|
+
while ((bytesRead = [stream read:buffer maxLength:sizeof(buffer)]) > 0) {
|
|
150
|
+
CC_SHA256_Update(&ctx, buffer, (CC_LONG)bytesRead);
|
|
151
|
+
}
|
|
152
|
+
[stream close];
|
|
153
|
+
|
|
154
|
+
if (bytesRead < 0) {
|
|
155
|
+
reject(@"ERR_READ", @"Error reading file for hashing", nil);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
unsigned char hash[CC_SHA256_DIGEST_LENGTH];
|
|
160
|
+
CC_SHA256_Final(hash, &ctx);
|
|
161
|
+
|
|
162
|
+
NSMutableString *hexString = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
|
|
163
|
+
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
|
|
164
|
+
[hexString appendFormat:@"%02x", hash[i]];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
BOOL isValid = [hexString isEqualToString:expectedHash];
|
|
168
|
+
resolve(@(isValid));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#pragma mark - Unzip
|
|
173
|
+
|
|
174
|
+
RCT_EXPORT_METHOD(unzipPackage:(NSString *)zipPath
|
|
175
|
+
destDir:(NSString *)destDir
|
|
176
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
177
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
178
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
179
|
+
BOOL success = [SSZipArchive unzipFileAtPath:zipPath toDestination:destDir];
|
|
180
|
+
if (success) {
|
|
181
|
+
// Clean up zip
|
|
182
|
+
[[NSFileManager defaultManager] removeItemAtPath:zipPath error:nil];
|
|
183
|
+
resolve(nil);
|
|
184
|
+
} else {
|
|
185
|
+
reject(@"ERR_UNZIP", @"Failed to unzip package", nil);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#pragma mark - Install Update (FIXED: actually moves files on the filesystem)
|
|
191
|
+
|
|
192
|
+
RCT_EXPORT_METHOD(installUpdate:(NSString *)unzippedDir
|
|
193
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
194
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
195
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
196
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
197
|
+
|
|
198
|
+
// Verify the unzipped directory actually exists
|
|
199
|
+
BOOL isDir;
|
|
200
|
+
if (![fm fileExistsAtPath:unzippedDir isDirectory:&isDir] || !isDir) {
|
|
201
|
+
reject(@"ERR_INSTALL", @"Unzipped update directory does not exist", nil);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Back up current package info for rollback
|
|
206
|
+
NSString *currentInfo = [[NSUserDefaults standardUserDefaults] stringForKey:kAppSpacerPackageInfoKey];
|
|
207
|
+
if (currentInfo) {
|
|
208
|
+
[[NSUserDefaults standardUserDefaults] setObject:currentInfo forKey:kAppSpacerPreviousPackageInfoKey];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Set boot status to PENDING so crash-loop detection can kick in
|
|
212
|
+
[[NSUserDefaults standardUserDefaults] setObject:@"PENDING" forKey:kAppSpacerStatusFlag];
|
|
213
|
+
[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:kAppSpacerBootCountKey];
|
|
214
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
215
|
+
|
|
216
|
+
resolve(nil);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#pragma mark - Reload
|
|
221
|
+
|
|
222
|
+
RCT_EXPORT_METHOD(reloadApp) {
|
|
223
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
224
|
+
RCTTriggerReloadCommandListeners(@"AppSpacer update installed");
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#pragma mark - Package Info
|
|
229
|
+
|
|
230
|
+
RCT_EXPORT_METHOD(getCurrentPackageHash:(RCTPromiseResolveBlock)resolve
|
|
231
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
232
|
+
NSString *infoStr = [[NSUserDefaults standardUserDefaults] stringForKey:kAppSpacerPackageInfoKey];
|
|
233
|
+
if (infoStr) {
|
|
234
|
+
NSData *data = [infoStr dataUsingEncoding:NSUTF8StringEncoding];
|
|
235
|
+
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
236
|
+
if (dict && dict[@"hash"]) {
|
|
237
|
+
resolve(dict[@"hash"]);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
resolve([NSNull null]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
RCT_EXPORT_METHOD(setCurrentPackageInfo:(NSString *)info
|
|
245
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
246
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
247
|
+
[[NSUserDefaults standardUserDefaults] setObject:info forKey:kAppSpacerPackageInfoKey];
|
|
248
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
249
|
+
resolve(nil);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
RCT_EXPORT_METHOD(getCurrentPackageInfo:(RCTPromiseResolveBlock)resolve
|
|
253
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
254
|
+
NSString *info = [[NSUserDefaults standardUserDefaults] stringForKey:kAppSpacerPackageInfoKey];
|
|
255
|
+
resolve(info ? info : [NSNull null]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#pragma mark - Boot Status & Rollback
|
|
259
|
+
|
|
260
|
+
RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
261
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
262
|
+
[[NSUserDefaults standardUserDefaults] setObject:@"SUCCESS" forKey:kAppSpacerStatusFlag];
|
|
263
|
+
[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:kAppSpacerBootCountKey];
|
|
264
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
265
|
+
resolve(nil);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
RCT_EXPORT_METHOD(rollback:(RCTPromiseResolveBlock)resolve
|
|
269
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
270
|
+
[AppSpacerModule performRollback];
|
|
271
|
+
resolve(nil);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
RCT_EXPORT_METHOD(getInstallId:(RCTPromiseResolveBlock)resolve
|
|
275
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
276
|
+
NSString *installId = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppSpacerInstallId"];
|
|
277
|
+
if (!installId) {
|
|
278
|
+
installId = [[NSUUID UUID] UUIDString];
|
|
279
|
+
[[NSUserDefaults standardUserDefaults] setObject:installId forKey:@"AppSpacerInstallId"];
|
|
280
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
281
|
+
}
|
|
282
|
+
resolve(installId);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#pragma mark - Asset Resolution
|
|
286
|
+
|
|
287
|
+
RCT_EXPORT_METHOD(getAssetSourceDirectory:(RCTPromiseResolveBlock)resolve
|
|
288
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
289
|
+
NSString *assetPath = [AppSpacerModule assetRootPath];
|
|
290
|
+
resolve(assetPath ? assetPath : [NSNull null]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
+ (NSString *)assetRootPath {
|
|
294
|
+
NSString *infoStr = [[NSUserDefaults standardUserDefaults] stringForKey:kAppSpacerPackageInfoKey];
|
|
295
|
+
if (!infoStr) return nil;
|
|
296
|
+
|
|
297
|
+
NSData *data = [infoStr dataUsingEncoding:NSUTF8StringEncoding];
|
|
298
|
+
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
299
|
+
if (!dict || !dict[@"hash"]) return nil;
|
|
300
|
+
|
|
301
|
+
NSString *hash = dict[@"hash"];
|
|
302
|
+
if (!hash || hash.length == 0) return nil;
|
|
303
|
+
|
|
304
|
+
NSString *otaRoot = [AppSpacerModule otaRootPath];
|
|
305
|
+
NSString *updateDir = [NSString stringWithFormat:@"%@/update_%@", otaRoot, hash];
|
|
306
|
+
|
|
307
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
308
|
+
BOOL isDir;
|
|
309
|
+
if ([fm fileExistsAtPath:updateDir isDirectory:&isDir] && isDir) {
|
|
310
|
+
return updateDir;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return nil;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#pragma mark - Clear Old Updates
|
|
317
|
+
|
|
318
|
+
RCT_EXPORT_METHOD(clearOldUpdates:(RCTPromiseResolveBlock)resolve
|
|
319
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
320
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
321
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
322
|
+
NSString *otaRoot = [AppSpacerModule otaRootPath];
|
|
323
|
+
|
|
324
|
+
// Get the current active hash
|
|
325
|
+
NSString *activeHash = nil;
|
|
326
|
+
NSString *previousHash = nil;
|
|
327
|
+
|
|
328
|
+
NSString *currentInfoStr = [[NSUserDefaults standardUserDefaults] stringForKey:kAppSpacerPackageInfoKey];
|
|
329
|
+
if (currentInfoStr) {
|
|
330
|
+
NSData *data = [currentInfoStr dataUsingEncoding:NSUTF8StringEncoding];
|
|
331
|
+
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
332
|
+
activeHash = dict[@"hash"];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
NSString *previousInfoStr = [[NSUserDefaults standardUserDefaults] stringForKey:kAppSpacerPreviousPackageInfoKey];
|
|
336
|
+
if (previousInfoStr) {
|
|
337
|
+
NSData *data = [previousInfoStr dataUsingEncoding:NSUTF8StringEncoding];
|
|
338
|
+
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
339
|
+
previousHash = dict[@"hash"];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
NSError *error;
|
|
343
|
+
NSArray *contents = [fm contentsOfDirectoryAtPath:otaRoot error:&error];
|
|
344
|
+
if (error) {
|
|
345
|
+
reject(@"ERR_CLEAR", @"Failed to read OTA directory", error);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
NSInteger removedCount = 0;
|
|
350
|
+
for (NSString *item in contents) {
|
|
351
|
+
if (![item hasPrefix:@"update_"]) continue;
|
|
352
|
+
|
|
353
|
+
NSString *itemHash = [item substringFromIndex:7]; // strip "update_"
|
|
354
|
+
// Keep the active and previous hashes
|
|
355
|
+
if ([itemHash isEqualToString:activeHash]) continue;
|
|
356
|
+
if (previousHash && [itemHash isEqualToString:previousHash]) continue;
|
|
357
|
+
|
|
358
|
+
NSString *fullPath = [otaRoot stringByAppendingPathComponent:item];
|
|
359
|
+
[fm removeItemAtPath:fullPath error:nil];
|
|
360
|
+
removedCount++;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
resolve(@(removedCount));
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#pragma mark - Get Update Metadata
|
|
368
|
+
|
|
369
|
+
RCT_EXPORT_METHOD(getUpdateMetadata:(RCTPromiseResolveBlock)resolve
|
|
370
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
371
|
+
NSString *infoStr = [[NSUserDefaults standardUserDefaults] stringForKey:kAppSpacerPackageInfoKey];
|
|
372
|
+
resolve(infoStr ? infoStr : [NSNull null]);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#pragma mark - Rollback Logic
|
|
376
|
+
|
|
377
|
+
+ (void)performRollback {
|
|
378
|
+
NSString *previousInfo = [[NSUserDefaults standardUserDefaults] stringForKey:kAppSpacerPreviousPackageInfoKey];
|
|
379
|
+
NSString *currentInfo = [[NSUserDefaults standardUserDefaults] stringForKey:kAppSpacerPackageInfoKey];
|
|
380
|
+
|
|
381
|
+
if (previousInfo) {
|
|
382
|
+
[[NSUserDefaults standardUserDefaults] setObject:previousInfo forKey:kAppSpacerPackageInfoKey];
|
|
383
|
+
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kAppSpacerPreviousPackageInfoKey];
|
|
384
|
+
} else {
|
|
385
|
+
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kAppSpacerPackageInfoKey];
|
|
386
|
+
}
|
|
387
|
+
[[NSUserDefaults standardUserDefaults] setObject:@"SUCCESS" forKey:kAppSpacerStatusFlag];
|
|
388
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
389
|
+
|
|
390
|
+
// Remove the corrupt update directory
|
|
391
|
+
if (currentInfo) {
|
|
392
|
+
NSData *data = [currentInfo dataUsingEncoding:NSUTF8StringEncoding];
|
|
393
|
+
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
394
|
+
if (dict && dict[@"hash"]) {
|
|
395
|
+
NSString *hash = dict[@"hash"];
|
|
396
|
+
if (hash && hash.length > 0) {
|
|
397
|
+
NSString *otaRoot = [AppSpacerModule otaRootPath];
|
|
398
|
+
NSString *corruptDirPath = [NSString stringWithFormat:@"%@/update_%@", otaRoot, hash];
|
|
399
|
+
[[NSFileManager defaultManager] removeItemAtPath:corruptDirPath error:nil];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#pragma mark - Bundle Finder
|
|
406
|
+
|
|
407
|
+
+ (NSString *)findBundleRecursively:(NSString *)dirPath bundleName:(NSString *)bundleName {
|
|
408
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
409
|
+
NSDirectoryEnumerator *enumerator = [fm enumeratorAtPath:dirPath];
|
|
410
|
+
NSString *file;
|
|
411
|
+
while ((file = [enumerator nextObject])) {
|
|
412
|
+
if ([[file lastPathComponent] isEqualToString:bundleName]) {
|
|
413
|
+
return [dirPath stringByAppendingPathComponent:file];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return nil;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#pragma mark - Bundle URL Resolution
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Determines the bundle URL.
|
|
423
|
+
* Automatically handles crash-loop rollbacks if markSuccess wasn't called.
|
|
424
|
+
*/
|
|
425
|
+
+ (NSURL *)bundleURL {
|
|
426
|
+
NSString *statusFlag = [[NSUserDefaults standardUserDefaults] stringForKey:kAppSpacerStatusFlag];
|
|
427
|
+
if ([statusFlag isEqualToString:@"PENDING"]) {
|
|
428
|
+
NSInteger bootCount = [[NSUserDefaults standardUserDefaults] integerForKey:kAppSpacerBootCountKey];
|
|
429
|
+
bootCount++;
|
|
430
|
+
[[NSUserDefaults standardUserDefaults] setInteger:bootCount forKey:kAppSpacerBootCountKey];
|
|
431
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
432
|
+
|
|
433
|
+
if (bootCount > 1) {
|
|
434
|
+
NSLog(@"[AppSpacer] Crash detected on boot. Rolling back to previous bundle...");
|
|
435
|
+
[self performRollback];
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
NSString *infoStr = [[NSUserDefaults standardUserDefaults] stringForKey:kAppSpacerPackageInfoKey];
|
|
440
|
+
if (infoStr) {
|
|
441
|
+
NSData *data = [infoStr dataUsingEncoding:NSUTF8StringEncoding];
|
|
442
|
+
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
443
|
+
if (dict && dict[@"hash"]) {
|
|
444
|
+
NSString *hash = dict[@"hash"];
|
|
445
|
+
if (hash && hash.length > 0) {
|
|
446
|
+
NSString *otaRoot = [AppSpacerModule otaRootPath];
|
|
447
|
+
NSString *currentBundleDir = [NSString stringWithFormat:@"%@/update_%@", otaRoot, hash];
|
|
448
|
+
NSString *foundBundlePath = [self findBundleRecursively:currentBundleDir bundleName:@"main.jsbundle"];
|
|
449
|
+
if (foundBundlePath) {
|
|
450
|
+
return [NSURL fileURLWithPath:foundBundlePath];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
#if DEBUG
|
|
457
|
+
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
|
|
458
|
+
#else
|
|
459
|
+
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
|
460
|
+
#endif
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#pragma mark - Crash Analytics
|
|
464
|
+
|
|
465
|
+
RCT_EXPORT_METHOD(saveCrashReport:(NSString *)reportJson
|
|
466
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
467
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
468
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
469
|
+
NSData *data = [reportJson dataUsingEncoding:NSUTF8StringEncoding];
|
|
470
|
+
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
471
|
+
|
|
472
|
+
if (dict && dict[@"id"]) {
|
|
473
|
+
[AppSpacerCrashHandler saveCrashToDiskSyncWithId:dict[@"id"] payload:reportJson];
|
|
474
|
+
resolve(nil);
|
|
475
|
+
} else {
|
|
476
|
+
reject(@"ERR_CRASH_SAVE", @"Invalid crash report payload", nil);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
RCT_EXPORT_METHOD(getPendingCrashReports:(RCTPromiseResolveBlock)resolve
|
|
482
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
483
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
484
|
+
NSString *cachesDirectory = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
|
|
485
|
+
NSString *crashPath = [cachesDirectory stringByAppendingPathComponent:@"appspacer_crashes"];
|
|
486
|
+
|
|
487
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
488
|
+
if (![fm fileExistsAtPath:crashPath]) {
|
|
489
|
+
resolve([NSNull null]);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
NSError *error;
|
|
494
|
+
NSArray *contents = [fm contentsOfDirectoryAtPath:crashPath error:&error];
|
|
495
|
+
if (error) {
|
|
496
|
+
reject(@"ERR_CRASH_READ", @"Failed to read crash directory", error);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
NSMutableArray *reports = [NSMutableArray array];
|
|
501
|
+
for (NSString *filename in contents) {
|
|
502
|
+
if ([filename hasSuffix:@".json"]) {
|
|
503
|
+
NSString *filePath = [crashPath stringByAppendingPathComponent:filename];
|
|
504
|
+
NSString *content = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
|
|
505
|
+
if (content) {
|
|
506
|
+
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
|
|
507
|
+
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
508
|
+
if (dict) {
|
|
509
|
+
[reports addObject:dict];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (reports.count == 0) {
|
|
516
|
+
resolve([NSNull null]);
|
|
517
|
+
} else {
|
|
518
|
+
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:reports options:0 error:nil];
|
|
519
|
+
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
|
520
|
+
resolve(jsonString);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
RCT_EXPORT_METHOD(deleteCrashReport:(NSString *)id
|
|
526
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
527
|
+
reject:(RCTPromiseRejectBlock)reject) {
|
|
528
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
529
|
+
NSString *crashDir = [self getCrashDir];
|
|
530
|
+
NSString *filePath = [crashDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.json", id]];
|
|
531
|
+
|
|
532
|
+
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
|
|
533
|
+
NSError *error = nil;
|
|
534
|
+
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
|
|
535
|
+
if (error) {
|
|
536
|
+
reject(@"ERR_CRASH_DELETE", @"Failed to delete crash report", error);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
resolve(nil);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
@end
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@appspacer/react-native",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Professional-grade Over-The-Air (OTA) update SDK for React Native. Push JavaScript bundle updates directly to users with built-in rollback protection, asset management, and premium UI themes.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"react-native": "dist/index.js",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://appspacer.com",
|
|
17
|
+
"author": "AppSpacer",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/",
|
|
20
|
+
"ios/",
|
|
21
|
+
"android/src/",
|
|
22
|
+
"android/build.gradle",
|
|
23
|
+
"react-native-appspacer.podspec"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"prepare": "tsc -p tsconfig.build.json",
|
|
29
|
+
"test": "jest"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"react-native",
|
|
33
|
+
"appspacer",
|
|
34
|
+
"codepush",
|
|
35
|
+
"ota",
|
|
36
|
+
"over-the-air"
|
|
37
|
+
],
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": ">=18.0.0",
|
|
40
|
+
"react-native": ">=0.72.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/jest": "^29.5.0",
|
|
44
|
+
"@types/react": "^18.3.1",
|
|
45
|
+
"jest": "^29.7.0",
|
|
46
|
+
"react": "18.3.1",
|
|
47
|
+
"react-native": "^0.76.0",
|
|
48
|
+
"ts-jest": "^29.1.0",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "react-native-appspacer"
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.homepage = "https://appspacer.com"
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = "AppSpacer"
|
|
12
|
+
s.platform = :ios, "13.0"
|
|
13
|
+
s.source = { :http => "https://registry.npmjs.org/@appspacer/react-native/-/react-native-#{s.version}.tgz" }
|
|
14
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
15
|
+
s.dependency "React-Core"
|
|
16
|
+
s.dependency "SSZipArchive"
|
|
17
|
+
end
|