@hot-updater/react-native 1.0.5 → 1.0.7

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 (73) hide show
  1. package/HotUpdater.podspec +24 -3
  2. package/Package.swift +100 -0
  3. package/android/build.gradle +3 -4
  4. package/android/hot-updater-builtin-assets.gradle +67 -0
  5. package/android/hot-updater-min-bundle-id.gradle +69 -0
  6. package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +85 -0
  7. package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +116 -11
  8. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +117 -12
  9. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +5 -52
  10. package/android/src/newarch/HotUpdaterModule.kt +129 -34
  11. package/android/src/oldarch/HotUpdaterModule.kt +130 -34
  12. package/android/src/oldarch/HotUpdaterSpec.kt +5 -0
  13. package/ios/HotUpdater/Interface/HotUpdaterInterface.h +69 -0
  14. package/ios/HotUpdater/Interface/HotUpdaterInterface.m +3 -0
  15. package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +87 -0
  16. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +88 -13
  17. package/ios/HotUpdater/Internal/HotUpdater.mm +90 -90
  18. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +154 -52
  19. package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +26 -0
  20. package/ios/HotUpdater/Internal/HotUpdaterMinBundleId.m +10 -0
  21. package/ios/HotUpdater/Package.swift +88 -0
  22. package/ios/Plugins/HotUpdaterMinBundleIdPlugin/HotUpdaterMinBundleIdPlugin.swift +28 -0
  23. package/ios/scripts/generate-min-bundle-id.zsh +49 -0
  24. package/lib/commonjs/checkForUpdate.js +17 -2
  25. package/lib/commonjs/checkForUpdate.js.map +1 -1
  26. package/lib/commonjs/fetchUpdateResponse.js +5 -2
  27. package/lib/commonjs/fetchUpdateResponse.js.map +1 -1
  28. package/lib/commonjs/httpClient.js +58 -5
  29. package/lib/commonjs/httpClient.js.map +1 -1
  30. package/lib/commonjs/native.js +42 -7
  31. package/lib/commonjs/native.js.map +1 -1
  32. package/lib/commonjs/sdkVersion.js +1 -1
  33. package/lib/commonjs/specs/NativeHotUpdater.js +5 -0
  34. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  35. package/lib/module/checkForUpdate.js +18 -3
  36. package/lib/module/checkForUpdate.js.map +1 -1
  37. package/lib/module/fetchUpdateResponse.js +4 -1
  38. package/lib/module/fetchUpdateResponse.js.map +1 -1
  39. package/lib/module/httpClient.js +61 -7
  40. package/lib/module/httpClient.js.map +1 -1
  41. package/lib/module/native.js +41 -7
  42. package/lib/module/native.js.map +1 -1
  43. package/lib/module/sdkVersion.js +1 -1
  44. package/lib/module/specs/NativeHotUpdater.js +7 -0
  45. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  46. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts +2 -0
  48. package/lib/typescript/commonjs/fetchUpdateResponse.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/httpClient.d.ts +26 -2
  50. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/native.d.ts +17 -3
  52. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  54. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +37 -1
  55. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  56. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  57. package/lib/typescript/module/fetchUpdateResponse.d.ts +2 -0
  58. package/lib/typescript/module/fetchUpdateResponse.d.ts.map +1 -1
  59. package/lib/typescript/module/httpClient.d.ts +26 -2
  60. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  61. package/lib/typescript/module/native.d.ts +17 -3
  62. package/lib/typescript/module/native.d.ts.map +1 -1
  63. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  64. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +37 -1
  65. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  66. package/package.json +5 -4
  67. package/react-native.config.js +7 -0
  68. package/src/checkForUpdate.ts +19 -3
  69. package/src/fetchUpdateResponse.ts +4 -1
  70. package/src/httpClient.ts +88 -9
  71. package/src/native.ts +53 -9
  72. package/src/sdkVersion.ts +1 -1
  73. package/src/specs/NativeHotUpdater.ts +39 -1
@@ -9,17 +9,41 @@
9
9
  #include <signal.h>
10
10
  #include <unistd.h>
11
11
 
12
+ #if defined(SWIFT_PACKAGE)
13
+ // Swift Package Manager: the Swift classes, through the protocols they conform
14
+ // to (see HotUpdaterInterface.h).
15
+ #import "HotUpdaterInterface.h"
16
+ typedef NSObject<HotUpdaterImplInterface> HotUpdaterImpl;
17
+ typedef NSObject<HotUpdaterConfigInterface> HotUpdaterConfig;
18
+ #else
12
19
  #if __has_include("HotUpdater/HotUpdater-Swift.h")
13
20
  #import "HotUpdater/HotUpdater-Swift.h"
14
21
  #else
15
22
  #import "HotUpdater-Swift.h"
16
23
  #endif
17
24
 
25
+ static HotUpdaterImpl *HotUpdaterMakeImpl(void)
26
+ {
27
+ return [[HotUpdaterImpl alloc] init];
28
+ }
29
+
30
+ static NSString *HotUpdaterAppVersion(void)
31
+ {
32
+ return [HotUpdaterImpl appVersion];
33
+ }
34
+
35
+ static HotUpdaterConfig *HotUpdaterSharedConfig(void)
36
+ {
37
+ return [HotUpdaterConfig shared];
38
+ }
39
+ #endif
40
+
18
41
  @interface HotUpdater (InternalSharedImpl)
19
42
  + (HotUpdaterImpl *)sharedImpl;
20
- + (NSString *)generateUUIDv7FromTimestamp:(uint64_t)timestampMs;
21
43
  @end
22
44
 
45
+ extern "C" NSString *HotUpdaterBuildMinBundleId(void);
46
+
23
47
  namespace {
24
48
  constexpr size_t kHotUpdaterMaxBundleIdLength = 128;
25
49
  const int kHotUpdaterSignals[] = {SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP};
@@ -207,61 +231,11 @@ extern "C" BOOL HotUpdaterPerformRecoveryReload(void)
207
231
 
208
232
  extern "C" NSString *HotUpdaterGetMinBundleId(void)
209
233
  {
210
- static NSString *uuid = nil;
211
- static dispatch_once_t onceToken;
212
- dispatch_once(&onceToken, ^{
213
234
  #if DEBUG
214
- uuid = @"00000000-0000-0000-0000-000000000000";
235
+ return @"00000000-0000-0000-0000-000000000000";
215
236
  #else
216
- // Step 1: Use the HOT_UPDATER_MIN_BUNDLE_ID build setting, which the
217
- // hot-updater CLI passes to xcodebuild and the Info.plist slot it adds
218
- // (`$(HOT_UPDATER_MIN_BUNDLE_ID)`) carries into the app, as Android
219
- // takes -PMIN_BUNDLE_ID.
220
- NSString *customValue = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"HOT_UPDATER_MIN_BUNDLE_ID"];
221
- if ([customValue isKindOfClass:[NSString class]]) {
222
- NSString *trimmed = [customValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
223
- // An unset build setting leaves the slot empty or unexpanded; anything
224
- // but a UUID falls back to the compile time below.
225
- NSUUID *parsed = trimmed.length > 0 ? [[NSUUID alloc] initWithUUIDString:trimmed] : nil;
226
- if (parsed) {
227
- uuid = parsed.UUIDString.lowercaseString;
228
- RCTLogInfo(@"[HotUpdater.mm] Using MIN_BUNDLE_ID from the HOT_UPDATER_MIN_BUNDLE_ID build setting: %@", uuid);
229
- return;
230
- }
231
- if (trimmed.length > 0 && ![trimmed isEqualToString:@"$(HOT_UPDATER_MIN_BUNDLE_ID)"]) {
232
- RCTLogWarn(@"[HotUpdater.mm] Ignoring HOT_UPDATER_MIN_BUNDLE_ID %@: not a UUID", trimmed);
233
- }
234
- }
235
-
236
- // Step 2: Fall back to the compile time (26-hour subtraction)
237
- RCTLogInfo(@"[HotUpdater.mm] No HOT_UPDATER_MIN_BUNDLE_ID build setting, using the compile time");
238
-
239
- NSString *compileDateStr = [NSString stringWithFormat:@"%s %s", __DATE__, __TIME__];
240
- NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
241
-
242
- // Parse __DATE__ __TIME__ as UTC to ensure consistent timezone handling across all build environments
243
- [formatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
244
- [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]];
245
- [formatter setDateFormat:@"MMM d yyyy HH:mm:ss"];
246
- NSDate *buildDate = [formatter dateFromString:compileDateStr];
247
- if (!buildDate) {
248
- RCTLogWarn(@"[HotUpdater.mm] Could not parse build date: %@", compileDateStr);
249
- uuid = @"00000000-0000-0000-0000-000000000000";
250
- return;
251
- }
252
-
253
- // Subtract 26 hours (93600 seconds) to ensure MIN_BUNDLE_ID is always in the past
254
- // This guarantees that uuidv7-based bundleIds (generated at runtime) will always be newer than MIN_BUNDLE_ID
255
- // Why 26 hours? Global timezone range spans from UTC-12 to UTC+14 (total 26 hours)
256
- // By subtracting 26 hours, MIN_BUNDLE_ID becomes a safe "past timestamp" regardless of build timezone
257
- // Example: Build at 15:00 in any timezone -> parse as 15:00 UTC -> subtract 26h -> 13:00 UTC (previous day)
258
- NSTimeInterval adjustedTimestamp = [buildDate timeIntervalSince1970] - 93600.0;
259
- uint64_t buildTimestampMs = (uint64_t)(adjustedTimestamp * 1000.0);
260
-
261
- uuid = [HotUpdater generateUUIDv7FromTimestamp:buildTimestampMs];
237
+ return HotUpdaterBuildMinBundleId();
262
238
  #endif
263
- });
264
- return uuid;
265
239
  }
266
240
 
267
241
  extern "C" void *HotUpdaterCopyMinBundleId(void)
@@ -405,54 +379,22 @@ RCT_EXPORT_MODULE();
405
379
  static HotUpdaterImpl *_sharedImpl = nil;
406
380
  static dispatch_once_t onceToken;
407
381
  dispatch_once(&onceToken, ^{
408
- _sharedImpl = [[HotUpdaterImpl alloc] init];
382
+ _sharedImpl = HotUpdaterMakeImpl();
409
383
  });
410
384
  return _sharedImpl;
411
385
  }
412
386
 
413
387
  #pragma mark - React Native Constants (Keep getMinBundleId, delegate others)
414
388
 
415
- // Returns the minimum bundle ID string, either from Info.plist or generated from build timestamp
389
+ // Returns the bundle ID of the bundle this build ships (MIN_BUNDLE_ID)
416
390
  - (NSString *)getMinBundleId {
417
391
  return HotUpdaterGetMinBundleId();
418
392
  }
419
393
 
420
- // Helper method: Generate UUID v7 from timestamp (milliseconds)
421
- + (NSString *)generateUUIDv7FromTimestamp:(uint64_t)timestampMs {
422
- unsigned char bytes[16];
423
-
424
- // UUID v7 format: timestamp_ms (48 bits) + ver (4 bits) + random (12 bits) + variant (2 bits) + random (62 bits)
425
- bytes[0] = (timestampMs >> 40) & 0xFF;
426
- bytes[1] = (timestampMs >> 32) & 0xFF;
427
- bytes[2] = (timestampMs >> 24) & 0xFF;
428
- bytes[3] = (timestampMs >> 16) & 0xFF;
429
- bytes[4] = (timestampMs >> 8) & 0xFF;
430
- bytes[5] = timestampMs & 0xFF;
431
-
432
- // Version 7
433
- bytes[6] = 0x70;
434
- bytes[7] = 0x00;
435
-
436
- // Variant bits (10xxxxxx)
437
- bytes[8] = 0x80;
438
- bytes[9] = 0x00;
439
-
440
- // Remaining bytes (zeros for deterministic MIN_BUNDLE_ID)
441
- for (int i = 10; i < 16; i++) {
442
- bytes[i] = 0x00;
443
- }
444
-
445
- return [NSString stringWithFormat:
446
- @"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
447
- bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
448
- bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]];
449
- }
450
-
451
-
452
394
  - (NSDictionary *)_buildConstantsDictionary {
453
395
  return @{
454
396
  @"MIN_BUNDLE_ID": [self getMinBundleId] ?: [NSNull null],
455
- @"APP_VERSION": [HotUpdaterImpl appVersion] ?: [NSNull null],
397
+ @"APP_VERSION": HotUpdaterAppVersion() ?: [NSNull null],
456
398
  @"CHANNEL": [[HotUpdater sharedImpl] getChannel] ?: [NSNull null],
457
399
  @"DEFAULT_CHANNEL": [[HotUpdater sharedImpl] getDefaultChannel] ?: [NSNull null],
458
400
  @"FINGERPRINT_HASH": [[HotUpdater sharedImpl] getFingerprintHash] ?: [NSNull null]
@@ -484,7 +426,7 @@ RCT_EXPORT_MODULE();
484
426
  publicKey:(NSString *)publicKey
485
427
  channel:(NSString *)channel
486
428
  isolationKey:(NSString *)isolationKey {
487
- HotUpdaterConfig *config = [HotUpdaterConfig shared];
429
+ HotUpdaterConfig *config = HotUpdaterSharedConfig();
488
430
  [config configureWithFingerprintHash:fingerprintHash publicKey:publicKey channel:channel isolationKey:isolationKey];
489
431
  }
490
432
 
@@ -590,11 +532,61 @@ RCT_EXPORT_MODULE();
590
532
  if (params.selection()) {
591
533
  paramDict[@"selection"] = params.selection();
592
534
  }
535
+ // Legacy: what JavaScript from the first artifact protocol v2 SDK sends.
536
+ if (params.filesUrl()) {
537
+ paramDict[@"filesUrl"] = params.filesUrl();
538
+ }
539
+ if (params.requestHeaders()) {
540
+ paramDict[@"requestHeaders"] = params.requestHeaders();
541
+ }
542
+ if (params.requestTimeout().has_value()) {
543
+ paramDict[@"requestTimeout"] = @(params.requestTimeout().value());
544
+ }
593
545
 
594
546
  HotUpdaterImpl *impl = [HotUpdater sharedImpl];
595
547
  [impl updateBundle:paramDict resolver:resolve rejecter:reject];
596
548
  }
597
549
 
550
+ - (void)installArtifact:(JS::NativeHotUpdater::InstallArtifactParams &)params
551
+ resolve:(RCTPromiseResolveBlock)resolve
552
+ reject:(RCTPromiseRejectBlock)reject {
553
+ NSLog(@"[HotUpdater.mm] installArtifact called.");
554
+ NSMutableDictionary *paramDict = [NSMutableDictionary dictionary];
555
+ if (params.bundleId()) {
556
+ paramDict[@"bundleId"] = params.bundleId();
557
+ }
558
+ if (params.manifestUrl()) {
559
+ paramDict[@"manifestUrl"] = params.manifestUrl();
560
+ }
561
+ if (params.manifestFileHash()) {
562
+ paramDict[@"manifestFileHash"] = params.manifestFileHash();
563
+ }
564
+ if (params.archiveUrl()) {
565
+ paramDict[@"archiveUrl"] = params.archiveUrl();
566
+ }
567
+ if (params.patch()) {
568
+ paramDict[@"patch"] = params.patch();
569
+ }
570
+ if (params.filesUrl()) {
571
+ paramDict[@"filesUrl"] = params.filesUrl();
572
+ }
573
+ if (params.requestHeaders()) {
574
+ paramDict[@"requestHeaders"] = params.requestHeaders();
575
+ }
576
+ if (params.requestTimeout().has_value()) {
577
+ paramDict[@"requestTimeout"] = @(params.requestTimeout().value());
578
+ }
579
+ if (params.channel()) {
580
+ paramDict[@"channel"] = params.channel();
581
+ }
582
+ if (params.selection()) {
583
+ paramDict[@"selection"] = params.selection();
584
+ }
585
+
586
+ HotUpdaterImpl *impl = [HotUpdater sharedImpl];
587
+ [impl installArtifact:paramDict resolver:resolve rejecter:reject];
588
+ }
589
+
598
590
  - (NSNumber *)acceptReleaseCatalog:(NSDictionary *)params {
599
591
  return @([[HotUpdater sharedImpl] acceptReleaseCatalog:params]);
600
592
  }
@@ -710,7 +702,7 @@ RCT_EXPORT_MODULE();
710
702
  HotUpdaterImpl *impl = [HotUpdater sharedImpl];
711
703
  return facebook::react::typedConstants<JS::NativeHotUpdater::Constants::Builder>({
712
704
  .MIN_BUNDLE_ID = [self getMinBundleId],
713
- .APP_VERSION = [HotUpdaterImpl appVersion],
705
+ .APP_VERSION = HotUpdaterAppVersion(),
714
706
  .CHANNEL = [impl getChannel],
715
707
  .DEFAULT_CHANNEL = [impl getDefaultChannel],
716
708
  .FINGERPRINT_HASH = [impl getFingerprintHash],
@@ -760,6 +752,14 @@ RCT_EXPORT_METHOD(updateBundle:(NSDictionary *)params
760
752
  [impl updateBundle:params resolver:resolve rejecter:reject];
761
753
  }
762
754
 
755
+ RCT_EXPORT_METHOD(installArtifact:(NSDictionary *)params
756
+ resolve:(RCTPromiseResolveBlock)resolve
757
+ reject:(RCTPromiseRejectBlock)reject) {
758
+ NSLog(@"[HotUpdater.mm] installArtifact called.");
759
+ HotUpdaterImpl *impl = [HotUpdater sharedImpl];
760
+ [impl installArtifact:params resolver:resolve rejecter:reject];
761
+ }
762
+
763
763
  RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(acceptReleaseCatalog:(NSDictionary *)params) {
764
764
  return @([[HotUpdater sharedImpl] acceptReleaseCatalog:params]);
765
765
  }
@@ -260,10 +260,52 @@ private func hotUpdaterGetMinBundleId() -> String {
260
260
  * @param reject Promise reject callback; download and install failures
261
261
  * carry `stage`, `reason`, and their details in the error's userInfo
262
262
  */
263
- public func updateBundle(_ params: NSDictionary?,
263
+ public func updateBundle(_ params: [AnyHashable: Any]?,
264
264
  resolver resolve: @escaping RCTPromiseResolveBlock,
265
265
  rejecter reject: @escaping RCTPromiseRejectBlock) {
266
-
266
+ // Legacy: artifact protocol v1, where every file comes with its URL.
267
+ // JavaScript from the first protocol v2 SDK also sends `filesUrl`
268
+ // here, with only the Hermes patch in `assets`; its OTA Bundles must
269
+ // still install.
270
+ stage(params, resolver: resolve, rejecter: reject) { data, reject in
271
+ switch Self.parseFileResolver(data, required: false) {
272
+ case .failure(let error):
273
+ reject("INVALID_FILE_URL", error.localizedDescription, error)
274
+ return nil
275
+ case .success(let fileResolver):
276
+ return Self.parseAssets(data, requireFileHash: fileResolver == nil, rejecter: reject)
277
+ .map { ($0, fileResolver) }
278
+ }
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Installs a Bundle over artifact protocol v2: native asks `filesUrl` for
284
+ * the files it downloads one by one, after reusing local files and
285
+ * choosing the archive.
286
+ * @param params Dictionary with bundleId, manifest, `filesUrl`, and the
287
+ * optional Hermes `patch` with its `assetPath`
288
+ * @param resolve Promise resolve callback, called with the staged bundle's
289
+ * `{ delivery, patchFallback }`
290
+ * @param reject Promise reject callback, as for `updateBundle`
291
+ */
292
+ public func installArtifact(_ params: [AnyHashable: Any]?,
293
+ resolver resolve: @escaping RCTPromiseResolveBlock,
294
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
295
+ stage(params, resolver: resolve, rejecter: reject) { data, reject in
296
+ Self.parseArtifactInstall(data, rejecter: reject)
297
+ }
298
+ }
299
+
300
+ private typealias StagePlan = (assets: [String: ChangedAssetDescriptor], fileResolver: ArtifactFileResolver?)
301
+
302
+ /// Validates the common parameters, then stages the Bundle with the
303
+ /// assets and file resolver `plan` returns; `plan` rejects and returns nil
304
+ /// when its own parameters are invalid.
305
+ private func stage(_ params: [AnyHashable: Any]?,
306
+ resolver resolve: @escaping RCTPromiseResolveBlock,
307
+ rejecter reject: @escaping RCTPromiseRejectBlock,
308
+ plan: ([AnyHashable: Any], RCTPromiseRejectBlock) -> StagePlan?) {
267
309
  do {
268
310
  // Validate parameters (this runs on calling thread - typically JS thread)
269
311
  guard let data = params else {
@@ -309,54 +351,9 @@ private func hotUpdaterGetMinBundleId() -> String {
309
351
  } else {
310
352
  archiveUrl = nil
311
353
  }
312
- guard let assetsPayload = data["assets"] as? [String: [String: Any]] else {
313
- let error = UpdateFailure.invalidResponse.with(resource: .manifest)
314
- .error(description: "Missing manifest assets")
315
- reject("INVALID_MANIFEST", error.localizedDescription, error)
354
+ guard let planned = plan(data, reject) else {
316
355
  return
317
356
  }
318
- let assets = assetsPayload.reduce(into: [String: ChangedAssetDescriptor]()) { partialResult, entry in
319
- guard let fileHash = entry.value["fileHash"] as? String,
320
- !fileHash.isEmpty
321
- else {
322
- return
323
- }
324
-
325
- let patchPayload = entry.value["patch"] as? [String: Any]
326
- let patch = patchPayload.flatMap { payload -> BsdiffPatchDescriptor? in
327
- guard let algorithm = payload["algorithm"] as? String,
328
- let baseBundleId = payload["baseBundleId"] as? String,
329
- let baseFileHash = payload["baseFileHash"] as? String,
330
- let patchFileHash = payload["patchFileHash"] as? String,
331
- let patchUrlString = payload["patchUrl"] as? String,
332
- let patchUrl = URL(string: patchUrlString)
333
- else {
334
- return nil
335
- }
336
-
337
- return BsdiffPatchDescriptor(
338
- algorithm: algorithm,
339
- baseBundleId: baseBundleId,
340
- baseFileHash: baseFileHash,
341
- patchFileHash: patchFileHash,
342
- patchUrl: patchUrl,
343
- byteSize: Self.parseSafeByteSize(payload["byteSize"])
344
- )
345
- }
346
- let filePayload = entry.value["file"] as? [String: Any]
347
- let fileUrl = (filePayload?["url"] as? String).flatMap { URL(string: $0) }
348
- let fileCompression = filePayload?["compression"] as? String
349
- guard fileUrl != nil || patch != nil else {
350
- return
351
- }
352
-
353
- partialResult[entry.key] = ChangedAssetDescriptor(
354
- fileUrl: fileUrl,
355
- fileHash: fileHash,
356
- fileCompression: fileCompression,
357
- patch: patch
358
- )
359
- }
360
357
  let selection = (data["selection"] as? [String: Any]).flatMap(Self.parseSelection)
361
358
  if let selection, !bundleStorage.stageReleaseSelection(selection) {
362
359
  // Not an update failure, so the rejection carries no stage or reason.
@@ -371,7 +368,7 @@ private func hotUpdaterGetMinBundleId() -> String {
371
368
  NSLog("[HotUpdaterImpl] updateBundle called with bundleId: \(bundleId), manifestUrl: \(manifestUrl.absoluteString)")
372
369
 
373
370
  // Heavy work is delegated to bundle storage service with safe error handling
374
- bundleStorage.updateBundle(bundleId: bundleId, manifestUrl: manifestUrl, manifestFileHash: manifestFileHash, archiveUrl: archiveUrl, assets: assets, progressHandler: { payload in
371
+ bundleStorage.updateBundle(bundleId: bundleId, manifestUrl: manifestUrl, manifestFileHash: manifestFileHash, archiveUrl: archiveUrl, assets: planned.assets, fileResolver: planned.fileResolver, progressHandler: { payload in
375
372
  DispatchQueue.main.async {
376
373
  NotificationCenter.default.post(
377
374
  name: .updateProgressDidChange,
@@ -429,6 +426,111 @@ private func hotUpdaterGetMinBundleId() -> String {
429
426
  }
430
427
  }
431
428
 
429
+ /// Every changed file with its hash and URL or Hermes patch; without
430
+ /// `requireFileHash`, also entries that hold only the Hermes patch.
431
+ private static func parseAssets(_ data: [AnyHashable: Any],
432
+ requireFileHash: Bool,
433
+ rejecter reject: RCTPromiseRejectBlock) -> [String: ChangedAssetDescriptor]? {
434
+ guard let assetsPayload = data["assets"] as? [String: [String: Any]] else {
435
+ let error = UpdateFailure.invalidResponse.with(resource: .manifest)
436
+ .error(description: "Missing manifest assets")
437
+ reject("INVALID_MANIFEST", error.localizedDescription, error)
438
+ return nil
439
+ }
440
+ return assetsPayload.reduce(into: [String: ChangedAssetDescriptor]()) { partialResult, entry in
441
+ let fileHash = entry.value["fileHash"] as? String ?? ""
442
+ guard !fileHash.isEmpty || !requireFileHash else {
443
+ return
444
+ }
445
+
446
+ let patch = (entry.value["patch"] as? [String: Any]).flatMap(parsePatch)
447
+ let filePayload = entry.value["file"] as? [String: Any]
448
+ let fileUrl = (filePayload?["url"] as? String).flatMap { URL(string: $0) }
449
+ let fileCompression = filePayload?["compression"] as? String
450
+ guard fileUrl != nil || patch != nil,
451
+ !fileHash.isEmpty || fileUrl == nil else {
452
+ return
453
+ }
454
+
455
+ partialResult[entry.key] = ChangedAssetDescriptor(
456
+ fileUrl: fileUrl,
457
+ fileHash: fileHash,
458
+ fileCompression: fileCompression,
459
+ patch: patch
460
+ )
461
+ }
462
+ }
463
+
464
+ /// The resolver that asks `filesUrl` for the files native downloads one
465
+ /// by one, with the update check's headers and timeout; nil when
466
+ /// `filesUrl` is missing and not `required`.
467
+ private static func parseFileResolver(_ data: [AnyHashable: Any],
468
+ required: Bool) -> Result<ArtifactFileResolver?, NSError> {
469
+ let filesUrlString = data["filesUrl"] as? String ?? ""
470
+ if filesUrlString.isEmpty && !required {
471
+ return .success(nil)
472
+ }
473
+ guard !filesUrlString.isEmpty, let filesUrl = URL(string: filesUrlString) else {
474
+ return .failure(UpdateFailure.invalidResponse.with(resource: .file).error(
475
+ description: "Invalid 'filesUrl' provided: \(filesUrlString)"
476
+ ))
477
+ }
478
+ // The update check's headers, which the artifact files request repeats.
479
+ let headers = (data["requestHeaders"] as? [String: Any] ?? [:])
480
+ .compactMapValues { $0 as? String }
481
+ let timeout = (data["requestTimeout"] as? NSNumber).map { $0.doubleValue / 1000 }
482
+ .flatMap { $0 > 0 ? $0 : nil } ?? HTTPArtifactFileResolver.defaultTimeout
483
+ return .success(HTTPArtifactFileResolver(filesUrl: filesUrl, headers: headers, timeout: timeout))
484
+ }
485
+
486
+ /// Artifact protocol v2: the Hermes patch as the only planned asset, and
487
+ /// the resolver that asks `filesUrl` for the files native downloads one
488
+ /// by one.
489
+ private static func parseArtifactInstall(_ data: [AnyHashable: Any],
490
+ rejecter reject: RCTPromiseRejectBlock) -> StagePlan? {
491
+ let fileResolver: ArtifactFileResolver?
492
+ switch parseFileResolver(data, required: true) {
493
+ case .failure(let error):
494
+ reject("INVALID_FILE_URL", error.localizedDescription, error)
495
+ return nil
496
+ case .success(let resolver):
497
+ fileResolver = resolver
498
+ }
499
+ var assets: [String: ChangedAssetDescriptor] = [:]
500
+ if let patchPayload = data["patch"] as? [String: Any],
501
+ let assetPath = patchPayload["assetPath"] as? String,
502
+ let patch = parsePatch(patchPayload) {
503
+ assets[assetPath] = ChangedAssetDescriptor(
504
+ fileUrl: nil,
505
+ fileHash: "",
506
+ fileCompression: nil,
507
+ patch: patch
508
+ )
509
+ }
510
+ return (assets, fileResolver)
511
+ }
512
+
513
+ private static func parsePatch(_ payload: [String: Any]) -> BsdiffPatchDescriptor? {
514
+ guard let algorithm = payload["algorithm"] as? String,
515
+ let baseBundleId = payload["baseBundleId"] as? String,
516
+ let baseFileHash = payload["baseFileHash"] as? String,
517
+ let patchFileHash = payload["patchFileHash"] as? String,
518
+ let patchUrlString = payload["patchUrl"] as? String,
519
+ let patchUrl = URL(string: patchUrlString)
520
+ else {
521
+ return nil
522
+ }
523
+
524
+ return BsdiffPatchDescriptor(
525
+ algorithm: algorithm,
526
+ baseBundleId: baseBundleId,
527
+ baseFileHash: baseFileHash,
528
+ patchFileHash: patchFileHash,
529
+ patchUrl: patchUrl,
530
+ byteSize: parseSafeByteSize(payload["byteSize"])
531
+ )
532
+ }
533
+
432
534
  private static func parseSelection(_ data: [String: Any]) -> PersistedSelection? {
433
535
  guard let kind = data["kind"] as? String,
434
536
  let bundleId = data["bundleId"] as? String,
@@ -448,7 +550,7 @@ private func hotUpdaterGetMinBundleId() -> String {
448
550
  )
449
551
  }
450
552
 
451
- public func acceptReleaseCatalog(_ params: NSDictionary?) -> Bool {
553
+ public func acceptReleaseCatalog(_ params: [AnyHashable: Any]?) -> Bool {
452
554
  guard let data = params as? [String: Any],
453
555
  let catalogId = data["catalogId"] as? String,
454
556
  let scopeKey = data["scopeKey"] as? String,
@@ -507,7 +609,7 @@ private func hotUpdaterGetMinBundleId() -> String {
507
609
  }
508
610
  }
509
611
 
510
- public func isReleaseSelectionCurrent(_ params: NSDictionary?) -> Bool {
612
+ public func isReleaseSelectionCurrent(_ params: [AnyHashable: Any]?) -> Bool {
511
613
  guard let data = params as? [String: Any],
512
614
  let catalogId = data["catalogId"] as? String,
513
615
  let scopeKey = data["scopeKey"] as? String,
@@ -528,7 +630,7 @@ private func hotUpdaterGetMinBundleId() -> String {
528
630
  }
529
631
 
530
632
  public func commitReleaseSelection(
531
- _ params: NSDictionary?,
633
+ _ params: [AnyHashable: Any]?,
532
634
  resolver resolve: @escaping RCTPromiseResolveBlock,
533
635
  rejecter reject: @escaping RCTPromiseRejectBlock
534
636
  ) {
@@ -0,0 +1,26 @@
1
+ #if SWIFT_PACKAGE
2
+ import Foundation
3
+ import HotUpdaterInterface
4
+
5
+ // Swift Package Manager only: the Objective-C++ module reaches these classes
6
+ // through the protocols in HotUpdaterInterface.h, and these conformances make
7
+ // the compiler check that the classes still match them.
8
+ extension HotUpdaterImpl: HotUpdaterImplInterface {}
9
+
10
+ extension HotUpdaterConfig: HotUpdaterConfigInterface {}
11
+
12
+ @_cdecl("HotUpdaterMakeImpl")
13
+ public func hotUpdaterMakeImpl() -> HotUpdaterImplInterface {
14
+ HotUpdaterImpl()
15
+ }
16
+
17
+ @_cdecl("HotUpdaterAppVersion")
18
+ public func hotUpdaterAppVersion() -> NSString? {
19
+ HotUpdaterImpl.appVersion as NSString?
20
+ }
21
+
22
+ @_cdecl("HotUpdaterSharedConfig")
23
+ public func hotUpdaterSharedConfig() -> HotUpdaterConfigInterface {
24
+ HotUpdaterConfig.shared
25
+ }
26
+ #endif
@@ -0,0 +1,10 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ // Generated before every compile by ios/scripts/generate-min-bundle-id.zsh,
4
+ // so this file is the only one rebuilt when the value changes.
5
+ #import "HotUpdaterBuildMinBundleId.h"
6
+
7
+ NSString *HotUpdaterBuildMinBundleId(void)
8
+ {
9
+ return @HOT_UPDATER_BUILD_MIN_BUNDLE_ID;
10
+ }
@@ -0,0 +1,88 @@
1
+ // swift-tools-version: 5.10
2
+ import PackageDescription
3
+
4
+ let coreSources = [
5
+ "ArtifactFileResolver.swift",
6
+ "FileUtilities.swift",
7
+ "BrotliFileDecompressor.swift",
8
+ "TarArchiveExtractor.swift",
9
+ "BundleFileStorageService.swift",
10
+ "BuiltInAssetResolver.swift",
11
+ "BundleMetadata.swift",
12
+ "FileManagerService.swift",
13
+ "HashUtils.swift",
14
+ "HotUpdaterConfig.swift",
15
+ "KeyValueStorageService.swift",
16
+ "NotificationExtension.swift",
17
+ "ReleaseCatalogCacheService.swift",
18
+ "SignatureVerifier.swift",
19
+ "UpdateFailure.swift",
20
+ "URLSessionDownloadService.swift",
21
+ "VersionedPreferencesService.swift",
22
+ ]
23
+
24
+ let coreExcludedFiles = [
25
+ "BsdiffPatchBridge.h",
26
+ "BsdiffPatchBridge.mm",
27
+ "CohortService.swift",
28
+ "HotUpdater-Bridging-Header.h",
29
+ "HotUpdater.mm",
30
+ "HotUpdaterImpl.swift",
31
+ "HotUpdaterInterface.swift",
32
+ "HotUpdaterMinBundleId.m",
33
+ ]
34
+
35
+ let bsdiffPatchBridgeExcludedFiles =
36
+ coreSources + coreExcludedFiles.filter {
37
+ $0 != "BsdiffPatchBridge.h" && $0 != "BsdiffPatchBridge.mm"
38
+ }
39
+
40
+ let package = Package(
41
+ name: "HotUpdater",
42
+ platforms: [
43
+ .iOS(.v13),
44
+ .macOS(.v10_15)
45
+ ],
46
+ products: [
47
+ .library(
48
+ name: "HotUpdaterCore",
49
+ targets: ["HotUpdaterCore"]
50
+ )
51
+ ],
52
+ dependencies: [],
53
+ targets: [
54
+ .target(
55
+ name: "HotUpdaterRecovery",
56
+ path: "Recovery",
57
+ exclude: ["HotUpdaterRecoveryHooks.mm"],
58
+ sources: ["HotUpdaterRecovery.mm"],
59
+ publicHeadersPath: ".",
60
+ cxxSettings: [.define("RCT_NEW_ARCH_ENABLED")]
61
+ ),
62
+ .target(
63
+ name: "HotUpdaterBsdiffPatch",
64
+ path: "Internal",
65
+ exclude: bsdiffPatchBridgeExcludedFiles,
66
+ sources: ["BsdiffPatchBridge.mm"],
67
+ publicHeadersPath: ".",
68
+ linkerSettings: [
69
+ .linkedFramework("Foundation"),
70
+ .linkedLibrary("bz2"),
71
+ ]
72
+ ),
73
+ // React Native's full native module cannot be built through SPM yet,
74
+ // but the pure-Swift update core can be.
75
+ .target(
76
+ name: "HotUpdaterCore",
77
+ dependencies: ["HotUpdaterBsdiffPatch"],
78
+ path: "Internal",
79
+ exclude: coreExcludedFiles,
80
+ sources: coreSources
81
+ ),
82
+ .testTarget(
83
+ name: "HotUpdaterTest",
84
+ dependencies: ["HotUpdaterCore", "HotUpdaterRecovery"],
85
+ path: "Test"
86
+ ),
87
+ ]
88
+ )
@@ -0,0 +1,28 @@
1
+ import Foundation
2
+ import PackagePlugin
3
+
4
+ // Runs ios/scripts/generate-min-bundle-id.zsh before every build of the target,
5
+ // the SwiftPM counterpart of the podspec's script phase. The script writes a
6
+ // Swift file defining HotUpdaterBuildMinBundleId(), which HotUpdater.mm calls.
7
+ @main
8
+ struct HotUpdaterMinBundleIdPlugin: BuildToolPlugin {
9
+ func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
10
+ let script = context.package.directoryURL
11
+ .appending(path: "ios/scripts/generate-min-bundle-id.zsh")
12
+ let outputDirectory = context.pluginWorkDirectoryURL
13
+ .appending(path: "MinBundleId")
14
+ var environment: [String: String] = [:]
15
+ if let minBundleId = ProcessInfo.processInfo.environment["HOT_UPDATER_MIN_BUNDLE_ID"] {
16
+ environment["HOT_UPDATER_MIN_BUNDLE_ID"] = minBundleId
17
+ }
18
+ return [
19
+ .prebuildCommand(
20
+ displayName: "[HotUpdater] Generate MIN_BUNDLE_ID",
21
+ executable: URL(filePath: "/bin/zsh"),
22
+ arguments: [script.path(percentEncoded: false), "--swift", outputDirectory.path(percentEncoded: false)],
23
+ environment: environment,
24
+ outputFilesDirectory: outputDirectory
25
+ ),
26
+ ]
27
+ }
28
+ }