react-native-bspatch 4.0.3 → 5.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.
Files changed (81) hide show
  1. package/README.md +87 -30
  2. package/android/build.gradle +25 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +86 -6
  5. package/android/src/main/res/values/bspatch_public_key.xml +5 -0
  6. package/cpp/bsdiff/bspatch_wrapper.c +34 -0
  7. package/cpp/bsdiff/bspatch_wrapper.h +2 -0
  8. package/cpp/bspatch_jni.cpp +18 -0
  9. package/ios/RNBsPatch.mm +142 -1
  10. package/lib/commonjs/NativeBsPatch.js.map +1 -1
  11. package/lib/commonjs/index.js +9 -2
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/ota/OtaProvider.js +124 -32
  14. package/lib/commonjs/ota/OtaProvider.js.map +1 -1
  15. package/lib/commonjs/ota/eligibility.js +40 -0
  16. package/lib/commonjs/ota/eligibility.js.map +1 -0
  17. package/lib/commonjs/ota/entrySignature.js +23 -0
  18. package/lib/commonjs/ota/entrySignature.js.map +1 -0
  19. package/lib/commonjs/ota/installId.js +27 -0
  20. package/lib/commonjs/ota/installId.js.map +1 -0
  21. package/lib/commonjs/ota/otaCore.js +96 -21
  22. package/lib/commonjs/ota/otaCore.js.map +1 -1
  23. package/lib/commonjs/ota/storage/{GitHubRawStorage.js → R2Storage.js} +20 -15
  24. package/lib/commonjs/ota/storage/R2Storage.js.map +1 -0
  25. package/lib/commonjs/ota/types.js.map +1 -1
  26. package/lib/module/NativeBsPatch.js.map +1 -1
  27. package/lib/module/index.js +2 -1
  28. package/lib/module/index.js.map +1 -1
  29. package/lib/module/ota/OtaProvider.js +124 -32
  30. package/lib/module/ota/OtaProvider.js.map +1 -1
  31. package/lib/module/ota/eligibility.js +34 -0
  32. package/lib/module/ota/eligibility.js.map +1 -0
  33. package/lib/module/ota/entrySignature.js +17 -0
  34. package/lib/module/ota/entrySignature.js.map +1 -0
  35. package/lib/module/ota/installId.js +22 -0
  36. package/lib/module/ota/installId.js.map +1 -0
  37. package/lib/module/ota/otaCore.js +96 -21
  38. package/lib/module/ota/otaCore.js.map +1 -1
  39. package/lib/module/ota/storage/{GitHubRawStorage.js → R2Storage.js} +18 -14
  40. package/lib/module/ota/storage/R2Storage.js.map +1 -0
  41. package/lib/module/ota/types.js.map +1 -1
  42. package/lib/typescript/NativeBsPatch.d.ts +28 -2
  43. package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
  44. package/lib/typescript/index.d.ts +3 -1
  45. package/lib/typescript/index.d.ts.map +1 -1
  46. package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
  47. package/lib/typescript/ota/eligibility.d.ts +19 -0
  48. package/lib/typescript/ota/eligibility.d.ts.map +1 -0
  49. package/lib/typescript/ota/entrySignature.d.ts +9 -0
  50. package/lib/typescript/ota/entrySignature.d.ts.map +1 -0
  51. package/lib/typescript/ota/installId.d.ts +3 -0
  52. package/lib/typescript/ota/installId.d.ts.map +1 -0
  53. package/lib/typescript/ota/otaCore.d.ts.map +1 -1
  54. package/lib/typescript/ota/storage/{GitHubRawStorage.d.ts → R2Storage.d.ts} +4 -3
  55. package/lib/typescript/ota/storage/R2Storage.d.ts.map +1 -0
  56. package/lib/typescript/ota/types.d.ts +54 -5
  57. package/lib/typescript/ota/types.d.ts.map +1 -1
  58. package/package.json +3 -2
  59. package/react-native-bspatch.podspec +3 -1
  60. package/src/NativeBsPatch.ts +34 -2
  61. package/src/index.ts +5 -1
  62. package/src/ota/OtaProvider.tsx +128 -43
  63. package/src/ota/eligibility.ts +48 -0
  64. package/src/ota/entrySignature.ts +31 -0
  65. package/src/ota/installId.ts +18 -0
  66. package/src/ota/otaCore.ts +102 -21
  67. package/src/ota/storage/{GitHubRawStorage.ts → R2Storage.ts} +17 -13
  68. package/src/ota/types.ts +51 -5
  69. package/tools/build_ota.sh +343 -116
  70. package/tools/doctor.js +130 -0
  71. package/tools/ensure_signing_key.js +216 -0
  72. package/tools/fingerprint.js +111 -0
  73. package/tools/ios_fingerprint.sh +9 -0
  74. package/tools/ota_admin.js +131 -0
  75. package/tools/setup_project.js +41 -15
  76. package/tools/update_registry.js +111 -11
  77. package/tools/upload_ota.sh +85 -184
  78. package/tools/verify_publish.js +81 -0
  79. package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +0 -1
  80. package/lib/module/ota/storage/GitHubRawStorage.js.map +0 -1
  81. package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +0 -1
package/ios/RNBsPatch.mm CHANGED
@@ -1,9 +1,11 @@
1
1
  #import <Foundation/Foundation.h>
2
+ #import <Network/Network.h>
2
3
  #import <QuartzCore/QuartzCore.h>
3
4
  #import <CommonCrypto/CommonDigest.h>
4
5
  #import <Security/Security.h>
5
6
  #import <React/RCTLog.h>
6
7
  #import <React/RCTReloadCommand.h>
8
+ #include <zlib.h>
7
9
 
8
10
  #if __has_include(<react_native_bspatch/react_native_bspatch-Swift.h>)
9
11
  #import <react_native_bspatch/react_native_bspatch-Swift.h>
@@ -29,6 +31,8 @@ extern "C" {
29
31
  /** One download: streams to a system temp file, moved to destPath only on a 2xx response. */
30
32
  @interface RNBsPatchDownload : NSObject <NSURLSessionDownloadDelegate>
31
33
  @property (nonatomic, copy) NSString *destPath;
34
+ /** Where a failed attempt leaves NSURLSession resume data for the next call */
35
+ @property (nonatomic, copy) NSString *resumePath;
32
36
  @property (nonatomic, copy) RCTPromiseResolveBlock resolve;
33
37
  @property (nonatomic, copy) RCTPromiseRejectBlock reject;
34
38
  @property (nonatomic, copy) void (^onProgress)(int64_t received, int64_t total);
@@ -48,6 +52,10 @@ totalBytesExpectedToWrite:(int64_t)expected
48
52
  CFTimeInterval now = CACurrentMediaTime();
49
53
  if (now - self.lastEmit < 0.25) return;
50
54
  self.lastEmit = now;
55
+ // written counts decoded bytes, expected the Content-Length on the wire: with a
56
+ // Content-Encoding they don't compare, so report the total as unknown like Android
57
+ NSString *encoding = [(NSHTTPURLResponse *)task.response valueForHTTPHeaderField:@"Content-Encoding"];
58
+ if (encoding.length > 0 && ![encoding isEqualToString:@"identity"]) expected = -1;
51
59
  self.onProgress(written, expected); // expected is -1 without a Content-Length
52
60
  }
53
61
 
@@ -79,6 +87,10 @@ didFinishDownloadingToURL:(NSURL *)location
79
87
  - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
80
88
  {
81
89
  NSError *e = error ?: self.error;
90
+ NSData *resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
91
+ if (resumeData) {
92
+ [resumeData writeToFile:self.resumePath atomically:YES];
93
+ }
82
94
  if (e) {
83
95
  self.reject(@"ERR_DOWNLOAD", e.localizedDescription, e);
84
96
  } else {
@@ -109,6 +121,32 @@ static NSData *RNBsPatchSHA256(NSString *path)
109
121
  return [NSData dataWithBytes:digest length:sizeof(digest)];
110
122
  }
111
123
 
124
+ /** Gunzips src into dst; NO on unreadable, non-gzip or truncated input. */
125
+ static BOOL RNBsPatchGunzip(NSString *src, NSString *dst)
126
+ {
127
+ gzFile in = gzopen(src.fileSystemRepresentation, "rb");
128
+ if (!in) return NO;
129
+ FILE *out = fopen(dst.fileSystemRepresentation, "wb");
130
+ if (!out) {
131
+ gzclose(in);
132
+ return NO;
133
+ }
134
+ char buffer[65536];
135
+ int n;
136
+ BOOL ok = YES;
137
+ while ((n = gzread(in, buffer, sizeof(buffer))) > 0) {
138
+ // gzdirect: zlib copies non-gzip input through as-is
139
+ if (gzdirect(in) || fwrite(buffer, 1, n, out) != (size_t)n) {
140
+ ok = NO;
141
+ break;
142
+ }
143
+ }
144
+ if (n < 0) ok = NO;
145
+ if (gzclose_r(in) != Z_OK) ok = NO; // Z_BUF_ERROR: input ended mid-stream
146
+ if (fclose(out) != 0) ok = NO;
147
+ return ok;
148
+ }
149
+
112
150
  /** Runs block off the JS thread: resolves *result, or rejects with *error when it returns NO. */
113
151
  static void RNBsPatchIO(RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject, BOOL (^block)(id *result, NSError **error))
114
152
  {
@@ -152,6 +190,34 @@ RCT_EXPORT_METHOD(applyPatch:(NSString *)oldPath
152
190
  });
153
191
  }
154
192
 
193
+ RCT_EXPORT_METHOD(decompressFile:(NSString *)srcPath
194
+ destPath:(NSString *)destPath
195
+ format:(NSString *)format
196
+ resolve:(RCTPromiseResolveBlock)resolve
197
+ reject:(RCTPromiseRejectBlock)reject)
198
+ {
199
+ RNBsPatchIO(resolve, reject, ^BOOL(id *result, NSError **error) {
200
+ NSString *part = [destPath stringByAppendingString:@".part"];
201
+ BOOL ok;
202
+ if ([format isEqualToString:@"gzip"]) {
203
+ ok = RNBsPatchGunzip(srcPath, part);
204
+ } else if ([format isEqualToString:@"bz2"]) {
205
+ ok = decompress_bz2(srcPath.fileSystemRepresentation, part.fileSystemRepresentation) == 0;
206
+ } else {
207
+ *error = [NSError errorWithDomain:@"RNBsPatch" code:1 userInfo:@{NSLocalizedDescriptionKey: [@"Unknown compression format: " stringByAppendingString:format]}];
208
+ return NO;
209
+ }
210
+ if (ok) ok = rename(part.fileSystemRepresentation, destPath.fileSystemRepresentation) == 0;
211
+ if (!ok) {
212
+ NSFileManager *fm = NSFileManager.defaultManager;
213
+ [fm removeItemAtPath:part error:nil];
214
+ [fm removeItemAtPath:destPath error:nil];
215
+ *error = [NSError errorWithDomain:@"RNBsPatch" code:2 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Corrupt or unreadable %@ file: %@", format, srcPath]}];
216
+ }
217
+ return ok;
218
+ });
219
+ }
220
+
155
221
  RCT_EXPORT_METHOD(reloadBundle)
156
222
  {
157
223
  dispatch_async(dispatch_get_main_queue(), ^{
@@ -288,9 +354,69 @@ RCT_EXPORT_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve
288
354
  return NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
289
355
  }
290
356
 
357
+ - (NSString *)getEmbeddedPublicKeys
358
+ {
359
+ id keys = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BsPatchPublicKey"];
360
+ return [keys isKindOfClass:[NSString class]] ? keys : @"";
361
+ }
362
+
363
+ - (NSString *)getAppBuild
364
+ {
365
+ id build = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
366
+ return [build isKindOfClass:[NSString class]] ? build : @"";
367
+ }
368
+
369
+ // Written into the .app by the build phase `setup` adds (tools/fingerprint.js)
370
+ - (NSString *)getRuntimeFingerprint
371
+ {
372
+ NSString *path = [[NSBundle mainBundle] pathForResource:@"bspatch_fingerprint" ofType:@"txt"];
373
+ NSString *fingerprint = path ? [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil] : nil;
374
+ return [fingerprint stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet] ?: @"";
375
+ }
376
+
377
+ - (void)getFreeDiskSpace:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
378
+ {
379
+ RNBsPatchIO(resolve, reject, ^BOOL(id *result, NSError **error) {
380
+ NSString *dir = [self getDocumentDir];
381
+ NSNumber *free = nil;
382
+ [[NSURL fileURLWithPath:dir] getResourceValue:&free forKey:NSURLVolumeAvailableCapacityForImportantUsageKey error:nil];
383
+ if (free.longLongValue <= 0) {
384
+ free = [NSFileManager.defaultManager attributesOfFileSystemForPath:dir error:error][NSFileSystemFreeSize];
385
+ }
386
+ *result = free;
387
+ return free != nil;
388
+ });
389
+ }
390
+
391
+ - (void)getNetworkType:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
392
+ {
393
+ nw_path_monitor_t monitor = nw_path_monitor_create();
394
+ nw_path_monitor_set_queue(monitor, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0));
395
+ __block BOOL done = NO;
396
+ // The first update is the current path; cancelling releases the handler (and the monitor)
397
+ nw_path_monitor_set_update_handler(monitor, ^(nw_path_t path) {
398
+ if (done) return;
399
+ done = YES;
400
+ NSString *type = @"unknown";
401
+ if (nw_path_get_status(path) != nw_path_status_satisfied) {
402
+ type = @"none";
403
+ } else if (nw_path_uses_interface_type(path, nw_interface_type_wifi)) {
404
+ type = @"wifi";
405
+ } else if (nw_path_uses_interface_type(path, nw_interface_type_cellular)) {
406
+ type = @"cellular";
407
+ } else if (nw_path_uses_interface_type(path, nw_interface_type_wired)) {
408
+ type = @"ethernet";
409
+ }
410
+ resolve(type);
411
+ nw_path_monitor_cancel(monitor);
412
+ });
413
+ nw_path_monitor_start(monitor);
414
+ }
415
+
291
416
  - (void)downloadFile:(NSString *)url
292
417
  destPath:(NSString *)destPath
293
418
  timeoutMs:(double)timeoutMs
419
+ headersJson:(NSString *)headersJson
294
420
  resolve:(RCTPromiseResolveBlock)resolve
295
421
  reject:(RCTPromiseRejectBlock)reject
296
422
  {
@@ -299,8 +425,18 @@ RCT_EXPORT_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve
299
425
  reject(@"ERR_DOWNLOAD", [NSString stringWithFormat:@"Invalid URL: %@", url], nil);
300
426
  return;
301
427
  }
428
+ id headers = [NSJSONSerialization JSONObjectWithData:[headersJson dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
429
+ if (![headers isKindOfClass:[NSDictionary class]]) {
430
+ reject(@"ERR_DOWNLOAD", [@"headersJson must be a JSON object, got: " stringByAppendingString:headersJson], nil);
431
+ return;
432
+ }
433
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:nsurl];
434
+ for (NSString *name in headers) {
435
+ [request setValue:[headers[name] description] forHTTPHeaderField:name];
436
+ }
302
437
  RNBsPatchDownload *download = [RNBsPatchDownload new];
303
438
  download.destPath = destPath;
439
+ download.resumePath = [destPath stringByAppendingString:@".resumedata"];
304
440
  download.resolve = resolve;
305
441
  download.reject = reject;
306
442
  __weak RNBsPatch *weakSelf = self;
@@ -312,7 +448,12 @@ RCT_EXPORT_METHOD(getAppVersion:(RCTPromiseResolveBlock)resolve
312
448
  config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
313
449
  config.timeoutIntervalForRequest = timeoutMs / 1000.0; // idle timeout, same as Android readTimeout
314
450
  NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:download delegateQueue:nil];
315
- [[session downloadTaskWithURL:nsurl] resume];
451
+ // Single use: a failed attempt writes fresh resume data, invalid data fails this
452
+ // attempt and the next one starts over
453
+ NSData *resumeData = [NSData dataWithContentsOfFile:download.resumePath];
454
+ [NSFileManager.defaultManager removeItemAtPath:download.resumePath error:nil];
455
+ NSURLSessionDownloadTask *task = resumeData ? [session downloadTaskWithResumeData:resumeData] : nil;
456
+ [task ?: [session downloadTaskWithRequest:request] resume];
316
457
  }
317
458
 
318
459
  - (void)hashFile:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
@@ -1 +1 @@
1
- {"version":3,"names":["_reactNative","require","_default","exports","default","TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeBsPatch.ts"],"mappings":";;;;;;AACA,IAAAA,YAAA,GAAAC,OAAA;AAAmD,IAAAC,QAAA,GAAAC,OAAA,CAAAC,OAAA,GA4FpCC,gCAAmB,CAACC,YAAY,CAAO,SAAS,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["_reactNative","require","_default","exports","default","TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeBsPatch.ts"],"mappings":";;;;;;AACA,IAAAA,YAAA,GAAAC,OAAA;AAAmD,IAAAC,QAAA,GAAAC,OAAA,CAAAC,OAAA,GA4HpCC,gCAAmB,CAACC,YAAY,CAAO,SAAS,CAAC","ignoreList":[]}
@@ -7,12 +7,19 @@ var _exportNames = {
7
7
  applyPatch: true,
8
8
  markAsHealthy: true,
9
9
  reloadBundle: true,
10
+ R2StorageProvider: true,
10
11
  GitHubRawStorageProvider: true
11
12
  };
12
13
  Object.defineProperty(exports, "GitHubRawStorageProvider", {
13
14
  enumerable: true,
14
15
  get: function () {
15
- return _GitHubRawStorage.GitHubRawStorageProvider;
16
+ return _R2Storage.R2StorageProvider;
17
+ }
18
+ });
19
+ Object.defineProperty(exports, "R2StorageProvider", {
20
+ enumerable: true,
21
+ get: function () {
22
+ return _R2Storage.R2StorageProvider;
16
23
  }
17
24
  });
18
25
  exports.applyPatch = applyPatch;
@@ -56,7 +63,7 @@ Object.keys(_types).forEach(function (key) {
56
63
  }
57
64
  });
58
65
  });
59
- var _GitHubRawStorage = require("./ota/storage/GitHubRawStorage");
66
+ var _R2Storage = require("./ota/storage/R2Storage");
60
67
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
61
68
  function applyPatch(oldBundlePath, newBundlePath, patchPath) {
62
69
  return _NativeBsPatch.default.applyPatch(oldBundlePath, newBundlePath, patchPath);
@@ -1 +1 @@
1
- {"version":3,"names":["_NativeBsPatch","_interopRequireDefault","require","_otaLogger","Object","keys","forEach","key","prototype","hasOwnProperty","call","_exportNames","exports","defineProperty","enumerable","get","_OtaProvider","_types","_GitHubRawStorage","e","__esModule","default","applyPatch","oldBundlePath","newBundlePath","patchPath","NativeBsPatch","markAsHealthy","reloadBundle","_default"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,cAAA,GAAAC,sBAAA,CAAAC,OAAA;AAkBA,IAAAC,UAAA,GAAAD,OAAA;AAAAE,MAAA,CAAAC,IAAA,CAAAF,UAAA,EAAAG,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAH,MAAA,CAAAI,SAAA,CAAAC,cAAA,CAAAC,IAAA,CAAAC,YAAA,EAAAJ,GAAA;EAAA,IAAAA,GAAA,IAAAK,OAAA,IAAAA,OAAA,CAAAL,GAAA,MAAAJ,UAAA,CAAAI,GAAA;EAAAH,MAAA,CAAAS,cAAA,CAAAD,OAAA,EAAAL,GAAA;IAAAO,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAZ,UAAA,CAAAI,GAAA;IAAA;EAAA;AAAA;AACA,IAAAS,YAAA,GAAAd,OAAA;AAAAE,MAAA,CAAAC,IAAA,CAAAW,YAAA,EAAAV,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAH,MAAA,CAAAI,SAAA,CAAAC,cAAA,CAAAC,IAAA,CAAAC,YAAA,EAAAJ,GAAA;EAAA,IAAAA,GAAA,IAAAK,OAAA,IAAAA,OAAA,CAAAL,GAAA,MAAAS,YAAA,CAAAT,GAAA;EAAAH,MAAA,CAAAS,cAAA,CAAAD,OAAA,EAAAL,GAAA;IAAAO,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAC,YAAA,CAAAT,GAAA;IAAA;EAAA;AAAA;AACA,IAAAU,MAAA,GAAAf,OAAA;AAAAE,MAAA,CAAAC,IAAA,CAAAY,MAAA,EAAAX,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAH,MAAA,CAAAI,SAAA,CAAAC,cAAA,CAAAC,IAAA,CAAAC,YAAA,EAAAJ,GAAA;EAAA,IAAAA,GAAA,IAAAK,OAAA,IAAAA,OAAA,CAAAL,GAAA,MAAAU,MAAA,CAAAV,GAAA;EAAAH,MAAA,CAAAS,cAAA,CAAAD,OAAA,EAAAL,GAAA;IAAAO,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAE,MAAA,CAAAV,GAAA;IAAA;EAAA;AAAA;AAEA,IAAAW,iBAAA,GAAAhB,OAAA;AAA0E,SAAAD,uBAAAkB,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AApBnE,SAASG,UAAUA,CACxBC,aAAqB,EACrBC,aAAqB,EACrBC,SAAiB,EACC;EAClB,OAAOC,sBAAa,CAACJ,UAAU,CAACC,aAAa,EAAEC,aAAa,EAAEC,SAAS,CAAC;AAC1E;AAEO,SAASE,aAAaA,CAAA,EAAS;EACpC,OAAOD,sBAAa,CAACC,aAAa,CAAC,CAAC;AACtC;AAEO,SAASC,YAAYA,CAAA,EAAS;EACnC,OAAOF,sBAAa,CAACE,YAAY,CAAC,CAAC;AACrC;AAAC,IAAAC,QAAA,GAAAjB,OAAA,CAAAS,OAAA,GAQc;EACbC,UAAU;EACVM,YAAY;EACZD;AACF,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["_NativeBsPatch","_interopRequireDefault","require","_otaLogger","Object","keys","forEach","key","prototype","hasOwnProperty","call","_exportNames","exports","defineProperty","enumerable","get","_OtaProvider","_types","_R2Storage","e","__esModule","default","applyPatch","oldBundlePath","newBundlePath","patchPath","NativeBsPatch","markAsHealthy","reloadBundle","_default"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,cAAA,GAAAC,sBAAA,CAAAC,OAAA;AAkBA,IAAAC,UAAA,GAAAD,OAAA;AAAAE,MAAA,CAAAC,IAAA,CAAAF,UAAA,EAAAG,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAH,MAAA,CAAAI,SAAA,CAAAC,cAAA,CAAAC,IAAA,CAAAC,YAAA,EAAAJ,GAAA;EAAA,IAAAA,GAAA,IAAAK,OAAA,IAAAA,OAAA,CAAAL,GAAA,MAAAJ,UAAA,CAAAI,GAAA;EAAAH,MAAA,CAAAS,cAAA,CAAAD,OAAA,EAAAL,GAAA;IAAAO,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAZ,UAAA,CAAAI,GAAA;IAAA;EAAA;AAAA;AACA,IAAAS,YAAA,GAAAd,OAAA;AAAAE,MAAA,CAAAC,IAAA,CAAAW,YAAA,EAAAV,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAH,MAAA,CAAAI,SAAA,CAAAC,cAAA,CAAAC,IAAA,CAAAC,YAAA,EAAAJ,GAAA;EAAA,IAAAA,GAAA,IAAAK,OAAA,IAAAA,OAAA,CAAAL,GAAA,MAAAS,YAAA,CAAAT,GAAA;EAAAH,MAAA,CAAAS,cAAA,CAAAD,OAAA,EAAAL,GAAA;IAAAO,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAC,YAAA,CAAAT,GAAA;IAAA;EAAA;AAAA;AACA,IAAAU,MAAA,GAAAf,OAAA;AAAAE,MAAA,CAAAC,IAAA,CAAAY,MAAA,EAAAX,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAH,MAAA,CAAAI,SAAA,CAAAC,cAAA,CAAAC,IAAA,CAAAC,YAAA,EAAAJ,GAAA;EAAA,IAAAA,GAAA,IAAAK,OAAA,IAAAA,OAAA,CAAAL,GAAA,MAAAU,MAAA,CAAAV,GAAA;EAAAH,MAAA,CAAAS,cAAA,CAAAD,OAAA,EAAAL,GAAA;IAAAO,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAE,MAAA,CAAAV,GAAA;IAAA;EAAA;AAAA;AAEA,IAAAW,UAAA,GAAAhB,OAAA;AAIiC,SAAAD,uBAAAkB,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAxB1B,SAASG,UAAUA,CACxBC,aAAqB,EACrBC,aAAqB,EACrBC,SAAiB,EACC;EAClB,OAAOC,sBAAa,CAACJ,UAAU,CAACC,aAAa,EAAEC,aAAa,EAAEC,SAAS,CAAC;AAC1E;AAEO,SAASE,aAAaA,CAAA,EAAS;EACpC,OAAOD,sBAAa,CAACC,aAAa,CAAC,CAAC;AACtC;AAEO,SAASC,YAAYA,CAAA,EAAS;EACnC,OAAOF,sBAAa,CAACE,YAAY,CAAC,CAAC;AACrC;AAAC,IAAAC,QAAA,GAAAjB,OAAA,CAAAS,OAAA,GAYc;EACbC,UAAU;EACVM,YAAY;EACZD;AACF,CAAC","ignoreList":[]}
@@ -8,6 +8,8 @@ exports.useOta = useOta;
8
8
  var _react = _interopRequireWildcard(require("react"));
9
9
  var _reactNative = require("react-native");
10
10
  var _NativeBsPatch = _interopRequireDefault(require("../NativeBsPatch"));
11
+ var _eligibility = require("./eligibility");
12
+ var _installId = require("./installId");
11
13
  var _otaCore = require("./otaCore");
12
14
  var _otaLogger = require("./otaLogger");
13
15
  var _otaMetadata = require("./otaMetadata");
@@ -16,6 +18,22 @@ var _jsxRuntime = require("react/jsx-runtime");
16
18
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
17
19
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
18
20
  const OtaContext = /*#__PURE__*/(0, _react.createContext)(undefined);
21
+ const DEFAULT_MIN_CHECK_INTERVAL_MS = 10 * 60 * 1000;
22
+
23
+ /** Sync native getter, '' if it throws */
24
+ const nativeString = get => {
25
+ try {
26
+ return get() ?? '';
27
+ } catch {
28
+ return '';
29
+ }
30
+ };
31
+
32
+ /** Bytecode version the running Hermes reads (undefined off Hermes) */
33
+ const hermesBytecodeVersion = () => {
34
+ const v = globalThis.HermesInternal?.getRuntimeProperties?.()?.['Bytecode Version'];
35
+ return v ? Number(v) : undefined;
36
+ };
19
37
  function OtaProvider({
20
38
  children,
21
39
  config,
@@ -33,13 +51,17 @@ function OtaProvider({
33
51
  const [patches, setPatches] = (0, _react.useState)([]);
34
52
  const [isInitialized, setIsInitialized] = (0, _react.useState)(false);
35
53
  const [isUpdateReady, setIsUpdateReady] = (0, _react.useState)(false);
54
+ const [isMandatory, setIsMandatory] = (0, _react.useState)(false);
55
+ const [installId, setInstallId] = (0, _react.useState)('');
36
56
  const appState = (0, _react.useRef)(_reactNative.AppState.currentState);
37
57
  const isCheckingRef = (0, _react.useRef)(false);
38
- const pendingReloadRef = (0, _react.useRef)(false);
39
58
  const bootstrapDone = (0, _react.useRef)(false);
40
59
  /** Version the native resolver loaded for this JS session */
41
60
  const runningVersionRef = (0, _react.useRef)(config.baseAppVersion);
42
61
  const progressRef = (0, _react.useRef)(0);
62
+ const lastCheckRef = (0, _react.useRef)(0);
63
+ const appVersionRef = (0, _react.useRef)(config.appVersion ?? '');
64
+ const installIdRef = (0, _react.useRef)('');
43
65
  const isDebugUpdateEnabled = !__DEV__ || config.allowDebugUpdates === true;
44
66
 
45
67
  // No fallbacks: a missing env value disables OTA instead of silently pointing elsewhere.
@@ -58,21 +80,53 @@ function OtaProvider({
58
80
  progressRef.current = nextProgress;
59
81
  setProgress(nextProgress);
60
82
  }, []);
83
+
84
+ /** config.onEvent, plus a fire-and-forget POST to config.telemetry.url */
85
+ const onEvent = (0, _react.useCallback)(entry => {
86
+ config.onEvent?.(entry);
87
+ const telemetry = config.telemetry;
88
+ if (!telemetry) return;
89
+ fetch(telemetry.url, {
90
+ method: 'POST',
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ ...telemetry.headers
94
+ },
95
+ body: JSON.stringify({
96
+ ...entry,
97
+ installId: installIdRef.current,
98
+ platform: _reactNative.Platform.OS,
99
+ appVersion: appVersionRef.current,
100
+ appBuild: nativeString(() => _NativeBsPatch.default.getAppBuild()),
101
+ runtimeFingerprint: nativeString(() => _NativeBsPatch.default.getRuntimeFingerprint()),
102
+ flavor: config.customServer?.channel,
103
+ otaVersion: runningVersionRef.current
104
+ })
105
+ }).catch(() => {});
106
+ }, [config]);
61
107
  const getEffectiveConfig = (0, _react.useCallback)(async () => {
62
- if (config.appVersion) return config;
108
+ // The keys `patch` embedded in the app (primary + backup), unless the app passes its own
109
+ const publicKey = config.publicKey || nativeString(() => _NativeBsPatch.default.getEmbeddedPublicKeys()) || undefined;
110
+ const base = {
111
+ ...config,
112
+ publicKey,
113
+ onEvent
114
+ };
115
+ if (config.appVersion) return base;
63
116
  try {
64
117
  const nativeVer = await _NativeBsPatch.default.getAppVersion();
65
118
  if (!nativeVer) {
66
119
  throw new Error('Native appVersion is empty');
67
120
  }
121
+ appVersionRef.current = nativeVer;
68
122
  return {
69
- ...config,
123
+ ...base,
70
124
  appVersion: nativeVer
71
125
  };
72
126
  } catch (e) {
73
127
  throw new Error(`[OtaProvider] Failed to auto-detect appVersion from native: ${e.message}. ` + `Please set versionName (Android) / CFBundleShortVersionString (iOS), or provide config.appVersion explicitly.`);
74
128
  }
75
- }, [config]);
129
+ }, [config, onEvent]);
76
130
 
77
131
  /**
78
132
  * Core apply update logic — separated so it can be mapped to button clicks
@@ -112,15 +166,16 @@ function OtaProvider({
112
166
  /**
113
167
  * Exposed method for manual trigger
114
168
  */
115
- const applyUpdate = (0, _react.useCallback)(async () => {
169
+ const applyVersion = (0, _react.useCallback)(async targetVersion => {
116
170
  if (!isDebugUpdateEnabled) {
117
171
  setPendingCount(0);
118
172
  setStatus(_types.OtaStatus.UP_TO_DATE);
119
173
  setMessage('OTA updates are disabled while running a debug build.');
120
174
  return;
121
175
  }
122
- await executeApplyUpdate(patches, currentVersion, latestVersion);
123
- }, [currentVersion, executeApplyUpdate, isDebugUpdateEnabled, latestVersion, patches]);
176
+ await executeApplyUpdate(patches, currentVersion, targetVersion);
177
+ }, [currentVersion, executeApplyUpdate, isDebugUpdateEnabled, patches]);
178
+ const applyUpdate = (0, _react.useCallback)(() => applyVersion(latestVersion), [applyVersion, latestVersion]);
124
179
  const downloadBackgroundUpdate = (0, _react.useCallback)(async (patchList, localV, targetV) => {
125
180
  const directPatch = (0, _otaCore.selectUpdate)(patchList, localV, targetV);
126
181
  if (!directPatch) return;
@@ -129,10 +184,15 @@ function OtaProvider({
129
184
  await (0, _otaCore.applyOtaPatchInternal)(directPatch, effectiveConfig, (s, msg, m) => {
130
185
  if (s === _types.OtaStatus.SUCCESS) {
131
186
  setIsUpdateReady(true);
132
- pendingReloadRef.current = true;
133
187
  setStatus(_types.OtaStatus.UP_TO_DATE);
134
188
  setMessage(`Update V${targetV} ready for next launch`);
135
189
  if (m) setMetrics(m);
190
+ if (directPatch.mandatory) {
191
+ setIsMandatory(true);
192
+ // Only in the foreground: a reload in the background is rolled back (see the
193
+ // AppState effect); the effect reloads once the app comes back
194
+ if (_reactNative.AppState.currentState === 'active') _NativeBsPatch.default.reloadBundle();
195
+ }
136
196
  }
137
197
  }, () => {},
138
198
  // silent: background doesn't spam progress
@@ -150,8 +210,9 @@ function OtaProvider({
150
210
  /**
151
211
  * Check for updates from the manifest.
152
212
  * Uses a ref to guard against concurrent calls (avoids stale closure).
213
+ * Automatic calls are throttled by minCheckIntervalMs; { force: true } always checks.
153
214
  */
154
- const checkUpdates = (0, _react.useCallback)(async () => {
215
+ const checkUpdates = (0, _react.useCallback)(async options => {
155
216
  if (isCheckingRef.current || configError) return;
156
217
  if (!isDebugUpdateEnabled) {
157
218
  setPendingCount(0);
@@ -160,6 +221,9 @@ function OtaProvider({
160
221
  setMessage('OTA updates are disabled while running a debug build.');
161
222
  return;
162
223
  }
224
+ const minInterval = config.minCheckIntervalMs ?? DEFAULT_MIN_CHECK_INTERVAL_MS;
225
+ if (!options?.force && Date.now() - lastCheckRef.current < minInterval) return;
226
+ lastCheckRef.current = Date.now();
163
227
  isCheckingRef.current = true;
164
228
  setIsChecking(true);
165
229
  setStatus(_types.OtaStatus.CHECKING);
@@ -171,10 +235,22 @@ function OtaProvider({
171
235
  const metadata = await (0, _otaMetadata.loadMetadata)(config.baseAppVersion);
172
236
  const localV = metadata.currentVersion;
173
237
 
174
- // The one platform filter (storage providers return every platform).
175
- // Also skips versions that crashed on boot and were rolled back natively.
176
- const failed = metadata.failedVersions ?? [];
177
- const platformPatches = remotePatches.filter(p => (!p.platform || p.platform.toLowerCase() === _reactNative.Platform.OS) && !failed.includes(p.toVersion));
238
+ // Storage providers return every platform: keep what this install may take (platform,
239
+ // native fingerprint, Hermes, rollout/QA lists, kill switch, versions that crashed here)
240
+ const context = {
241
+ platform: _reactNative.Platform.OS,
242
+ failedVersions: metadata.failedVersions ?? [],
243
+ runtimeFingerprint: nativeString(() => _NativeBsPatch.default.getRuntimeFingerprint()),
244
+ hermesBytecodeVersion: hermesBytecodeVersion(),
245
+ installId: await (0, _installId.getInstallId)()
246
+ };
247
+ const platformPatches = remotePatches.filter(p => {
248
+ const reason = (0, _eligibility.ineligibleReason)(p, context);
249
+ if (reason && __DEV__ && p.toVersion > localV) {
250
+ console.log(`[OtaProvider] Skipping V${p.toVersion} (${p.id}): ${reason}`);
251
+ }
252
+ return !reason;
253
+ });
178
254
  const maxV = platformPatches.length > 0 ? Math.max(...platformPatches.map(p => p.toVersion)) : localV;
179
255
  setPatches(platformPatches);
180
256
  setCurrentVersion(localV);
@@ -185,7 +261,10 @@ function OtaProvider({
185
261
  setStatus(_types.OtaStatus.UPDATE_AVAILABLE);
186
262
 
187
263
  // Trigger behaviors based on mode
188
- if (config.updateMode === _types.OtaUpdateMode.BACKGROUND_DOWNLOAD) {
264
+ const network = config.network?.wifiOnly ? await _NativeBsPatch.default.getNetworkType().catch(() => 'unknown') : 'wifi';
265
+ if (network !== 'wifi' && network !== 'ethernet') {
266
+ setMessage(`Update available: V${localV} → V${maxV} (waiting for Wi-Fi)`);
267
+ } else if (config.updateMode === _types.OtaUpdateMode.BACKGROUND_DOWNLOAD) {
189
268
  downloadBackgroundUpdate(platformPatches, localV, maxV);
190
269
  } else if (config.updateMode === _types.OtaUpdateMode.ON_APP_START || config.updateMode === _types.OtaUpdateMode.ON_APP_FOREGROUND) {
191
270
  // Auto-apply immediately!
@@ -218,6 +297,11 @@ function OtaProvider({
218
297
  */
219
298
  (0, _react.useEffect)(() => {
220
299
  const bootstrap = async () => {
300
+ (0, _installId.getInstallId)().then(id => {
301
+ installIdRef.current = id;
302
+ setInstallId(id);
303
+ }).catch(() => {});
304
+
221
305
  // Clean up any pending trash directories from previous resets (non-blocking)
222
306
  _NativeBsPatch.default.readDir(_otaMetadata.DOCUMENT_DIR).then(dirs => {
223
307
  for (const dir of dirs) {
@@ -250,27 +334,32 @@ function OtaProvider({
250
334
  setStatus(_types.OtaStatus.UP_TO_DATE);
251
335
  }
252
336
  };
253
- bootstrap().then(() => setIsInitialized(true)).catch(() => {
254
- // Bootstrap failed: keep UI blocked so user sees the error state.
255
- // setIsInitialized stays false → isBlocking remains true → the error
256
- // message is shown via loadingView instead of a silent blank screen.
257
- });
337
+
338
+ // OTA must never take the app down: if bootstrap fails, render the app with OTA idle
339
+ bootstrap().catch(error => {
340
+ console.warn('[OtaProvider] Bootstrap failed, OTA disabled for this session:', error);
341
+ setMessage(`OTA disabled: ${error.message}`);
342
+ setStatus(_types.OtaStatus.ERROR);
343
+ }).finally(() => setIsInitialized(true));
258
344
  // eslint-disable-next-line react-hooks/exhaustive-deps
259
345
  }, []);
260
346
 
261
347
  /**
262
- * AppState listener for ON_APP_FOREGROUND mode and silent background reload
348
+ * AppState listener for ON_APP_FOREGROUND mode, and for a mandatory update that got
349
+ * ready while in the background.
350
+ * A downloaded update is not reloaded on background: iOS suspends JS before the
351
+ * first render, so the native resolver counts the launch as failed and rolls it back.
352
+ * metadata already points at the new version, so the next cold start applies it
263
353
  */
354
+ const isMandatoryRef = (0, _react.useRef)(false);
355
+ isMandatoryRef.current = isMandatory && isUpdateReady;
264
356
  (0, _react.useEffect)(() => {
265
357
  const subscription = _reactNative.AppState.addEventListener('change', nextAppState => {
266
- // When going into background: if update is downloaded and ready, reload bundle silently
267
- // Only 'background', not 'inactive' (iOS fires it for Control Center, calls,
268
- // Face ID). Any previous state: iOS goes active → inactive → background.
269
- if (nextAppState === 'background' && pendingReloadRef.current) {
270
- pendingReloadRef.current = false;
271
- _NativeBsPatch.default.reloadBundle();
272
- }
273
358
  if (appState.current.match(/inactive|background/) && nextAppState === 'active' && bootstrapDone.current) {
359
+ if (isMandatoryRef.current) {
360
+ _NativeBsPatch.default.reloadBundle();
361
+ return;
362
+ }
274
363
  const mode = config.updateMode ?? _types.OtaUpdateMode.MANUAL;
275
364
  if (mode === _types.OtaUpdateMode.ON_APP_FOREGROUND) {
276
365
  checkUpdates();
@@ -283,13 +372,13 @@ function OtaProvider({
283
372
  };
284
373
  }, [config.updateMode, checkUpdates]);
285
374
  const markAsHealthy = (0, _react.useCallback)(async () => {
286
- const log = (0, _otaLogger.createOtaLogger)(config.onEvent);
375
+ const log = (0, _otaLogger.createOtaLogger)(onEvent);
287
376
  log.emit(_otaLogger.OtaEvent.MARK_HEALTHY);
288
377
  const appVersion = await _NativeBsPatch.default.getAppVersion().catch(() => config.appVersion);
289
378
  await (0, _otaMetadata.markHealthy)(config.baseAppVersion, runningVersionRef.current, appVersion);
290
- }, [config]);
379
+ }, [config, onEvent]);
291
380
  const resetToFactory = (0, _react.useCallback)(async () => {
292
- const log = (0, _otaLogger.createOtaLogger)(config.onEvent);
381
+ const log = (0, _otaLogger.createOtaLogger)(onEvent);
293
382
  try {
294
383
  setStatus(_types.OtaStatus.APPLYING);
295
384
  setMessage('Executing system wipe...');
@@ -305,7 +394,7 @@ function OtaProvider({
305
394
  setStatus(_types.OtaStatus.ERROR);
306
395
  setMessage(`Reset failed: ${error.message}`);
307
396
  }
308
- }, [config]);
397
+ }, [onEvent]);
309
398
 
310
399
  // Memoized: consumers re-render only when something they can see changes
311
400
  const value = (0, _react.useMemo)(() => ({
@@ -318,12 +407,15 @@ function OtaProvider({
318
407
  isChecking,
319
408
  metrics,
320
409
  isUpdateReady,
410
+ isMandatory,
411
+ installId,
321
412
  applyUpdate,
413
+ applyVersion,
322
414
  resetToFactory,
323
415
  markAsHealthy,
324
416
  checkUpdate: checkUpdates,
325
417
  reloadBundle: () => _NativeBsPatch.default.reloadBundle()
326
- }), [status, message, progress, currentVersion, latestVersion, pendingCount, isChecking, metrics, isUpdateReady, applyUpdate, resetToFactory, markAsHealthy, checkUpdates]);
418
+ }), [status, message, progress, currentVersion, latestVersion, pendingCount, isChecking, metrics, isUpdateReady, isMandatory, installId, applyUpdate, applyVersion, resetToFactory, markAsHealthy, checkUpdates]);
327
419
 
328
420
  // Logic to determine if we should block the UI
329
421
  const isBlocking = (0, _react.useMemo)(() => {