@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.
@@ -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