react-native-bspatch 4.1.0 → 5.0.1

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 +84 -27
  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 +165 -35
  14. package/lib/commonjs/ota/OtaProvider.js.map +1 -1
  15. package/lib/commonjs/ota/eligibility.js +47 -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 +165 -35
  30. package/lib/module/ota/OtaProvider.js.map +1 -1
  31. package/lib/module/ota/eligibility.js +41 -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 +25 -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 +58 -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 +167 -42
  63. package/src/ota/eligibility.ts +57 -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 +55 -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,12 +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
58
  const bootstrapDone = (0, _react.useRef)(false);
39
59
  /** Version the native resolver loaded for this JS session */
40
60
  const runningVersionRef = (0, _react.useRef)(config.baseAppVersion);
41
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)('');
42
65
  const isDebugUpdateEnabled = !__DEV__ || config.allowDebugUpdates === true;
43
66
 
44
67
  // No fallbacks: a missing env value disables OTA instead of silently pointing elsewhere.
@@ -57,36 +80,68 @@ function OtaProvider({
57
80
  progressRef.current = nextProgress;
58
81
  setProgress(nextProgress);
59
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]);
60
107
  const getEffectiveConfig = (0, _react.useCallback)(async () => {
61
- 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;
62
116
  try {
63
117
  const nativeVer = await _NativeBsPatch.default.getAppVersion();
64
118
  if (!nativeVer) {
65
119
  throw new Error('Native appVersion is empty');
66
120
  }
121
+ appVersionRef.current = nativeVer;
67
122
  return {
68
- ...config,
123
+ ...base,
69
124
  appVersion: nativeVer
70
125
  };
71
126
  } catch (e) {
72
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.`);
73
128
  }
74
- }, [config]);
129
+ }, [config, onEvent]);
75
130
 
76
- /**
77
- * Core apply update logic — separated so it can be mapped to button clicks
78
- * OR called automatically by checkUpdates()
79
- */
80
- const executeApplyUpdate = (0, _react.useCallback)(async (patchList, currentVer, targetVer) => {
81
- const directPatch = (0, _otaCore.selectUpdate)(patchList, currentVer, targetVer);
82
- if (!directPatch) {
83
- if (__DEV__) {
84
- console.warn('[OtaProvider] No update entry found for', currentVer, '->', targetVer);
85
- }
86
- setStatus(_types.OtaStatus.ERROR);
87
- setMessage('No upgrade path found.');
88
- return;
89
- }
131
+ /** What decides which registry entries this install may take */
132
+ const eligibilityContext = (0, _react.useCallback)(async () => {
133
+ const metadata = await (0, _otaMetadata.loadMetadata)(config.baseAppVersion);
134
+ return {
135
+ platform: _reactNative.Platform.OS,
136
+ failedVersions: metadata.failedVersions ?? [],
137
+ runtimeFingerprint: nativeString(() => _NativeBsPatch.default.getRuntimeFingerprint()),
138
+ hermesBytecodeVersion: hermesBytecodeVersion(),
139
+ installId: await (0, _installId.getInstallId)()
140
+ };
141
+ }, [config.baseAppVersion]);
142
+
143
+ /** Installs one registry entry in the foreground (status + progress), then reloads */
144
+ const installEntry = (0, _react.useCallback)(async directPatch => {
90
145
  try {
91
146
  const effectiveConfig = await getEffectiveConfig();
92
147
  setStatus(_types.OtaStatus.DOWNLOADING);
@@ -108,6 +163,45 @@ function OtaProvider({
108
163
  }
109
164
  }, [getEffectiveConfig, setProgressIfChanged]);
110
165
 
166
+ /**
167
+ * Core apply update logic — separated so it can be mapped to button clicks
168
+ * OR called automatically by checkUpdates()
169
+ */
170
+ const executeApplyUpdate = (0, _react.useCallback)(async (patchList, currentVer, targetVer) => {
171
+ const directPatch = (0, _otaCore.selectUpdate)(patchList, currentVer, targetVer);
172
+ if (!directPatch) {
173
+ if (__DEV__) {
174
+ console.warn('[OtaProvider] No update entry found for', currentVer, '->', targetVer);
175
+ }
176
+ setStatus(_types.OtaStatus.ERROR);
177
+ setMessage('No upgrade path found.');
178
+ return;
179
+ }
180
+ await installEntry(directPatch);
181
+ }, [installEntry]);
182
+
183
+ /**
184
+ * Installs exactly this registry entry (a version list in a debug/QA screen): the patch
185
+ * when it starts from the running version, else its full bundle. Ignores the kill switch,
186
+ * rollout and QA list; still refuses what would break the app (native code, Hermes, …).
187
+ */
188
+ const applyEntry = (0, _react.useCallback)(async entry => {
189
+ if (!isDebugUpdateEnabled) {
190
+ setStatus(_types.OtaStatus.UP_TO_DATE);
191
+ setMessage('OTA updates are disabled while running a debug build.');
192
+ return;
193
+ }
194
+ const reason = (0, _eligibility.ineligibleReason)(entry, await eligibilityContext(), {
195
+ ignoreControls: true
196
+ });
197
+ if (reason) {
198
+ setStatus(_types.OtaStatus.ERROR);
199
+ setMessage(`Can't install V${entry.toVersion}: ${reason}`);
200
+ return;
201
+ }
202
+ await installEntry(entry);
203
+ }, [eligibilityContext, installEntry, isDebugUpdateEnabled]);
204
+
111
205
  /**
112
206
  * Exposed method for manual trigger
113
207
  */
@@ -132,6 +226,12 @@ function OtaProvider({
132
226
  setStatus(_types.OtaStatus.UP_TO_DATE);
133
227
  setMessage(`Update V${targetV} ready for next launch`);
134
228
  if (m) setMetrics(m);
229
+ if (directPatch.mandatory) {
230
+ setIsMandatory(true);
231
+ // Only in the foreground: a reload in the background is rolled back (see the
232
+ // AppState effect); the effect reloads once the app comes back
233
+ if (_reactNative.AppState.currentState === 'active') _NativeBsPatch.default.reloadBundle();
234
+ }
135
235
  }
136
236
  }, () => {},
137
237
  // silent: background doesn't spam progress
@@ -149,8 +249,9 @@ function OtaProvider({
149
249
  /**
150
250
  * Check for updates from the manifest.
151
251
  * Uses a ref to guard against concurrent calls (avoids stale closure).
252
+ * Automatic calls are throttled by minCheckIntervalMs; { force: true } always checks.
152
253
  */
153
- const checkUpdates = (0, _react.useCallback)(async () => {
254
+ const checkUpdates = (0, _react.useCallback)(async options => {
154
255
  if (isCheckingRef.current || configError) return;
155
256
  if (!isDebugUpdateEnabled) {
156
257
  setPendingCount(0);
@@ -159,6 +260,9 @@ function OtaProvider({
159
260
  setMessage('OTA updates are disabled while running a debug build.');
160
261
  return;
161
262
  }
263
+ const minInterval = config.minCheckIntervalMs ?? DEFAULT_MIN_CHECK_INTERVAL_MS;
264
+ if (!options?.force && Date.now() - lastCheckRef.current < minInterval) return;
265
+ lastCheckRef.current = Date.now();
162
266
  isCheckingRef.current = true;
163
267
  setIsChecking(true);
164
268
  setStatus(_types.OtaStatus.CHECKING);
@@ -170,10 +274,16 @@ function OtaProvider({
170
274
  const metadata = await (0, _otaMetadata.loadMetadata)(config.baseAppVersion);
171
275
  const localV = metadata.currentVersion;
172
276
 
173
- // The one platform filter (storage providers return every platform).
174
- // Also skips versions that crashed on boot and were rolled back natively.
175
- const failed = metadata.failedVersions ?? [];
176
- const platformPatches = remotePatches.filter(p => (!p.platform || p.platform.toLowerCase() === _reactNative.Platform.OS) && !failed.includes(p.toVersion));
277
+ // Storage providers return every platform: keep what this install may take (platform,
278
+ // native fingerprint, Hermes, rollout/QA lists, kill switch, versions that crashed here)
279
+ const context = await eligibilityContext();
280
+ const platformPatches = remotePatches.filter(p => {
281
+ const reason = (0, _eligibility.ineligibleReason)(p, context);
282
+ if (reason && __DEV__ && p.toVersion > localV) {
283
+ console.log(`[OtaProvider] Skipping V${p.toVersion} (${p.id}): ${reason}`);
284
+ }
285
+ return !reason;
286
+ });
177
287
  const maxV = platformPatches.length > 0 ? Math.max(...platformPatches.map(p => p.toVersion)) : localV;
178
288
  setPatches(platformPatches);
179
289
  setCurrentVersion(localV);
@@ -184,7 +294,10 @@ function OtaProvider({
184
294
  setStatus(_types.OtaStatus.UPDATE_AVAILABLE);
185
295
 
186
296
  // Trigger behaviors based on mode
187
- if (config.updateMode === _types.OtaUpdateMode.BACKGROUND_DOWNLOAD) {
297
+ const network = config.network?.wifiOnly ? await _NativeBsPatch.default.getNetworkType().catch(() => 'unknown') : 'wifi';
298
+ if (network !== 'wifi' && network !== 'ethernet') {
299
+ setMessage(`Update available: V${localV} → V${maxV} (waiting for Wi-Fi)`);
300
+ } else if (config.updateMode === _types.OtaUpdateMode.BACKGROUND_DOWNLOAD) {
188
301
  downloadBackgroundUpdate(platformPatches, localV, maxV);
189
302
  } else if (config.updateMode === _types.OtaUpdateMode.ON_APP_START || config.updateMode === _types.OtaUpdateMode.ON_APP_FOREGROUND) {
190
303
  // Auto-apply immediately!
@@ -202,7 +315,7 @@ function OtaProvider({
202
315
  isCheckingRef.current = false;
203
316
  setIsChecking(false);
204
317
  }
205
- }, [config, configError, executeApplyUpdate, isDebugUpdateEnabled, downloadBackgroundUpdate, getEffectiveConfig]);
318
+ }, [config, configError, eligibilityContext, executeApplyUpdate, isDebugUpdateEnabled, downloadBackgroundUpdate, getEffectiveConfig]);
206
319
 
207
320
  /**
208
321
  * Bootstrap: runs once on mount.
@@ -217,6 +330,11 @@ function OtaProvider({
217
330
  */
218
331
  (0, _react.useEffect)(() => {
219
332
  const bootstrap = async () => {
333
+ (0, _installId.getInstallId)().then(id => {
334
+ installIdRef.current = id;
335
+ setInstallId(id);
336
+ }).catch(() => {});
337
+
220
338
  // Clean up any pending trash directories from previous resets (non-blocking)
221
339
  _NativeBsPatch.default.readDir(_otaMetadata.DOCUMENT_DIR).then(dirs => {
222
340
  for (const dir of dirs) {
@@ -249,23 +367,32 @@ function OtaProvider({
249
367
  setStatus(_types.OtaStatus.UP_TO_DATE);
250
368
  }
251
369
  };
252
- bootstrap().then(() => setIsInitialized(true)).catch(() => {
253
- // Bootstrap failed: keep UI blocked so user sees the error state.
254
- // setIsInitialized stays false → isBlocking remains true → the error
255
- // message is shown via loadingView instead of a silent blank screen.
256
- });
370
+
371
+ // OTA must never take the app down: if bootstrap fails, render the app with OTA idle
372
+ bootstrap().catch(error => {
373
+ console.warn('[OtaProvider] Bootstrap failed, OTA disabled for this session:', error);
374
+ setMessage(`OTA disabled: ${error.message}`);
375
+ setStatus(_types.OtaStatus.ERROR);
376
+ }).finally(() => setIsInitialized(true));
257
377
  // eslint-disable-next-line react-hooks/exhaustive-deps
258
378
  }, []);
259
379
 
260
380
  /**
261
- * AppState listener for ON_APP_FOREGROUND mode.
381
+ * AppState listener for ON_APP_FOREGROUND mode, and for a mandatory update that got
382
+ * ready while in the background.
262
383
  * A downloaded update is not reloaded on background: iOS suspends JS before the
263
384
  * first render, so the native resolver counts the launch as failed and rolls it back.
264
385
  * metadata already points at the new version, so the next cold start applies it
265
386
  */
387
+ const isMandatoryRef = (0, _react.useRef)(false);
388
+ isMandatoryRef.current = isMandatory && isUpdateReady;
266
389
  (0, _react.useEffect)(() => {
267
390
  const subscription = _reactNative.AppState.addEventListener('change', nextAppState => {
268
391
  if (appState.current.match(/inactive|background/) && nextAppState === 'active' && bootstrapDone.current) {
392
+ if (isMandatoryRef.current) {
393
+ _NativeBsPatch.default.reloadBundle();
394
+ return;
395
+ }
269
396
  const mode = config.updateMode ?? _types.OtaUpdateMode.MANUAL;
270
397
  if (mode === _types.OtaUpdateMode.ON_APP_FOREGROUND) {
271
398
  checkUpdates();
@@ -278,13 +405,13 @@ function OtaProvider({
278
405
  };
279
406
  }, [config.updateMode, checkUpdates]);
280
407
  const markAsHealthy = (0, _react.useCallback)(async () => {
281
- const log = (0, _otaLogger.createOtaLogger)(config.onEvent);
408
+ const log = (0, _otaLogger.createOtaLogger)(onEvent);
282
409
  log.emit(_otaLogger.OtaEvent.MARK_HEALTHY);
283
410
  const appVersion = await _NativeBsPatch.default.getAppVersion().catch(() => config.appVersion);
284
411
  await (0, _otaMetadata.markHealthy)(config.baseAppVersion, runningVersionRef.current, appVersion);
285
- }, [config]);
412
+ }, [config, onEvent]);
286
413
  const resetToFactory = (0, _react.useCallback)(async () => {
287
- const log = (0, _otaLogger.createOtaLogger)(config.onEvent);
414
+ const log = (0, _otaLogger.createOtaLogger)(onEvent);
288
415
  try {
289
416
  setStatus(_types.OtaStatus.APPLYING);
290
417
  setMessage('Executing system wipe...');
@@ -300,7 +427,7 @@ function OtaProvider({
300
427
  setStatus(_types.OtaStatus.ERROR);
301
428
  setMessage(`Reset failed: ${error.message}`);
302
429
  }
303
- }, [config]);
430
+ }, [onEvent]);
304
431
 
305
432
  // Memoized: consumers re-render only when something they can see changes
306
433
  const value = (0, _react.useMemo)(() => ({
@@ -313,13 +440,16 @@ function OtaProvider({
313
440
  isChecking,
314
441
  metrics,
315
442
  isUpdateReady,
443
+ isMandatory,
444
+ installId,
316
445
  applyUpdate,
317
446
  applyVersion,
447
+ applyEntry,
318
448
  resetToFactory,
319
449
  markAsHealthy,
320
450
  checkUpdate: checkUpdates,
321
451
  reloadBundle: () => _NativeBsPatch.default.reloadBundle()
322
- }), [status, message, progress, currentVersion, latestVersion, pendingCount, isChecking, metrics, isUpdateReady, applyUpdate, applyVersion, resetToFactory, markAsHealthy, checkUpdates]);
452
+ }), [status, message, progress, currentVersion, latestVersion, pendingCount, isChecking, metrics, isUpdateReady, isMandatory, installId, applyUpdate, applyVersion, applyEntry, resetToFactory, markAsHealthy, checkUpdates]);
323
453
 
324
454
  // Logic to determine if we should block the UI
325
455
  const isBlocking = (0, _react.useMemo)(() => {