@capgo/capacitor-updater 8.51.21 → 8.51.23

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.
@@ -363,6 +363,38 @@ public class DownloadService extends Worker {
363
363
  return seenTargets.add(targetFile.getCanonicalPath());
364
364
  }
365
365
 
366
+ private static final class ManifestDownloadTask {
367
+
368
+ final String fileName;
369
+ final String finalFileHash;
370
+ final String downloadUrl;
371
+ final boolean isBrotli;
372
+ final File targetFile;
373
+ final File builtinFile;
374
+ final File cacheFile;
375
+ final File legacyCacheFile;
376
+
377
+ ManifestDownloadTask(
378
+ final String fileName,
379
+ final String finalFileHash,
380
+ final String downloadUrl,
381
+ final boolean isBrotli,
382
+ final File targetFile,
383
+ final File builtinFile,
384
+ final File cacheFile,
385
+ final File legacyCacheFile
386
+ ) {
387
+ this.fileName = fileName;
388
+ this.finalFileHash = finalFileHash;
389
+ this.downloadUrl = downloadUrl;
390
+ this.isBrotli = isBrotli;
391
+ this.targetFile = targetFile;
392
+ this.builtinFile = builtinFile;
393
+ this.cacheFile = cacheFile;
394
+ this.legacyCacheFile = legacyCacheFile;
395
+ }
396
+ }
397
+
366
398
  static boolean tryCopyBuiltinAsset(final AssetManager assets, final String fileName, final File dest, final String expectedHash) {
367
399
  if (assets == null || fileName == null || dest == null) {
368
400
  return false;
@@ -424,7 +456,7 @@ public class DownloadService extends Worker {
424
456
  return createFailureResult("Manifest is null");
425
457
  }
426
458
  } else {
427
- handleSingleFileDownload(url, id, documentsDir, dest, version, sessionKey, checksum);
459
+ handleSingleFileDownload(url, id, documentsDir, dest, version, sessionKey, checksum, publicKey);
428
460
  return createSuccessResult(dest, version, sessionKey, checksum, false);
429
461
  }
430
462
  } catch (DownloadRetryException e) {
@@ -515,6 +547,12 @@ public class DownloadService extends Worker {
515
547
  try {
516
548
  logger.debug("handleManifestDownload");
517
549
 
550
+ if (publicKey != null && !publicKey.isEmpty() && !CryptoCipher.isValidSessionKey(sessionKey)) {
551
+ logger.error("Public key present but no valid session key provided");
552
+ sendStatsAsync("session_key_required", version);
553
+ throw new IOException("Session key required when public key is present");
554
+ }
555
+
518
556
  // Send stats for manifest download start
519
557
  sendStatsAsync("download_manifest_start", version);
520
558
 
@@ -535,9 +573,7 @@ public class DownloadService extends Worker {
535
573
  int totalFiles = manifest.length();
536
574
  final AtomicLong completedFiles = new AtomicLong(0);
537
575
  final AtomicBoolean hasError = new AtomicBoolean(false);
538
-
539
- ExecutorService executor = Executors.newFixedThreadPool(Math.min(MANIFEST_MAX_CONCURRENT_FILES, Math.max(1, totalFiles)));
540
- List<Future<?>> futures = new ArrayList<>();
576
+ final List<ManifestDownloadTask> tasks = new ArrayList<>();
541
577
  final Set<String> seenTargets = new HashSet<>();
542
578
 
543
579
  for (int i = 0; i < totalFiles; i++) {
@@ -552,7 +588,13 @@ public class DownloadService extends Worker {
552
588
  continue;
553
589
  }
554
590
 
555
- if (publicKey != null && !publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
591
+ if (publicKey != null && !publicKey.isEmpty()) {
592
+ if (!CryptoCipher.isValidSessionKey(sessionKey)) {
593
+ logger.error("Public key present but no valid session key provided");
594
+ sendStatsAsync("session_key_required", version);
595
+ hasError.set(true);
596
+ continue;
597
+ }
556
598
  try {
557
599
  fileHash = CryptoCipher.decryptChecksum(fileHash, publicKey);
558
600
  } catch (Exception e) {
@@ -592,35 +634,56 @@ public class DownloadService extends Worker {
592
634
  final File legacyCacheFile =
593
635
  isBrotli && cacheFile != null ? new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName()) : null;
594
636
 
595
- // Ensure parent directories of the target file exist
596
- if (!Objects.requireNonNull(targetFile.getParentFile()).exists() && !targetFile.getParentFile().mkdirs()) {
597
- logger.error("Failed to create parent directory for: " + targetFile.getAbsolutePath());
598
- hasError.set(true);
599
- continue;
637
+ tasks.add(
638
+ new ManifestDownloadTask(
639
+ fileName,
640
+ finalFileHash,
641
+ downloadUrl,
642
+ isBrotli,
643
+ targetFile,
644
+ builtinFile,
645
+ cacheFile,
646
+ legacyCacheFile
647
+ )
648
+ );
649
+ }
650
+
651
+ if (hasError.get()) {
652
+ throw new IOException("Manifest contains invalid or duplicate file paths");
653
+ }
654
+
655
+ for (final ManifestDownloadTask task : tasks) {
656
+ if (!Objects.requireNonNull(task.targetFile.getParentFile()).exists() && !task.targetFile.getParentFile().mkdirs()) {
657
+ logger.error("Failed to create parent directory for: " + task.targetFile.getAbsolutePath());
658
+ throw new IOException("Failed to create parent directory for: " + task.targetFile.getAbsolutePath());
600
659
  }
660
+ }
601
661
 
602
- final boolean finalIsBrotli = isBrotli;
662
+ ExecutorService executor = Executors.newFixedThreadPool(Math.min(MANIFEST_MAX_CONCURRENT_FILES, Math.max(1, totalFiles)));
663
+ List<Future<?>> futures = new ArrayList<>();
664
+
665
+ for (final ManifestDownloadTask task : tasks) {
603
666
  Future<?> future = executor.submit(() -> {
604
667
  try {
605
- if (tryCopyBuiltinAsset(assets, fileName, targetFile, finalFileHash)) {
606
- logger.debug("using builtin asset " + fileName);
607
- } else if (tryCopyBuiltinFile(builtinFile, targetFile, finalFileHash)) {
608
- logger.debug("using builtin file " + fileName);
668
+ if (tryCopyBuiltinAsset(assets, task.fileName, task.targetFile, task.finalFileHash)) {
669
+ logger.debug("using builtin asset " + task.fileName);
670
+ } else if (tryCopyBuiltinFile(task.builtinFile, task.targetFile, task.finalFileHash)) {
671
+ logger.debug("using builtin file " + task.fileName);
609
672
  } else if (
610
- tryCopyFromCache(cacheFile, targetFile, finalFileHash) ||
611
- (legacyCacheFile != null && tryCopyFromCache(legacyCacheFile, targetFile, finalFileHash))
673
+ tryCopyFromCache(task.cacheFile, task.targetFile, task.finalFileHash) ||
674
+ (task.legacyCacheFile != null && tryCopyFromCache(task.legacyCacheFile, task.targetFile, task.finalFileHash))
612
675
  ) {
613
- logger.debug("already cached " + fileName);
676
+ logger.debug("already cached " + task.fileName);
614
677
  } else {
615
678
  downloadAndVerify(
616
- downloadUrl,
617
- targetFile,
618
- cacheFile,
619
- finalFileHash,
679
+ task.downloadUrl,
680
+ task.targetFile,
681
+ task.cacheFile,
682
+ task.finalFileHash,
620
683
  sessionKey,
621
684
  publicKey,
622
- finalIsBrotli,
623
- fileName
685
+ task.isBrotli,
686
+ task.fileName
624
687
  );
625
688
  }
626
689
 
@@ -628,8 +691,8 @@ public class DownloadService extends Worker {
628
691
  int percent = calcTotalPercent(completed, totalFiles);
629
692
  setProgress(percent);
630
693
  } catch (Exception e) {
631
- logger.error("Error processing file: " + fileName + " " + e.getMessage());
632
- sendStatsAsync("download_manifest_file_fail", version + ":" + fileName);
694
+ logger.error("Error processing file: " + task.fileName + " " + e.getMessage());
695
+ sendStatsAsync("download_manifest_file_fail", version + ":" + task.fileName);
633
696
  hasError.set(true);
634
697
  }
635
698
  });
@@ -676,8 +739,20 @@ public class DownloadService extends Worker {
676
739
  String dest,
677
740
  String version,
678
741
  String sessionKey,
679
- String checksum
742
+ String checksum,
743
+ String publicKey
680
744
  ) {
745
+ if (publicKey != null && !publicKey.isEmpty() && !CryptoCipher.isValidSessionKey(sessionKey)) {
746
+ logger.error("Public key present but no valid session key provided");
747
+ sendStatsAsync("session_key_required", version);
748
+ throw new RuntimeException("Session key required when public key is present");
749
+ }
750
+ if (checksum == null || checksum.isEmpty()) {
751
+ logger.error("No checksum provided");
752
+ sendStatsAsync("checksum_required", version);
753
+ throw new RuntimeException("Checksum required");
754
+ }
755
+
681
756
  // Send stats for zip download start
682
757
  sendStatsAsync("download_zip_start", version);
683
758
 
@@ -1059,7 +1134,7 @@ public class DownloadService extends Worker {
1059
1134
  }
1060
1135
  }
1061
1136
 
1062
- boolean needDecrypt = publicKey != null && !publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty();
1137
+ boolean needDecrypt = publicKey != null && !publicKey.isEmpty() && CryptoCipher.isValidSessionKey(sessionKey);
1063
1138
  File source = partial;
1064
1139
  if (needDecrypt) {
1065
1140
  workFile = new File(cacheFolder, "work_" + UUID.randomUUID() + "_" + targetFile.getName() + ".tmp");
@@ -96,7 +96,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
96
96
  deinit {
97
97
  implementation.shutdown()
98
98
  }
99
- private let pluginVersion: String = "8.51.21"
99
+ private let pluginVersion: String = "8.51.23"
100
100
  private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
101
101
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
102
102
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
@@ -582,8 +582,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
582
582
  var dest: URL
583
583
  if BundleInfo.ID_BUILTIN == id {
584
584
  dest = Bundle.main.resourceURL!.appendingPathComponent("public")
585
+ } else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
586
+ dest = bundleDir
585
587
  } else {
586
- dest = self.implementation.getBundleDirectory(id: id)
588
+ dest = Bundle.main.resourceURL!.appendingPathComponent("public")
587
589
  }
588
590
 
589
591
  if !FileManager.default.fileExists(atPath: dest.path) {
@@ -1609,7 +1611,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1609
1611
  throw makePreviewError("Invalid download URL")
1610
1612
  }
1611
1613
 
1614
+ if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
1615
+ self.logger.error("Public key present but no valid session key provided")
1616
+ self.implementation.sendStats(action: "session_key_required", versionName: version)
1617
+ throw makePreviewError("Session key required when public key is present")
1618
+ }
1619
+
1612
1620
  var checksum = rawChecksum
1621
+ if manifestEntries == nil && checksum.isEmpty {
1622
+ self.logger.error("No checksum provided")
1623
+ self.implementation.sendStats(action: "checksum_required", versionName: version)
1624
+ throw makePreviewError("Checksum required")
1625
+ }
1626
+
1613
1627
  let next: BundleInfo
1614
1628
  if let manifestEntries = manifestEntries {
1615
1629
  next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
@@ -1617,29 +1631,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1617
1631
  next = try self.implementation.download(url: url, version: version, sessionKey: sessionKey)
1618
1632
  }
1619
1633
 
1620
- if self.implementation.publicKey != "" && checksum == "" {
1621
- self.logger.error("Public key present but no checksum provided")
1622
- self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
1623
- let id = next.getId()
1624
- let resDel = self.implementation.delete(id: id)
1625
- if !resDel {
1626
- self.logger.error("Delete failed, id \(id) doesn't exist")
1627
- }
1628
- throw ObjectSavableError.checksum
1629
- }
1630
-
1631
- checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
1632
- CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
1633
- CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
1634
- if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
1635
- self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
1636
- self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
1637
- let id = next.getId()
1638
- let resDel = self.implementation.delete(id: id)
1639
- if !resDel {
1640
- self.logger.error("Delete failed, id \(id) doesn't exist")
1634
+ if manifestEntries == nil {
1635
+ checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
1636
+ CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
1637
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
1638
+ if next.getChecksum() != checksum {
1639
+ self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
1640
+ self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
1641
+ let id = next.getId()
1642
+ let resDel = self.implementation.delete(id: id)
1643
+ if !resDel {
1644
+ self.logger.error("Delete failed, id \(id) doesn't exist")
1645
+ }
1646
+ throw ObjectSavableError.checksum
1641
1647
  }
1642
- throw ObjectSavableError.checksum
1643
1648
  }
1644
1649
 
1645
1650
  self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
@@ -1691,8 +1696,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1691
1696
  let id = self.implementation.getCurrentBundleId()
1692
1697
  if BundleInfo.ID_BUILTIN == id {
1693
1698
  return Bundle.main.resourceURL!.appendingPathComponent("public")
1699
+ } else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
1700
+ return bundleDir
1694
1701
  } else {
1695
- return self.implementation.getBundleDirectory(id: id)
1702
+ return Bundle.main.resourceURL!.appendingPathComponent("public")
1696
1703
  }
1697
1704
  }
1698
1705
 
@@ -3496,13 +3503,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3496
3503
  }
3497
3504
 
3498
3505
  private func performHideSplashscreen() {
3499
- self.cancelSplashscreenTimeout()
3500
3506
  self.removeSplashscreenLoader()
3501
3507
  self.splashscreenInvocationToken += 1
3502
- self.invokeSplashscreenMethod(
3503
- methodName: "hide",
3504
- callbackId: "autoHideSplashscreen",
3505
- options: self.splashscreenOptions(methodName: "hide"),
3508
+ self.invokeSplashscreenAction(
3509
+ .hide,
3506
3510
  retriesRemaining: self.splashscreenMaxRetries,
3507
3511
  requestToken: self.splashscreenInvocationToken
3508
3512
  )
@@ -3522,10 +3526,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3522
3526
  self.cancelSplashscreenTimeout()
3523
3527
  self.autoSplashscreenTimedOut = false
3524
3528
  self.splashscreenInvocationToken += 1
3525
- self.invokeSplashscreenMethod(
3526
- methodName: "show",
3527
- callbackId: "autoShowSplashscreen",
3528
- options: self.splashscreenOptions(methodName: "show"),
3529
+ self.invokeSplashscreenAction(
3530
+ .show,
3529
3531
  retriesRemaining: self.splashscreenMaxRetries,
3530
3532
  requestToken: self.splashscreenInvocationToken
3531
3533
  )
@@ -3535,7 +3537,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3535
3537
  }
3536
3538
 
3537
3539
  private func splashscreenOptions(methodName: String) -> [String: Any] {
3538
- methodName == "show" ? ["autoHide": false] : [:]
3540
+ methodName == "show" ? ["autoHide": false, "fadeInDuration": 0] : [:]
3539
3541
  }
3540
3542
 
3541
3543
  private func splashscreenCompletedMessage(methodName: String) -> String {
@@ -3554,20 +3556,59 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3554
3556
  self.splashscreenInvocationToken += 1
3555
3557
  }
3556
3558
 
3557
- private func makeSplashscreenCall(callbackId: String, options: [String: Any], methodName: String) -> CAPPluginCall {
3558
- CAPPluginCall(callbackId: callbackId, methodName: methodName, options: options, success: { [weak self] (_, _) in
3559
- guard let self = self else { return }
3560
- self.logger.info(self.splashscreenCompletedMessage(methodName: methodName))
3561
- }, error: { [weak self] (_) in
3562
- guard let self = self else { return }
3563
- self.logger.error("Failed to auto-\(methodName) splashscreen")
3564
- })
3559
+ private enum SplashscreenAction {
3560
+ case show
3561
+ case hide
3562
+
3563
+ var methodName: String {
3564
+ switch self {
3565
+ case .show:
3566
+ return "show"
3567
+ case .hide:
3568
+ return "hide"
3569
+ }
3570
+ }
3571
+
3572
+ var unavailableMessageVerb: String {
3573
+ switch self {
3574
+ case .show:
3575
+ return "showing"
3576
+ case .hide:
3577
+ return "hiding"
3578
+ }
3579
+ }
3565
3580
  }
3566
3581
 
3567
- private func invokeSplashscreenMethod(
3568
- methodName: String,
3569
- callbackId: String,
3570
- options: [String: Any],
3582
+ private func splashscreenOptionsJSON(methodName: String) -> String {
3583
+ let options = self.splashscreenOptions(methodName: methodName)
3584
+ guard !options.isEmpty,
3585
+ let data = try? JSONSerialization.data(withJSONObject: options),
3586
+ let json = String(data: data, encoding: .utf8) else {
3587
+ return "{}"
3588
+ }
3589
+ return json
3590
+ }
3591
+
3592
+ private func splashscreenPluginExposesMethod(_ methodName: String, on plugin: CAPPlugin) -> Bool {
3593
+ guard let bridgedPlugin = plugin as? CAPBridgedPlugin else {
3594
+ return false
3595
+ }
3596
+ return bridgedPlugin.pluginMethods.contains { $0.name == methodName }
3597
+ }
3598
+
3599
+ private func splashscreenBridgeScript(for action: SplashscreenAction) -> String {
3600
+ let optionsJSON = self.splashscreenOptionsJSON(methodName: action.methodName)
3601
+ return """
3602
+ var cap = window.Capacitor;
3603
+ if (!cap || typeof cap.nativePromise !== 'function') {
3604
+ throw new Error('Capacitor bridge not ready');
3605
+ }
3606
+ return await cap.nativePromise('\(self.splashscreenPluginName)', '\(action.methodName)', \(optionsJSON));
3607
+ """
3608
+ }
3609
+
3610
+ private func invokeSplashscreenAction(
3611
+ _ action: SplashscreenAction,
3571
3612
  retriesRemaining: Int,
3572
3613
  requestToken: Int
3573
3614
  ) {
@@ -3576,22 +3617,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3576
3617
  }
3577
3618
 
3578
3619
  guard let bridge = self.bridge else {
3579
- self.retrySplashscreenMethod(
3580
- methodName: methodName,
3581
- callbackId: callbackId,
3582
- options: options,
3620
+ self.retrySplashscreenAction(
3621
+ action,
3583
3622
  retriesRemaining: retriesRemaining,
3584
3623
  requestToken: requestToken,
3585
- message: "Bridge not available for \(methodName == "show" ? "showing" : "hiding") splashscreen with autoSplashscreen"
3624
+ message: "Bridge not available for \(action.unavailableMessageVerb) splashscreen with autoSplashscreen"
3586
3625
  )
3587
3626
  return
3588
3627
  }
3589
3628
 
3590
3629
  guard let splashScreenPlugin = bridge.plugin(withName: self.splashscreenPluginName) else {
3591
- self.retrySplashscreenMethod(
3592
- methodName: methodName,
3593
- callbackId: callbackId,
3594
- options: options,
3630
+ self.retrySplashscreenAction(
3631
+ action,
3595
3632
  retriesRemaining: retriesRemaining,
3596
3633
  requestToken: requestToken,
3597
3634
  message: "autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin."
@@ -3599,37 +3636,86 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3599
3636
  return
3600
3637
  }
3601
3638
 
3602
- let selector = NSSelectorFromString("\(methodName):")
3603
- guard splashScreenPlugin.responds(to: selector) else {
3604
- self.retrySplashscreenMethod(
3605
- methodName: methodName,
3606
- callbackId: callbackId,
3607
- options: options,
3639
+ guard self.splashscreenPluginExposesMethod(action.methodName, on: splashScreenPlugin) else {
3640
+ self.retrySplashscreenAction(
3641
+ action,
3642
+ retriesRemaining: retriesRemaining,
3643
+ requestToken: requestToken,
3644
+ message: "autoSplashscreen: SplashScreen plugin does not expose \(action.methodName)(). Make sure @capacitor/splash-screen plugin is properly installed."
3645
+ )
3646
+ return
3647
+ }
3648
+
3649
+ guard let webView = bridge.webView else {
3650
+ self.retrySplashscreenAction(
3651
+ action,
3608
3652
  retriesRemaining: retriesRemaining,
3609
3653
  requestToken: requestToken,
3610
- message: "autoSplashscreen: SplashScreen plugin does not respond to \(methodName): method. Make sure @capacitor/splash-screen plugin is properly installed."
3654
+ message: "WebView not available for \(action.unavailableMessageVerb) splashscreen with autoSplashscreen"
3611
3655
  )
3612
3656
  return
3613
3657
  }
3614
3658
 
3615
- let call = self.makeSplashscreenCall(callbackId: callbackId, options: options, methodName: methodName)
3616
- _ = splashScreenPlugin.perform(selector, with: call)
3617
- self.logger.info("Called SplashScreen \(methodName) method")
3659
+ let script = self.splashscreenBridgeScript(for: action)
3660
+ let runInvocation = { [weak self] in
3661
+ guard let self = self, requestToken == self.splashscreenInvocationToken else {
3662
+ return
3663
+ }
3664
+ webView.callAsyncJavaScript(
3665
+ script,
3666
+ arguments: [:],
3667
+ in: nil,
3668
+ in: .page
3669
+ ) { [weak self] (result: Result<Any, Error>) in
3670
+ guard let self = self, requestToken == self.splashscreenInvocationToken else {
3671
+ return
3672
+ }
3673
+ if case .failure(let error) = result {
3674
+ self.retrySplashscreenAction(
3675
+ action,
3676
+ retriesRemaining: retriesRemaining,
3677
+ requestToken: requestToken,
3678
+ message: "Failed to invoke SplashScreen \(action.methodName) via Capacitor bridge: \(error.localizedDescription)"
3679
+ )
3680
+ return
3681
+ }
3682
+ if action == .hide {
3683
+ self.cancelSplashscreenTimeout()
3684
+ }
3685
+ self.logger.info("Called SplashScreen \(action.methodName) method")
3686
+ self.logger.info(self.splashscreenCompletedMessage(methodName: action.methodName))
3687
+ }
3688
+ }
3689
+
3690
+ if Thread.isMainThread {
3691
+ runInvocation()
3692
+ } else {
3693
+ DispatchQueue.main.async(execute: runInvocation)
3694
+ }
3618
3695
  }
3619
3696
 
3620
- private func retrySplashscreenMethod(
3621
- methodName: String,
3622
- callbackId: String,
3623
- options: [String: Any],
3697
+ private func retrySplashscreenAction(
3698
+ _ action: SplashscreenAction,
3624
3699
  retriesRemaining: Int,
3625
3700
  requestToken: Int,
3626
3701
  message: String
3627
3702
  ) {
3628
3703
  guard retriesRemaining > 0 else {
3629
- if methodName == "show" {
3704
+ if action == .show {
3630
3705
  self.logger.warn(message)
3631
- } else {
3632
- self.logger.error(message)
3706
+ return
3707
+ }
3708
+
3709
+ self.logger.error("\(message). Scheduling another hide retry.")
3710
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.splashscreenRetryDelayMilliseconds)) { [weak self] in
3711
+ guard let self = self, requestToken == self.splashscreenInvocationToken else {
3712
+ return
3713
+ }
3714
+ self.invokeSplashscreenAction(
3715
+ action,
3716
+ retriesRemaining: self.splashscreenMaxRetries,
3717
+ requestToken: requestToken
3718
+ )
3633
3719
  }
3634
3720
  return
3635
3721
  }
@@ -3639,10 +3725,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3639
3725
  guard let self = self, requestToken == self.splashscreenInvocationToken else {
3640
3726
  return
3641
3727
  }
3642
- self.invokeSplashscreenMethod(
3643
- methodName: methodName,
3644
- callbackId: callbackId,
3645
- options: options,
3728
+ self.invokeSplashscreenAction(
3729
+ action,
3646
3730
  retriesRemaining: retriesRemaining - 1,
3647
3731
  requestToken: requestToken
3648
3732
  )
@@ -4095,6 +4179,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4095
4179
  self.updateUrl = updateUrl
4096
4180
  }
4097
4181
 
4182
+ func downloadBundleForTesting(
4183
+ urlString: String,
4184
+ version: String,
4185
+ sessionKey: String,
4186
+ checksum: String,
4187
+ manifestEntries: [ManifestEntry]?
4188
+ ) throws -> BundleInfo {
4189
+ try downloadBundle(
4190
+ urlString: urlString,
4191
+ version: version,
4192
+ sessionKey: sessionKey,
4193
+ checksum: checksum,
4194
+ manifestEntries: manifestEntries
4195
+ )
4196
+ }
4197
+
4098
4198
  func setAutoUpdateModeForTesting(_ autoUpdateMode: String) {
4099
4199
  self.autoUpdateMode = Self.normalizedAutoUpdateMode(autoUpdateMode)
4100
4200
  self.autoUpdate = Self.isAutoUpdateModeEnabled(self.autoUpdateMode)
@@ -4430,6 +4530,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4430
4530
  if latestVersionName != "" && current.getVersionName() != latestVersionName {
4431
4531
  do {
4432
4532
  self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
4533
+ if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
4534
+ self.logger.error("Public key present but no valid session key provided")
4535
+ self.implementation.sendStats(action: "session_key_required", versionName: latestVersionName)
4536
+ self.endBackGroundTaskWithNotif(
4537
+ msg: "Session key required when public key is present",
4538
+ latestVersionName: latestVersionName,
4539
+ current: current,
4540
+ plannedDirectUpdate: plannedDirectUpdate
4541
+ )
4542
+ return
4543
+ }
4433
4544
  var nextImpl = self.implementation.getBundleInfoByVersionName(version: latestVersionName)
4434
4545
  let needsDownload = nextImpl.map(Self.shouldRetryDownloadForExistingBundle) ?? true
4435
4546
  if needsDownload {
@@ -4447,6 +4558,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4447
4558
  if res.manifest != nil {
4448
4559
  nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
4449
4560
  } else {
4561
+ if res.checksum.isEmpty {
4562
+ self.logger.error("No checksum provided")
4563
+ self.implementation.sendStats(action: "checksum_required", versionName: latestVersionName)
4564
+ self.endBackGroundTaskWithNotif(
4565
+ msg: "Checksum required",
4566
+ latestVersionName: latestVersionName,
4567
+ current: current,
4568
+ plannedDirectUpdate: plannedDirectUpdate
4569
+ )
4570
+ return
4571
+ }
4450
4572
  nextImpl = try self.implementation.download(url: downloadUrl, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
4451
4573
  }
4452
4574
  }
@@ -4475,24 +4597,26 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4475
4597
  self.endBackGroundTask()
4476
4598
  return
4477
4599
  }
4478
- res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
4479
- CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
4480
- CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
4481
- if res.checksum != "" && next.getChecksum() != res.checksum && res.manifest == nil {
4482
- self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
4483
- self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
4484
- let id = next.getId()
4485
- let resDel = self.implementation.delete(id: id)
4486
- if !resDel {
4487
- self.logger.error("Delete failed, id \(id) doesn't exist")
4600
+ if res.manifest == nil {
4601
+ res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
4602
+ CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
4603
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
4604
+ if next.getChecksum() != res.checksum {
4605
+ self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
4606
+ self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
4607
+ let id = next.getId()
4608
+ let resDel = self.implementation.delete(id: id)
4609
+ if !resDel {
4610
+ self.logger.error("Delete failed, id \(id) doesn't exist")
4611
+ }
4612
+ self.endBackGroundTaskWithNotif(
4613
+ msg: "Error checksum",
4614
+ latestVersionName: latestVersionName,
4615
+ current: current,
4616
+ plannedDirectUpdate: plannedDirectUpdate
4617
+ )
4618
+ return
4488
4619
  }
4489
- self.endBackGroundTaskWithNotif(
4490
- msg: "Error checksum",
4491
- latestVersionName: latestVersionName,
4492
- current: current,
4493
- plannedDirectUpdate: plannedDirectUpdate
4494
- )
4495
- return
4496
4620
  }
4497
4621
  if self.shouldBlockAutoUpdateForPreviewSession() {
4498
4622
  self.clearDownloadInProgressState()