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.
- package/README.md +87 -30
- package/android/build.gradle +25 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +86 -6
- package/android/src/main/res/values/bspatch_public_key.xml +5 -0
- package/cpp/bsdiff/bspatch_wrapper.c +34 -0
- package/cpp/bsdiff/bspatch_wrapper.h +2 -0
- package/cpp/bspatch_jni.cpp +18 -0
- package/ios/RNBsPatch.mm +142 -1
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/index.js +9 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +124 -32
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/eligibility.js +40 -0
- package/lib/commonjs/ota/eligibility.js.map +1 -0
- package/lib/commonjs/ota/entrySignature.js +23 -0
- package/lib/commonjs/ota/entrySignature.js.map +1 -0
- package/lib/commonjs/ota/installId.js +27 -0
- package/lib/commonjs/ota/installId.js.map +1 -0
- package/lib/commonjs/ota/otaCore.js +96 -21
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/storage/{GitHubRawStorage.js → R2Storage.js} +20 -15
- package/lib/commonjs/ota/storage/R2Storage.js.map +1 -0
- package/lib/commonjs/ota/types.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +124 -32
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/eligibility.js +34 -0
- package/lib/module/ota/eligibility.js.map +1 -0
- package/lib/module/ota/entrySignature.js +17 -0
- package/lib/module/ota/entrySignature.js.map +1 -0
- package/lib/module/ota/installId.js +22 -0
- package/lib/module/ota/installId.js.map +1 -0
- package/lib/module/ota/otaCore.js +96 -21
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/storage/{GitHubRawStorage.js → R2Storage.js} +18 -14
- package/lib/module/ota/storage/R2Storage.js.map +1 -0
- package/lib/module/ota/types.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +28 -2
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/eligibility.d.ts +19 -0
- package/lib/typescript/ota/eligibility.d.ts.map +1 -0
- package/lib/typescript/ota/entrySignature.d.ts +9 -0
- package/lib/typescript/ota/entrySignature.d.ts.map +1 -0
- package/lib/typescript/ota/installId.d.ts +3 -0
- package/lib/typescript/ota/installId.d.ts.map +1 -0
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/storage/{GitHubRawStorage.d.ts → R2Storage.d.ts} +4 -3
- package/lib/typescript/ota/storage/R2Storage.d.ts.map +1 -0
- package/lib/typescript/ota/types.d.ts +54 -5
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/react-native-bspatch.podspec +3 -1
- package/src/NativeBsPatch.ts +34 -2
- package/src/index.ts +5 -1
- package/src/ota/OtaProvider.tsx +128 -43
- package/src/ota/eligibility.ts +48 -0
- package/src/ota/entrySignature.ts +31 -0
- package/src/ota/installId.ts +18 -0
- package/src/ota/otaCore.ts +102 -21
- package/src/ota/storage/{GitHubRawStorage.ts → R2Storage.ts} +17 -13
- package/src/ota/types.ts +51 -5
- package/tools/build_ota.sh +343 -116
- package/tools/doctor.js +130 -0
- package/tools/ensure_signing_key.js +216 -0
- package/tools/fingerprint.js +111 -0
- package/tools/ios_fingerprint.sh +9 -0
- package/tools/ota_admin.js +131 -0
- package/tools/setup_project.js +41 -15
- package/tools/update_registry.js +111 -11
- package/tools/upload_ota.sh +85 -184
- package/tools/verify_publish.js +81 -0
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +0 -1
- package/lib/module/ota/storage/GitHubRawStorage.js.map +0 -1
- 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
|
-
|
|
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,
|
|
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":[]}
|
package/lib/commonjs/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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","
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
|
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,
|
|
123
|
-
}, [currentVersion, executeApplyUpdate, isDebugUpdateEnabled,
|
|
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
|
-
//
|
|
175
|
-
//
|
|
176
|
-
const
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
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)(
|
|
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)(
|
|
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
|
-
}, [
|
|
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)(() => {
|