@capgo/capacitor-updater 7.51.20 → 7.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.
@@ -96,7 +96,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
96
96
  deinit {
97
97
  implementation.shutdown()
98
98
  }
99
- private let pluginVersion: String = "7.51.20"
99
+ private let pluginVersion: String = "7.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) {
@@ -1611,7 +1613,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1611
1613
  throw makePreviewError("Invalid download URL")
1612
1614
  }
1613
1615
 
1616
+ if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
1617
+ self.logger.error("Public key present but no valid session key provided")
1618
+ self.implementation.sendStats(action: "session_key_required", versionName: version)
1619
+ throw makePreviewError("Session key required when public key is present")
1620
+ }
1621
+
1614
1622
  var checksum = rawChecksum
1623
+ if manifestEntries == nil && checksum.isEmpty {
1624
+ self.logger.error("No checksum provided")
1625
+ self.implementation.sendStats(action: "checksum_required", versionName: version)
1626
+ throw makePreviewError("Checksum required")
1627
+ }
1628
+
1615
1629
  let next: BundleInfo
1616
1630
  if let manifestEntries = manifestEntries {
1617
1631
  next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
@@ -1619,29 +1633,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1619
1633
  next = try self.implementation.download(url: url, version: version, sessionKey: sessionKey)
1620
1634
  }
1621
1635
 
1622
- if self.implementation.publicKey != "" && checksum == "" {
1623
- self.logger.error("Public key present but no checksum provided")
1624
- self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
1625
- let id = next.getId()
1626
- let resDel = self.implementation.delete(id: id)
1627
- if !resDel {
1628
- self.logger.error("Delete failed, id \(id) doesn't exist")
1629
- }
1630
- throw ObjectSavableError.checksum
1631
- }
1632
-
1633
- checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
1634
- CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
1635
- CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
1636
- if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
1637
- self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
1638
- self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
1639
- let id = next.getId()
1640
- let resDel = self.implementation.delete(id: id)
1641
- if !resDel {
1642
- self.logger.error("Delete failed, id \(id) doesn't exist")
1636
+ if manifestEntries == nil {
1637
+ checksum = try CryptoCipher.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
1638
+ CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
1639
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: checksum)
1640
+ if next.getChecksum() != checksum {
1641
+ self.logger.error("Error checksum \(next.getChecksum()) \(checksum)")
1642
+ self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
1643
+ let id = next.getId()
1644
+ let resDel = self.implementation.delete(id: id)
1645
+ if !resDel {
1646
+ self.logger.error("Delete failed, id \(id) doesn't exist")
1647
+ }
1648
+ throw ObjectSavableError.checksum
1643
1649
  }
1644
- throw ObjectSavableError.checksum
1645
1650
  }
1646
1651
 
1647
1652
  self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
@@ -1693,8 +1698,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1693
1698
  let id = self.implementation.getCurrentBundleId()
1694
1699
  if BundleInfo.ID_BUILTIN == id {
1695
1700
  return Bundle.main.resourceURL!.appendingPathComponent("public")
1701
+ } else if let bundleDir = try? self.implementation.getBundleDirectory(id: id) {
1702
+ return bundleDir
1696
1703
  } else {
1697
- return self.implementation.getBundleDirectory(id: id)
1704
+ return Bundle.main.resourceURL!.appendingPathComponent("public")
1698
1705
  }
1699
1706
  }
1700
1707
 
@@ -3498,13 +3505,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3498
3505
  }
3499
3506
 
3500
3507
  private func performHideSplashscreen() {
3501
- self.cancelSplashscreenTimeout()
3502
3508
  self.removeSplashscreenLoader()
3503
3509
  self.splashscreenInvocationToken += 1
3504
- self.invokeSplashscreenMethod(
3505
- methodName: "hide",
3506
- callbackId: "autoHideSplashscreen",
3507
- options: self.splashscreenOptions(methodName: "hide"),
3510
+ self.invokeSplashscreenAction(
3511
+ .hide,
3508
3512
  retriesRemaining: self.splashscreenMaxRetries,
3509
3513
  requestToken: self.splashscreenInvocationToken
3510
3514
  )
@@ -3524,10 +3528,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3524
3528
  self.cancelSplashscreenTimeout()
3525
3529
  self.autoSplashscreenTimedOut = false
3526
3530
  self.splashscreenInvocationToken += 1
3527
- self.invokeSplashscreenMethod(
3528
- methodName: "show",
3529
- callbackId: "autoShowSplashscreen",
3530
- options: self.splashscreenOptions(methodName: "show"),
3531
+ self.invokeSplashscreenAction(
3532
+ .show,
3531
3533
  retriesRemaining: self.splashscreenMaxRetries,
3532
3534
  requestToken: self.splashscreenInvocationToken
3533
3535
  )
@@ -3537,7 +3539,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3537
3539
  }
3538
3540
 
3539
3541
  private func splashscreenOptions(methodName: String) -> [String: Any] {
3540
- methodName == "show" ? ["autoHide": false] : [:]
3542
+ methodName == "show" ? ["autoHide": false, "fadeInDuration": 0] : [:]
3541
3543
  }
3542
3544
 
3543
3545
  private func splashscreenCompletedMessage(methodName: String) -> String {
@@ -3556,20 +3558,59 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3556
3558
  self.splashscreenInvocationToken += 1
3557
3559
  }
3558
3560
 
3559
- private func makeSplashscreenCall(callbackId: String, options: [String: Any], methodName: String) -> CAPPluginCall {
3560
- CAPPluginCall(callbackId: callbackId, methodName: methodName, options: options, success: { [weak self] (_, _) in
3561
- guard let self = self else { return }
3562
- self.logger.info(self.splashscreenCompletedMessage(methodName: methodName))
3563
- }, error: { [weak self] (_) in
3564
- guard let self = self else { return }
3565
- self.logger.error("Failed to auto-\(methodName) splashscreen")
3566
- })
3561
+ private enum SplashscreenAction {
3562
+ case show
3563
+ case hide
3564
+
3565
+ var methodName: String {
3566
+ switch self {
3567
+ case .show:
3568
+ return "show"
3569
+ case .hide:
3570
+ return "hide"
3571
+ }
3572
+ }
3573
+
3574
+ var unavailableMessageVerb: String {
3575
+ switch self {
3576
+ case .show:
3577
+ return "showing"
3578
+ case .hide:
3579
+ return "hiding"
3580
+ }
3581
+ }
3567
3582
  }
3568
3583
 
3569
- private func invokeSplashscreenMethod(
3570
- methodName: String,
3571
- callbackId: String,
3572
- options: [String: Any],
3584
+ private func splashscreenOptionsJSON(methodName: String) -> String {
3585
+ let options = self.splashscreenOptions(methodName: methodName)
3586
+ guard !options.isEmpty,
3587
+ let data = try? JSONSerialization.data(withJSONObject: options),
3588
+ let json = String(data: data, encoding: .utf8) else {
3589
+ return "{}"
3590
+ }
3591
+ return json
3592
+ }
3593
+
3594
+ private func splashscreenPluginExposesMethod(_ methodName: String, on plugin: CAPPlugin) -> Bool {
3595
+ guard let bridgedPlugin = plugin as? CAPBridgedPlugin else {
3596
+ return false
3597
+ }
3598
+ return bridgedPlugin.pluginMethods.contains { $0.name == methodName }
3599
+ }
3600
+
3601
+ private func splashscreenBridgeScript(for action: SplashscreenAction) -> String {
3602
+ let optionsJSON = self.splashscreenOptionsJSON(methodName: action.methodName)
3603
+ return """
3604
+ var cap = window.Capacitor;
3605
+ if (!cap || typeof cap.nativePromise !== 'function') {
3606
+ throw new Error('Capacitor bridge not ready');
3607
+ }
3608
+ return await cap.nativePromise('\(self.splashscreenPluginName)', '\(action.methodName)', \(optionsJSON));
3609
+ """
3610
+ }
3611
+
3612
+ private func invokeSplashscreenAction(
3613
+ _ action: SplashscreenAction,
3573
3614
  retriesRemaining: Int,
3574
3615
  requestToken: Int
3575
3616
  ) {
@@ -3578,22 +3619,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3578
3619
  }
3579
3620
 
3580
3621
  guard let bridge = self.bridge else {
3581
- self.retrySplashscreenMethod(
3582
- methodName: methodName,
3583
- callbackId: callbackId,
3584
- options: options,
3622
+ self.retrySplashscreenAction(
3623
+ action,
3585
3624
  retriesRemaining: retriesRemaining,
3586
3625
  requestToken: requestToken,
3587
- message: "Bridge not available for \(methodName == "show" ? "showing" : "hiding") splashscreen with autoSplashscreen"
3626
+ message: "Bridge not available for \(action.unavailableMessageVerb) splashscreen with autoSplashscreen"
3588
3627
  )
3589
3628
  return
3590
3629
  }
3591
3630
 
3592
3631
  guard let splashScreenPlugin = bridge.plugin(withName: self.splashscreenPluginName) else {
3593
- self.retrySplashscreenMethod(
3594
- methodName: methodName,
3595
- callbackId: callbackId,
3596
- options: options,
3632
+ self.retrySplashscreenAction(
3633
+ action,
3597
3634
  retriesRemaining: retriesRemaining,
3598
3635
  requestToken: requestToken,
3599
3636
  message: "autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin."
@@ -3601,37 +3638,86 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3601
3638
  return
3602
3639
  }
3603
3640
 
3604
- let selector = NSSelectorFromString("\(methodName):")
3605
- guard splashScreenPlugin.responds(to: selector) else {
3606
- self.retrySplashscreenMethod(
3607
- methodName: methodName,
3608
- callbackId: callbackId,
3609
- options: options,
3641
+ guard self.splashscreenPluginExposesMethod(action.methodName, on: splashScreenPlugin) else {
3642
+ self.retrySplashscreenAction(
3643
+ action,
3644
+ retriesRemaining: retriesRemaining,
3645
+ requestToken: requestToken,
3646
+ message: "autoSplashscreen: SplashScreen plugin does not expose \(action.methodName)(). Make sure @capacitor/splash-screen plugin is properly installed."
3647
+ )
3648
+ return
3649
+ }
3650
+
3651
+ guard let webView = bridge.webView else {
3652
+ self.retrySplashscreenAction(
3653
+ action,
3610
3654
  retriesRemaining: retriesRemaining,
3611
3655
  requestToken: requestToken,
3612
- message: "autoSplashscreen: SplashScreen plugin does not respond to \(methodName): method. Make sure @capacitor/splash-screen plugin is properly installed."
3656
+ message: "WebView not available for \(action.unavailableMessageVerb) splashscreen with autoSplashscreen"
3613
3657
  )
3614
3658
  return
3615
3659
  }
3616
3660
 
3617
- let call = self.makeSplashscreenCall(callbackId: callbackId, options: options, methodName: methodName)
3618
- _ = splashScreenPlugin.perform(selector, with: call)
3619
- self.logger.info("Called SplashScreen \(methodName) method")
3661
+ let script = self.splashscreenBridgeScript(for: action)
3662
+ let runInvocation = { [weak self] in
3663
+ guard let self = self, requestToken == self.splashscreenInvocationToken else {
3664
+ return
3665
+ }
3666
+ webView.callAsyncJavaScript(
3667
+ script,
3668
+ arguments: [:],
3669
+ in: nil,
3670
+ in: .page
3671
+ ) { [weak self] (result: Result<Any, Error>) in
3672
+ guard let self = self, requestToken == self.splashscreenInvocationToken else {
3673
+ return
3674
+ }
3675
+ if case .failure(let error) = result {
3676
+ self.retrySplashscreenAction(
3677
+ action,
3678
+ retriesRemaining: retriesRemaining,
3679
+ requestToken: requestToken,
3680
+ message: "Failed to invoke SplashScreen \(action.methodName) via Capacitor bridge: \(error.localizedDescription)"
3681
+ )
3682
+ return
3683
+ }
3684
+ if action == .hide {
3685
+ self.cancelSplashscreenTimeout()
3686
+ }
3687
+ self.logger.info("Called SplashScreen \(action.methodName) method")
3688
+ self.logger.info(self.splashscreenCompletedMessage(methodName: action.methodName))
3689
+ }
3690
+ }
3691
+
3692
+ if Thread.isMainThread {
3693
+ runInvocation()
3694
+ } else {
3695
+ DispatchQueue.main.async(execute: runInvocation)
3696
+ }
3620
3697
  }
3621
3698
 
3622
- private func retrySplashscreenMethod(
3623
- methodName: String,
3624
- callbackId: String,
3625
- options: [String: Any],
3699
+ private func retrySplashscreenAction(
3700
+ _ action: SplashscreenAction,
3626
3701
  retriesRemaining: Int,
3627
3702
  requestToken: Int,
3628
3703
  message: String
3629
3704
  ) {
3630
3705
  guard retriesRemaining > 0 else {
3631
- if methodName == "show" {
3706
+ if action == .show {
3632
3707
  self.logger.warn(message)
3633
- } else {
3634
- self.logger.error(message)
3708
+ return
3709
+ }
3710
+
3711
+ self.logger.error("\(message). Scheduling another hide retry.")
3712
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.splashscreenRetryDelayMilliseconds)) { [weak self] in
3713
+ guard let self = self, requestToken == self.splashscreenInvocationToken else {
3714
+ return
3715
+ }
3716
+ self.invokeSplashscreenAction(
3717
+ action,
3718
+ retriesRemaining: self.splashscreenMaxRetries,
3719
+ requestToken: requestToken
3720
+ )
3635
3721
  }
3636
3722
  return
3637
3723
  }
@@ -3641,10 +3727,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3641
3727
  guard let self = self, requestToken == self.splashscreenInvocationToken else {
3642
3728
  return
3643
3729
  }
3644
- self.invokeSplashscreenMethod(
3645
- methodName: methodName,
3646
- callbackId: callbackId,
3647
- options: options,
3730
+ self.invokeSplashscreenAction(
3731
+ action,
3648
3732
  retriesRemaining: retriesRemaining - 1,
3649
3733
  requestToken: requestToken
3650
3734
  )
@@ -4097,6 +4181,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4097
4181
  self.updateUrl = updateUrl
4098
4182
  }
4099
4183
 
4184
+ func downloadBundleForTesting(
4185
+ urlString: String,
4186
+ version: String,
4187
+ sessionKey: String,
4188
+ checksum: String,
4189
+ manifestEntries: [ManifestEntry]?
4190
+ ) throws -> BundleInfo {
4191
+ try downloadBundle(
4192
+ urlString: urlString,
4193
+ version: version,
4194
+ sessionKey: sessionKey,
4195
+ checksum: checksum,
4196
+ manifestEntries: manifestEntries
4197
+ )
4198
+ }
4199
+
4100
4200
  func setAutoUpdateModeForTesting(_ autoUpdateMode: String) {
4101
4201
  self.autoUpdateMode = Self.normalizedAutoUpdateMode(autoUpdateMode)
4102
4202
  self.autoUpdate = Self.isAutoUpdateModeEnabled(self.autoUpdateMode)
@@ -4432,6 +4532,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4432
4532
  if latestVersionName != "" && current.getVersionName() != latestVersionName {
4433
4533
  do {
4434
4534
  self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")
4535
+ if !self.implementation.publicKey.isEmpty && !CryptoCipher.isValidSessionKey(sessionKey) {
4536
+ self.logger.error("Public key present but no valid session key provided")
4537
+ self.implementation.sendStats(action: "session_key_required", versionName: latestVersionName)
4538
+ self.endBackGroundTaskWithNotif(
4539
+ msg: "Session key required when public key is present",
4540
+ latestVersionName: latestVersionName,
4541
+ current: current,
4542
+ plannedDirectUpdate: plannedDirectUpdate
4543
+ )
4544
+ return
4545
+ }
4435
4546
  var nextImpl = self.implementation.getBundleInfoByVersionName(version: latestVersionName)
4436
4547
  let needsDownload = nextImpl.map(Self.shouldRetryDownloadForExistingBundle) ?? true
4437
4548
  if needsDownload {
@@ -4449,6 +4560,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4449
4560
  if res.manifest != nil {
4450
4561
  nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
4451
4562
  } else {
4563
+ if res.checksum.isEmpty {
4564
+ self.logger.error("No checksum provided")
4565
+ self.implementation.sendStats(action: "checksum_required", versionName: latestVersionName)
4566
+ self.endBackGroundTaskWithNotif(
4567
+ msg: "Checksum required",
4568
+ latestVersionName: latestVersionName,
4569
+ current: current,
4570
+ plannedDirectUpdate: plannedDirectUpdate
4571
+ )
4572
+ return
4573
+ }
4452
4574
  nextImpl = try self.implementation.download(url: downloadUrl, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
4453
4575
  }
4454
4576
  }
@@ -4477,24 +4599,26 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
4477
4599
  self.endBackGroundTask()
4478
4600
  return
4479
4601
  }
4480
- res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
4481
- CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
4482
- CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
4483
- if res.checksum != "" && next.getChecksum() != res.checksum && res.manifest == nil {
4484
- self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
4485
- self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
4486
- let id = next.getId()
4487
- let resDel = self.implementation.delete(id: id)
4488
- if !resDel {
4489
- self.logger.error("Delete failed, id \(id) doesn't exist")
4602
+ if res.manifest == nil {
4603
+ res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
4604
+ CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
4605
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
4606
+ if next.getChecksum() != res.checksum {
4607
+ self.logger.error("Error checksum \(next.getChecksum()) \(res.checksum)")
4608
+ self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
4609
+ let id = next.getId()
4610
+ let resDel = self.implementation.delete(id: id)
4611
+ if !resDel {
4612
+ self.logger.error("Delete failed, id \(id) doesn't exist")
4613
+ }
4614
+ self.endBackGroundTaskWithNotif(
4615
+ msg: "Error checksum",
4616
+ latestVersionName: latestVersionName,
4617
+ current: current,
4618
+ plannedDirectUpdate: plannedDirectUpdate
4619
+ )
4620
+ return
4490
4621
  }
4491
- self.endBackGroundTaskWithNotif(
4492
- msg: "Error checksum",
4493
- latestVersionName: latestVersionName,
4494
- current: current,
4495
- plannedDirectUpdate: plannedDirectUpdate
4496
- )
4497
- return
4498
4622
  }
4499
4623
  if self.shouldBlockAutoUpdateForPreviewSession() {
4500
4624
  self.clearDownloadInProgressState()