@capgo/capacitor-updater 6.43.4 → 6.45.10

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.
@@ -44,7 +44,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
44
44
  CAPPluginMethod(name: "current", returnType: CAPPluginReturnPromise),
45
45
  CAPPluginMethod(name: "reload", returnType: CAPPluginReturnPromise),
46
46
  CAPPluginMethod(name: "notifyAppReady", returnType: CAPPluginReturnPromise),
47
- CAPPluginMethod(name: "setDelay", returnType: CAPPluginReturnPromise),
48
47
  CAPPluginMethod(name: "setMultiDelay", returnType: CAPPluginReturnPromise),
49
48
  CAPPluginMethod(name: "cancelDelay", returnType: CAPPluginReturnPromise),
50
49
  CAPPluginMethod(name: "getLatest", returnType: CAPPluginReturnPromise),
@@ -73,7 +72,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
73
72
  CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
74
73
  ]
75
74
  public var implementation = CapgoUpdater()
76
- private let pluginVersion: String = "6.43.4"
75
+ private let pluginVersion: String = "6.45.10"
77
76
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
78
77
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
79
78
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -103,7 +102,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
103
102
  private var autoSplashscreenTimeoutWorkItem: DispatchWorkItem?
104
103
  private var splashscreenLoaderView: UIActivityIndicatorView?
105
104
  private var splashscreenLoaderContainer: UIView?
105
+ private let splashscreenPluginName = "SplashScreen"
106
+ private let splashscreenRetryDelayMilliseconds = 100
107
+ private let splashscreenMaxRetries = 20
106
108
  private var autoSplashscreenTimedOut = false
109
+ private var splashscreenInvocationToken = 0
107
110
  private var autoDeleteFailed = false
108
111
  private var autoDeletePrevious = false
109
112
  var allowSetDefaultChannel = true
@@ -112,6 +115,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
112
115
  private var taskRunning = false
113
116
  private var periodCheckDelay = 0
114
117
  private let downloadLock = NSLock()
118
+ private let onLaunchDirectUpdateStateLock = NSLock()
115
119
  private var downloadInProgress = false
116
120
  private var downloadStartTime: Date?
117
121
  private let downloadTimeout: TimeInterval = 3600 // 1 hour timeout
@@ -226,12 +230,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
226
230
  periodCheckDelay = periodCheckDelayValue
227
231
  }
228
232
 
229
- implementation.privateKey = getConfig().getString("privateKey", "")!
230
- implementation.publicKey = getConfig().getString("publicKey", "")!
231
- if !implementation.privateKey.isEmpty {
232
- implementation.hasOldPrivateKeyPropertyInConfig = true
233
- }
233
+ implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
234
234
  implementation.notifyDownloadRaw = notifyDownload
235
+ implementation.notifyListeners = { [weak self] eventName, data in
236
+ self?.notifyListeners(eventName, data: data)
237
+ }
235
238
  implementation.pluginVersion = self.pluginVersion
236
239
 
237
240
  // Set logger for shared classes
@@ -276,11 +279,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
276
279
  }
277
280
  self.implementation.autoReset()
278
281
 
279
- // Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
282
+ // Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
280
283
  self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
281
284
 
282
285
  if resetWhenUpdate {
283
- self.cleanupObsoleteVersions()
286
+ let didResetCurrentBundle = self.resetCurrentBundleForNativeBuildChangeIfNeeded()
287
+ self.cleanupObsoleteVersions(didResetCurrentBundle: didResetCurrentBundle)
284
288
  }
285
289
 
286
290
  // Load the server
@@ -396,7 +400,29 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
396
400
  semaphoreReady.signal()
397
401
  }
398
402
 
399
- private func cleanupObsoleteVersions() {
403
+ func storedNativeBuildVersion() -> String {
404
+ UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
405
+ }
406
+
407
+ func hasNativeBuildVersionChanged() -> Bool {
408
+ let previous = self.storedNativeBuildVersion()
409
+ return previous != "0" && self.currentBuildVersion != previous
410
+ }
411
+
412
+ @discardableResult
413
+ func resetCurrentBundleForNativeBuildChangeIfNeeded() -> Bool {
414
+ let previous = self.storedNativeBuildVersion()
415
+ guard previous != "0" && self.currentBuildVersion != previous else {
416
+ return false
417
+ }
418
+
419
+ // Reset startup state synchronously so initialLoad() boots from the builtin bundle.
420
+ self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting startup bundle to builtin.")
421
+ self.implementation.reset(isInternal: true)
422
+ return true
423
+ }
424
+
425
+ private func cleanupObsoleteVersions(didResetCurrentBundle: Bool = false) {
400
426
  cleanupThread = Thread {
401
427
  self.cleanupLock.lock()
402
428
  defer {
@@ -431,9 +457,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
431
457
  // 1. Write "LatestVersionNative" - this fixes the part 1 of this bug
432
458
  // 2. Compare both keys. If any is not equal to "currentBuildVersion", then revert to builtin version. This fixes the part 2 of this bug
433
459
 
434
- let previous = UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
460
+ let previous = self.storedNativeBuildVersion()
435
461
  if previous != "0" && self.currentBuildVersion != previous {
436
- _ = self._reset(toLastSuccessful: false)
462
+ if !didResetCurrentBundle {
463
+ self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting current bundle to builtin.")
464
+ self.implementation.reset(isInternal: true)
465
+ }
437
466
  let res = self.implementation.list()
438
467
  for version in res {
439
468
  // Check if thread was cancelled
@@ -652,47 +681,77 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
652
681
  }
653
682
  }
654
683
 
655
- public func _reload() -> Bool {
656
- guard let bridge = self.bridge else { return false }
657
- self.semaphoreUp()
684
+ private func currentReloadDestination() -> URL {
658
685
  let id = self.implementation.getCurrentBundleId()
659
- let dest: URL
660
686
  if BundleInfo.ID_BUILTIN == id {
661
- dest = Bundle.main.resourceURL!.appendingPathComponent("public")
687
+ return Bundle.main.resourceURL!.appendingPathComponent("public")
662
688
  } else {
663
- dest = self.implementation.getBundleDirectory(id: id)
689
+ return self.implementation.getBundleDirectory(id: id)
664
690
  }
691
+ }
692
+
693
+ private func applyCurrentBundleToBridge(_ bridge: CAPBridgeProtocol) -> Bool {
694
+ let id = self.implementation.getCurrentBundleId()
695
+ let dest = self.currentReloadDestination()
665
696
  logger.info("Reloading \(id)")
666
697
 
667
- let performReload: () -> Bool = {
668
- guard let vc = bridge.viewController as? CAPBridgeViewController else {
669
- self.logger.error("Cannot get viewController")
670
- return false
671
- }
672
- guard let capBridge = vc.bridge else {
673
- self.logger.error("Cannot get capBridge")
674
- return false
675
- }
676
- if self.keepUrlPathAfterReload {
677
- if let currentURL = vc.webView?.url {
678
- capBridge.setServerBasePath(dest.path)
679
- var urlComponents = URLComponents(url: capBridge.config.serverURL, resolvingAgainstBaseURL: false)!
680
- urlComponents.path = currentURL.path
681
- urlComponents.query = currentURL.query
682
- urlComponents.fragment = currentURL.fragment
683
- if let finalUrl = urlComponents.url {
684
- _ = vc.webView?.load(URLRequest(url: finalUrl))
685
- } else {
686
- self.logger.error("Unable to build final URL when keeping path after reload; falling back to base path")
687
- vc.setServerBasePath(path: dest.path)
688
- }
698
+ guard let vc = bridge.viewController as? CAPBridgeViewController else {
699
+ self.logger.error("Cannot get viewController")
700
+ return false
701
+ }
702
+ guard let capBridge = vc.bridge else {
703
+ self.logger.error("Cannot get capBridge")
704
+ return false
705
+ }
706
+ if self.keepUrlPathAfterReload {
707
+ if let currentURL = vc.webView?.url {
708
+ capBridge.setServerBasePath(dest.path)
709
+ var urlComponents = URLComponents(url: capBridge.config.serverURL, resolvingAgainstBaseURL: false)!
710
+ urlComponents.path = currentURL.path
711
+ urlComponents.query = currentURL.query
712
+ urlComponents.fragment = currentURL.fragment
713
+ if let finalUrl = urlComponents.url {
714
+ _ = vc.webView?.load(URLRequest(url: finalUrl))
689
715
  } else {
690
- self.logger.error("vc.webView?.url is null? Falling back to base path reload.")
716
+ self.logger.error("Unable to build final URL when keeping path after reload; falling back to base path")
691
717
  vc.setServerBasePath(path: dest.path)
692
718
  }
693
719
  } else {
720
+ self.logger.error("vc.webView?.url is null? Falling back to base path reload.")
694
721
  vc.setServerBasePath(path: dest.path)
695
722
  }
723
+ } else {
724
+ vc.setServerBasePath(path: dest.path)
725
+ }
726
+ return true
727
+ }
728
+
729
+ func restoreLiveBundleStateAfterFailedReload() {
730
+ guard let bridge = self.bridge else {
731
+ return
732
+ }
733
+
734
+ let restoreLiveState = {
735
+ _ = self.applyCurrentBundleToBridge(bridge)
736
+ }
737
+
738
+ if Thread.isMainThread {
739
+ restoreLiveState()
740
+ } else {
741
+ DispatchQueue.main.sync {
742
+ restoreLiveState()
743
+ }
744
+ }
745
+ }
746
+
747
+ public func _reload() -> Bool {
748
+ guard let bridge = self.bridge else { return false }
749
+ self.semaphoreUp()
750
+
751
+ let performReload: () -> Bool = {
752
+ guard self.applyCurrentBundleToBridge(bridge) else {
753
+ return false
754
+ }
696
755
  self.checkAppReady()
697
756
  self.notifyListeners("appReloaded", data: [:])
698
757
  return true
@@ -710,6 +769,38 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
710
769
  }
711
770
 
712
771
  @objc func reload(_ call: CAPPluginCall) {
772
+ let current: BundleInfo = self.implementation.getCurrentBundle()
773
+ let next: BundleInfo? = self.implementation.getNextBundle()
774
+
775
+ if let next = next, !next.isErrorStatus(), next.getId() != current.getId() {
776
+ let previousState = self.implementation.captureResetState()
777
+ let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
778
+ logger.info("Applying pending bundle before reload: \(next.toString())")
779
+ let didApplyPendingBundle: Bool
780
+ if next.isBuiltin() {
781
+ self.implementation.prepareResetStateForTransition()
782
+ didApplyPendingBundle = true
783
+ } else {
784
+ didApplyPendingBundle = self.implementation.stagePendingReload(bundle: next)
785
+ }
786
+ if didApplyPendingBundle && self._reload() {
787
+ if next.isBuiltin() {
788
+ self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: false)
789
+ } else {
790
+ self.implementation.finalizePendingReload(bundle: next, previousBundleName: previousBundleName)
791
+ }
792
+ self.notifyBundleSet(next)
793
+ _ = self.implementation.setNextBundle(next: Optional<String>.none)
794
+ call.resolve()
795
+ return
796
+ }
797
+ self.implementation.restoreResetState(previousState)
798
+ self.restoreLiveBundleStateAfterFailedReload()
799
+ logger.error("Reload failed after applying pending bundle: \(next.toString())")
800
+ call.reject("Reload failed after applying pending bundle")
801
+ return
802
+ }
803
+
713
804
  if self._reload() {
714
805
  call.resolve()
715
806
  } else {
@@ -744,8 +835,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
744
835
  if !res {
745
836
  logger.info("Bundle successfully set to: \(id) ")
746
837
  call.reject("Update failed, id \(id) doesn't exist")
838
+ } else if !self._reload() {
839
+ call.reject("Reload failed after setting bundle \(id)")
747
840
  } else {
748
- self.reload(call)
841
+ self.notifyBundleSet(self.implementation.getBundleInfo(id: id))
842
+ call.resolve()
749
843
  }
750
844
  }
751
845
 
@@ -808,12 +902,32 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
808
902
 
809
903
  @objc func getLatest(_ call: CAPPluginCall) {
810
904
  let channel = call.getString("channel")
811
- DispatchQueue.global(qos: .background).async {
905
+ runGetLatestWork {
812
906
  let res = self.implementation.getLatest(url: URL(string: self.updateUrl)!, channel: channel)
813
- if res.error != nil {
814
- call.reject( res.error!)
815
- } else if res.message != nil {
816
- call.reject( res.message!)
907
+ if let error = res.error, !error.isEmpty {
908
+ let responseKind = self.updateResponseKind(kind: res.kind)
909
+ res.kind = responseKind
910
+ if responseKind == "failed" {
911
+ call.reject(error)
912
+ } else {
913
+ if res.version.isEmpty {
914
+ res.version = self.implementation.getCurrentBundle().getVersionName()
915
+ }
916
+ call.resolve(res.toDict())
917
+ }
918
+ } else if let kind = res.kind, !kind.isEmpty {
919
+ let responseKind = self.updateResponseKind(kind: kind)
920
+ res.kind = responseKind
921
+ if responseKind != "failed" {
922
+ if res.version.isEmpty {
923
+ res.version = self.implementation.getCurrentBundle().getVersionName()
924
+ }
925
+ call.resolve(res.toDict())
926
+ } else {
927
+ call.reject(res.message ?? "server did not provide a message")
928
+ }
929
+ } else if let message = res.message, !message.isEmpty {
930
+ call.reject(message)
817
931
  } else {
818
932
  call.resolve(res.toDict())
819
933
  }
@@ -930,32 +1044,90 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
930
1044
  call.resolve()
931
1045
  }
932
1046
 
933
- @objc func _reset(toLastSuccessful: Bool) -> Bool {
934
- guard let bridge = self.bridge else { return false }
1047
+ @objc func _reset(toLastSuccessful: Bool, usePendingBundle: Bool) -> Bool {
1048
+ self.performReset(toLastSuccessful: toLastSuccessful, usePendingBundle: usePendingBundle, isInternal: false)
1049
+ }
935
1050
 
936
- if (bridge.viewController as? CAPBridgeViewController) != nil {
937
- let fallback: BundleInfo = self.implementation.getFallbackBundle()
1051
+ func performReset(toLastSuccessful: Bool, usePendingBundle: Bool, isInternal: Bool) -> Bool {
1052
+ guard self.canPerformResetTransition() else { return false }
938
1053
 
939
- // If developer wants to reset to the last successful bundle, and that bundle is not
940
- // the built-in bundle, set it as the bundle to use and reload.
941
- if toLastSuccessful && !fallback.isBuiltin() {
942
- logger.info("Resetting to: \(fallback.toString())")
943
- return self.implementation.set(bundle: fallback) && self._reload()
944
- }
1054
+ let fallback: BundleInfo = self.implementation.getFallbackBundle()
1055
+ let pending: BundleInfo? = self.implementation.getNextBundle()
1056
+ let previousState = self.implementation.captureResetState()
1057
+ let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
945
1058
 
946
- logger.info("Resetting to builtin version")
1059
+ if usePendingBundle {
1060
+ guard let pending = pending, !pending.isErrorStatus() else {
1061
+ logger.error("No pending bundle available to reset to")
1062
+ return false
1063
+ }
1064
+ guard self.implementation.canSet(bundle: pending) else {
1065
+ logger.error("Pending bundle is not installable")
1066
+ return false
1067
+ }
1068
+ self.implementation.prepareResetStateForTransition()
1069
+ logger.info("Resetting to pending bundle: \(pending.toString())")
1070
+ let didApplyPendingBundle: Bool
1071
+ if pending.isBuiltin() {
1072
+ didApplyPendingBundle = true
1073
+ } else {
1074
+ didApplyPendingBundle = self.implementation.set(bundle: pending)
1075
+ }
1076
+ if didApplyPendingBundle && self._reload() {
1077
+ self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
1078
+ self.notifyBundleSet(pending)
1079
+ _ = self.implementation.setNextBundle(next: Optional<String>.none)
1080
+ return true
1081
+ }
1082
+ self.implementation.restoreResetState(previousState)
1083
+ self.restoreLiveBundleStateAfterFailedReload()
1084
+ return false
1085
+ }
947
1086
 
948
- // Otherwise, reset back to the built-in bundle and reload.
949
- self.implementation.reset()
950
- return self._reload()
1087
+ // If developer wants to reset to the last successful bundle, and that bundle is not
1088
+ // the built-in bundle, set it as the bundle to use and reload.
1089
+ if toLastSuccessful && !fallback.isBuiltin() {
1090
+ if self.implementation.canSet(bundle: fallback) {
1091
+ self.implementation.prepareResetStateForTransition()
1092
+ logger.info("Resetting to: \(fallback.toString())")
1093
+ if self.implementation.set(bundle: fallback) && self._reload() {
1094
+ self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
1095
+ self.notifyBundleSet(fallback)
1096
+ return true
1097
+ }
1098
+ if !isInternal {
1099
+ self.implementation.restoreResetState(previousState)
1100
+ self.restoreLiveBundleStateAfterFailedReload()
1101
+ return false
1102
+ }
1103
+ logger.warn("Fallback reload failed during internal reset, resetting to builtin instead")
1104
+ } else {
1105
+ logger.warn("Fallback bundle is not installable, resetting to builtin instead")
1106
+ }
951
1107
  }
952
1108
 
1109
+ self.implementation.prepareResetStateForTransition()
1110
+ logger.info("Resetting to builtin version")
1111
+ if self._reload() {
1112
+ self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: isInternal)
1113
+ return true
1114
+ }
1115
+ if !isInternal {
1116
+ self.implementation.restoreResetState(previousState)
1117
+ self.restoreLiveBundleStateAfterFailedReload()
1118
+ }
953
1119
  return false
954
1120
  }
955
1121
 
1122
+ func canPerformResetTransition() -> Bool {
1123
+ guard let bridge = self.bridge else { return false }
1124
+ return (bridge.viewController as? CAPBridgeViewController) != nil
1125
+ }
1126
+
956
1127
  @objc func reset(_ call: CAPPluginCall) {
957
1128
  let toLastSuccessful = call.getBool("toLastSuccessful") ?? false
958
- if self._reset(toLastSuccessful: toLastSuccessful) {
1129
+ let usePendingBundle = call.getBool("usePendingBundle") ?? false
1130
+ if self._reset(toLastSuccessful: toLastSuccessful, usePendingBundle: usePendingBundle) {
959
1131
  call.resolve()
960
1132
  } else {
961
1133
  logger.error("Reset failed")
@@ -1074,7 +1246,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1074
1246
  self.persistLastFailedBundle(current)
1075
1247
  self.implementation.sendStats(action: "update_fail", versionName: current.getVersionName())
1076
1248
  self.implementation.setError(bundle: current)
1077
- _ = self._reset(toLastSuccessful: true)
1249
+ _ = self.performReset(toLastSuccessful: true, usePendingBundle: false, isInternal: true)
1078
1250
  if self.autoDeleteFailed && !current.isBuiltin() {
1079
1251
  logger.info("Deleting failing bundle: \(current.toString())")
1080
1252
  let res = self.implementation.delete(id: current.getId(), removeInfo: false)
@@ -1099,6 +1271,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1099
1271
  self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
1100
1272
  }
1101
1273
 
1274
+ private func notifyBundleSet(_ bundle: BundleInfo) {
1275
+ self.notifyListeners("set", data: ["bundle": bundle.toJSON()], retainUntilConsumed: true)
1276
+ }
1277
+
1102
1278
  func sendReadyToJs(current: BundleInfo, msg: String) {
1103
1279
  logger.info("sendReadyToJs")
1104
1280
  DispatchQueue.global().async {
@@ -1126,32 +1302,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1126
1302
  private func performHideSplashscreen() {
1127
1303
  self.cancelSplashscreenTimeout()
1128
1304
  self.removeSplashscreenLoader()
1129
-
1130
- guard let bridge = self.bridge else {
1131
- self.logger.warn("Bridge not available for hiding splashscreen with autoSplashscreen")
1132
- return
1133
- }
1134
-
1135
- // Create a plugin call for the hide method
1136
- let call = CAPPluginCall(callbackId: "autoHideSplashscreen", options: [:], success: { (_, _) in
1137
- self.logger.info("Splashscreen hidden automatically")
1138
- }, error: { (_) in
1139
- self.logger.error("Failed to auto-hide splashscreen")
1140
- })
1141
-
1142
- // Try to call the SplashScreen hide method directly through the bridge
1143
- if let splashScreenPlugin = bridge.plugin(withName: "SplashScreen") {
1144
- // Use runtime method invocation to call hide method
1145
- let selector = NSSelectorFromString("hide:")
1146
- if splashScreenPlugin.responds(to: selector) {
1147
- _ = splashScreenPlugin.perform(selector, with: call)
1148
- self.logger.info("Called SplashScreen hide method")
1149
- } else {
1150
- self.logger.warn("autoSplashscreen: SplashScreen plugin does not respond to hide: method. Make sure @capacitor/splash-screen plugin is properly installed.")
1151
- }
1152
- } else {
1153
- self.logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.")
1154
- }
1305
+ self.splashscreenInvocationToken += 1
1306
+ self.invokeSplashscreenMethod(
1307
+ methodName: "hide",
1308
+ callbackId: "autoHideSplashscreen",
1309
+ options: self.splashscreenOptions(methodName: "hide"),
1310
+ retriesRemaining: self.splashscreenMaxRetries,
1311
+ requestToken: self.splashscreenInvocationToken
1312
+ )
1155
1313
  }
1156
1314
 
1157
1315
  private func showSplashscreen() {
@@ -1167,35 +1325,132 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1167
1325
  private func performShowSplashscreen() {
1168
1326
  self.cancelSplashscreenTimeout()
1169
1327
  self.autoSplashscreenTimedOut = false
1328
+ self.splashscreenInvocationToken += 1
1329
+ self.invokeSplashscreenMethod(
1330
+ methodName: "show",
1331
+ callbackId: "autoShowSplashscreen",
1332
+ options: self.splashscreenOptions(methodName: "show"),
1333
+ retriesRemaining: self.splashscreenMaxRetries,
1334
+ requestToken: self.splashscreenInvocationToken
1335
+ )
1336
+
1337
+ self.addSplashscreenLoaderIfNeeded()
1338
+ self.scheduleSplashscreenTimeout()
1339
+ }
1340
+
1341
+ private func splashscreenOptions(methodName: String) -> [String: Any] {
1342
+ methodName == "show" ? ["autoHide": false] : [:]
1343
+ }
1344
+
1345
+ private func splashscreenCompletedMessage(methodName: String) -> String {
1346
+ methodName == "show" ? "Splashscreen shown automatically" : "Splashscreen hidden automatically"
1347
+ }
1348
+
1349
+ func splashscreenOptionsForTesting(methodName: String) -> [String: Any] {
1350
+ self.splashscreenOptions(methodName: methodName)
1351
+ }
1352
+
1353
+ func isCurrentSplashscreenInvocationTokenForTesting(_ requestToken: Int) -> Bool {
1354
+ requestToken == self.splashscreenInvocationToken
1355
+ }
1356
+
1357
+ func advanceSplashscreenInvocationTokenForTesting() {
1358
+ self.splashscreenInvocationToken += 1
1359
+ }
1360
+
1361
+ private func makeSplashscreenCall(callbackId: String, options: [String: Any], methodName: String) -> CAPPluginCall {
1362
+ CAPPluginCall(callbackId: callbackId, options: options, success: { [weak self] (_, _) in
1363
+ guard let self = self else { return }
1364
+ self.logger.info(self.splashscreenCompletedMessage(methodName: methodName))
1365
+ }, error: { [weak self] (_) in
1366
+ guard let self = self else { return }
1367
+ self.logger.error("Failed to auto-\(methodName) splashscreen")
1368
+ })
1369
+ }
1370
+
1371
+ private func invokeSplashscreenMethod(
1372
+ methodName: String,
1373
+ callbackId: String,
1374
+ options: [String: Any],
1375
+ retriesRemaining: Int,
1376
+ requestToken: Int
1377
+ ) {
1378
+ guard requestToken == self.splashscreenInvocationToken else {
1379
+ return
1380
+ }
1170
1381
 
1171
1382
  guard let bridge = self.bridge else {
1172
- self.logger.warn("Bridge not available for showing splashscreen with autoSplashscreen")
1383
+ self.retrySplashscreenMethod(
1384
+ methodName: methodName,
1385
+ callbackId: callbackId,
1386
+ options: options,
1387
+ retriesRemaining: retriesRemaining,
1388
+ requestToken: requestToken,
1389
+ message: "Bridge not available for \(methodName == "show" ? "showing" : "hiding") splashscreen with autoSplashscreen"
1390
+ )
1173
1391
  return
1174
1392
  }
1175
1393
 
1176
- // Create a plugin call for the show method
1177
- let call = CAPPluginCall(callbackId: "autoShowSplashscreen", options: [:], success: { (_, _) in
1178
- self.logger.info("Splashscreen shown automatically")
1179
- }, error: { (_) in
1180
- self.logger.error("Failed to auto-show splashscreen")
1181
- })
1394
+ guard let splashScreenPlugin = bridge.plugin(withName: self.splashscreenPluginName) else {
1395
+ self.retrySplashscreenMethod(
1396
+ methodName: methodName,
1397
+ callbackId: callbackId,
1398
+ options: options,
1399
+ retriesRemaining: retriesRemaining,
1400
+ requestToken: requestToken,
1401
+ message: "autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin."
1402
+ )
1403
+ return
1404
+ }
1405
+
1406
+ let selector = NSSelectorFromString("\(methodName):")
1407
+ guard splashScreenPlugin.responds(to: selector) else {
1408
+ self.retrySplashscreenMethod(
1409
+ methodName: methodName,
1410
+ callbackId: callbackId,
1411
+ options: options,
1412
+ retriesRemaining: retriesRemaining,
1413
+ requestToken: requestToken,
1414
+ message: "autoSplashscreen: SplashScreen plugin does not respond to \(methodName): method. Make sure @capacitor/splash-screen plugin is properly installed."
1415
+ )
1416
+ return
1417
+ }
1418
+
1419
+ let call = self.makeSplashscreenCall(callbackId: callbackId, options: options, methodName: methodName)
1420
+ _ = splashScreenPlugin.perform(selector, with: call)
1421
+ self.logger.info("Called SplashScreen \(methodName) method")
1422
+ }
1182
1423
 
1183
- // Try to call the SplashScreen show method directly through the bridge
1184
- if let splashScreenPlugin = bridge.plugin(withName: "SplashScreen") {
1185
- // Use runtime method invocation to call show method
1186
- let selector = NSSelectorFromString("show:")
1187
- if splashScreenPlugin.responds(to: selector) {
1188
- _ = splashScreenPlugin.perform(selector, with: call)
1189
- self.logger.info("Called SplashScreen show method")
1424
+ private func retrySplashscreenMethod(
1425
+ methodName: String,
1426
+ callbackId: String,
1427
+ options: [String: Any],
1428
+ retriesRemaining: Int,
1429
+ requestToken: Int,
1430
+ message: String
1431
+ ) {
1432
+ guard retriesRemaining > 0 else {
1433
+ if methodName == "show" {
1434
+ self.logger.warn(message)
1190
1435
  } else {
1191
- self.logger.warn("autoSplashscreen: SplashScreen plugin does not respond to show: method. Make sure @capacitor/splash-screen plugin is properly installed.")
1436
+ self.logger.error(message)
1192
1437
  }
1193
- } else {
1194
- self.logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.")
1438
+ return
1195
1439
  }
1196
1440
 
1197
- self.addSplashscreenLoaderIfNeeded()
1198
- self.scheduleSplashscreenTimeout()
1441
+ self.logger.info("\(message). Retrying.")
1442
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.splashscreenRetryDelayMilliseconds)) { [weak self] in
1443
+ guard let self = self, requestToken == self.splashscreenInvocationToken else {
1444
+ return
1445
+ }
1446
+ self.invokeSplashscreenMethod(
1447
+ methodName: methodName,
1448
+ callbackId: callbackId,
1449
+ options: options,
1450
+ retriesRemaining: retriesRemaining - 1,
1451
+ requestToken: requestToken
1452
+ )
1453
+ }
1199
1454
  }
1200
1455
 
1201
1456
  private func addSplashscreenLoaderIfNeeded() {
@@ -1349,7 +1604,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1349
1604
  }
1350
1605
  return false
1351
1606
  case "onLaunch":
1352
- if !self.onLaunchDirectUpdateUsed {
1607
+ if !self.getOnLaunchDirectUpdateUsed() {
1353
1608
  return true
1354
1609
  }
1355
1610
  return false
@@ -1359,6 +1614,51 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1359
1614
  }
1360
1615
  }
1361
1616
 
1617
+ static func shouldConsumeOnLaunchDirectUpdate(directUpdateMode: String, plannedDirectUpdate: Bool) -> Bool {
1618
+ plannedDirectUpdate && directUpdateMode == "onLaunch"
1619
+ }
1620
+
1621
+ private func getOnLaunchDirectUpdateUsed() -> Bool {
1622
+ self.onLaunchDirectUpdateStateLock.lock()
1623
+ defer { self.onLaunchDirectUpdateStateLock.unlock() }
1624
+ return self.onLaunchDirectUpdateUsed
1625
+ }
1626
+
1627
+ private func setOnLaunchDirectUpdateUsed(_ used: Bool) {
1628
+ self.onLaunchDirectUpdateStateLock.lock()
1629
+ self.onLaunchDirectUpdateUsed = used
1630
+ self.onLaunchDirectUpdateStateLock.unlock()
1631
+ }
1632
+
1633
+ private func consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: Bool) {
1634
+ guard Self.shouldConsumeOnLaunchDirectUpdate(directUpdateMode: self.directUpdateMode, plannedDirectUpdate: plannedDirectUpdate) else {
1635
+ return
1636
+ }
1637
+
1638
+ self.setOnLaunchDirectUpdateUsed(true)
1639
+ }
1640
+
1641
+ func configureDirectUpdateModeForTesting(_ directUpdateMode: String, onLaunchDirectUpdateUsed: Bool = false) {
1642
+ self.directUpdateMode = directUpdateMode
1643
+ self.setOnLaunchDirectUpdateUsed(onLaunchDirectUpdateUsed)
1644
+ }
1645
+
1646
+ func setUpdateUrlForTesting(_ updateUrl: String) {
1647
+ self.updateUrl = updateUrl
1648
+ }
1649
+
1650
+ func setCurrentBuildVersionForTesting(_ currentBuildVersion: String) {
1651
+ self.currentBuildVersion = currentBuildVersion
1652
+ }
1653
+
1654
+ func shouldUseDirectUpdateForTesting() -> Bool {
1655
+ self.shouldUseDirectUpdate()
1656
+ }
1657
+
1658
+ var hasConsumedOnLaunchDirectUpdateForTesting: Bool {
1659
+ self.getOnLaunchDirectUpdateUsed()
1660
+ }
1661
+
1362
1662
  private func notifyBreakingEvents(version: String) {
1363
1663
  guard !version.isEmpty else {
1364
1664
  return
@@ -1368,11 +1668,58 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1368
1668
  self.notifyListeners("majorAvailable", data: payload)
1369
1669
  }
1370
1670
 
1671
+ private func updateResponseKind(kind: String?) -> String {
1672
+ if let kind, ["up_to_date", "blocked", "failed"].contains(kind) {
1673
+ return kind
1674
+ }
1675
+ return "failed"
1676
+ }
1677
+
1678
+ private func endBackgroundDownloadAfterLatestError(
1679
+ backendError: String,
1680
+ res: AppVersion,
1681
+ current: BundleInfo,
1682
+ plannedDirectUpdate: Bool
1683
+ ) {
1684
+ let statusCode = res.statusCode
1685
+ let responseKind = self.updateResponseKind(kind: res.kind)
1686
+ let responseMessage = res.message?.isEmpty == false ? res.message : nil
1687
+ let message = responseMessage ?? (backendError.isEmpty ? "server did not provide a message" : backendError)
1688
+ let latestVersionName = res.version.isEmpty ? current.getVersionName() : res.version
1689
+ self.notifyListeners("updateCheckResult", data: [
1690
+ "kind": responseKind,
1691
+ "error": backendError,
1692
+ "message": message,
1693
+ "statusCode": statusCode,
1694
+ "version": latestVersionName,
1695
+ "bundle": current.toJSON()
1696
+ ])
1697
+
1698
+ if responseKind == "up_to_date" {
1699
+ self.logger.info("No new version available")
1700
+ } else if responseKind == "blocked" {
1701
+ self.logger.info("Update check blocked with error: \(backendError)")
1702
+ } else {
1703
+ self.logger.error("getLatest failed with error: \(backendError)")
1704
+ }
1705
+
1706
+ let isFailure = responseKind == "failed"
1707
+ self.endBackGroundTaskWithNotif(
1708
+ msg: message,
1709
+ latestVersionName: latestVersionName,
1710
+ current: current,
1711
+ error: isFailure,
1712
+ plannedDirectUpdate: plannedDirectUpdate,
1713
+ sendStats: isFailure
1714
+ )
1715
+ }
1716
+
1371
1717
  func endBackGroundTaskWithNotif(
1372
1718
  msg: String,
1373
1719
  latestVersionName: String,
1374
1720
  current: BundleInfo,
1375
1721
  error: Bool = true,
1722
+ plannedDirectUpdate: Bool = false,
1376
1723
  failureAction: String = "download_fail",
1377
1724
  failureEvent: String = "downloadFailed",
1378
1725
  sendStats: Bool = true
@@ -1384,6 +1731,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1384
1731
  downloadInProgress = false
1385
1732
  downloadStartTime = nil
1386
1733
 
1734
+ self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
1735
+
1387
1736
  if error {
1388
1737
  if sendStats {
1389
1738
  self.implementation.sendStats(action: failureAction, versionName: current.getVersionName())
@@ -1418,6 +1767,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1418
1767
  return true
1419
1768
  }
1420
1769
 
1770
+ func runBackgroundDownloadWork(_ work: @escaping () -> Void) {
1771
+ DispatchQueue.global(qos: .background).async(execute: work)
1772
+ }
1773
+
1774
+ func runGetLatestWork(_ work: @escaping () -> Void) {
1775
+ DispatchQueue.global(qos: .background).async(execute: work)
1776
+ }
1777
+
1421
1778
  func backgroundDownload() {
1422
1779
  // Set download in progress flag (thread-safe)
1423
1780
  downloadLock.lock()
@@ -1437,7 +1794,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1437
1794
  return
1438
1795
  }
1439
1796
 
1440
- DispatchQueue.global(qos: .background).async {
1797
+ self.runBackgroundDownloadWork {
1441
1798
  // Wait for cleanup to complete before starting download
1442
1799
  self.waitForCleanupIfNeeded()
1443
1800
  self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Download Tasks") {
@@ -1449,16 +1806,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1449
1806
  let current = self.implementation.getCurrentBundle()
1450
1807
 
1451
1808
  // Handle network errors and other failures first
1452
- if let backendError = res.error, !backendError.isEmpty {
1453
- self.logger.error("getLatest failed with error: \(backendError)")
1454
- let statusCode = res.statusCode
1455
- let responseIsOk = statusCode >= 200 && statusCode < 300
1456
- self.endBackGroundTaskWithNotif(
1457
- msg: res.message ?? backendError,
1458
- latestVersionName: res.version,
1809
+ let backendError = res.error ?? ""
1810
+ let backendKind = res.kind ?? ""
1811
+ if !backendError.isEmpty || !backendKind.isEmpty {
1812
+ self.endBackgroundDownloadAfterLatestError(
1813
+ backendError: backendError,
1814
+ res: res,
1459
1815
  current: current,
1460
- error: true,
1461
- sendStats: !responseIsOk
1816
+ plannedDirectUpdate: plannedDirectUpdate
1462
1817
  )
1463
1818
  return
1464
1819
  }
@@ -1467,26 +1822,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1467
1822
  let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
1468
1823
  if directUpdateAllowed {
1469
1824
  self.logger.info("Direct update to builtin version")
1470
- if self.directUpdateMode == "onLaunch" {
1471
- self.onLaunchDirectUpdateUsed = true
1472
- self.directUpdate = false
1473
- }
1474
- _ = self._reset(toLastSuccessful: false)
1475
- self.endBackGroundTaskWithNotif(msg: "Updated to builtin version", latestVersionName: res.version, current: self.implementation.getCurrentBundle(), error: false)
1825
+ _ = self._reset(toLastSuccessful: false, usePendingBundle: false)
1826
+ self.endBackGroundTaskWithNotif(
1827
+ msg: "Updated to builtin version",
1828
+ latestVersionName: res.version,
1829
+ current: self.implementation.getCurrentBundle(),
1830
+ error: false,
1831
+ plannedDirectUpdate: plannedDirectUpdate
1832
+ )
1476
1833
  } else {
1477
1834
  if plannedDirectUpdate && !directUpdateAllowed {
1478
1835
  self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will apply later.")
1479
1836
  }
1480
1837
  self.logger.info("Setting next bundle to builtin")
1481
1838
  _ = self.implementation.setNextBundle(next: BundleInfo.ID_BUILTIN)
1482
- self.endBackGroundTaskWithNotif(msg: "Next update will be to builtin version", latestVersionName: res.version, current: current, error: false)
1839
+ self.endBackGroundTaskWithNotif(
1840
+ msg: "Next update will be to builtin version",
1841
+ latestVersionName: res.version,
1842
+ current: current,
1843
+ error: false,
1844
+ plannedDirectUpdate: plannedDirectUpdate
1845
+ )
1483
1846
  }
1484
1847
  return
1485
1848
  }
1486
1849
  let sessionKey = res.sessionKey ?? ""
1487
1850
  guard let downloadUrl = URL(string: res.url) else {
1488
1851
  self.logger.error("Error no url or wrong format")
1489
- self.endBackGroundTaskWithNotif(msg: "Error no url or wrong format", latestVersionName: res.version, current: current)
1852
+ self.endBackGroundTaskWithNotif(
1853
+ msg: "Error no url or wrong format",
1854
+ latestVersionName: res.version,
1855
+ current: current,
1856
+ plannedDirectUpdate: plannedDirectUpdate
1857
+ )
1490
1858
  return
1491
1859
  }
1492
1860
  let latestVersionName = res.version
@@ -1504,6 +1872,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1504
1872
  self.logger.error("Failed to delete failed bundle: \(nextImpl!.toString())")
1505
1873
  }
1506
1874
  }
1875
+ self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
1507
1876
  if res.manifest != nil {
1508
1877
  nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
1509
1878
  } else {
@@ -1512,12 +1881,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1512
1881
  }
1513
1882
  guard let next = nextImpl else {
1514
1883
  self.logger.error("Error downloading file")
1515
- self.endBackGroundTaskWithNotif(msg: "Error downloading file", latestVersionName: latestVersionName, current: current)
1884
+ self.endBackGroundTaskWithNotif(
1885
+ msg: "Error downloading file",
1886
+ latestVersionName: latestVersionName,
1887
+ current: current,
1888
+ plannedDirectUpdate: plannedDirectUpdate
1889
+ )
1516
1890
  return
1517
1891
  }
1518
1892
  if next.isErrorStatus() {
1519
1893
  self.logger.error("Latest bundle already exists and is in error state. Aborting update.")
1520
- self.endBackGroundTaskWithNotif(msg: "Latest version is in error state. Aborting update.", latestVersionName: latestVersionName, current: current)
1894
+ self.endBackGroundTaskWithNotif(
1895
+ msg: "Latest version is in error state. Aborting update.",
1896
+ latestVersionName: latestVersionName,
1897
+ current: current,
1898
+ plannedDirectUpdate: plannedDirectUpdate
1899
+ )
1521
1900
  return
1522
1901
  }
1523
1902
  res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
@@ -1531,7 +1910,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1531
1910
  if !resDel {
1532
1911
  self.logger.error("Delete failed, id \(id) doesn't exist")
1533
1912
  }
1534
- self.endBackGroundTaskWithNotif(msg: "Error checksum", latestVersionName: latestVersionName, current: current)
1913
+ self.endBackGroundTaskWithNotif(
1914
+ msg: "Error checksum",
1915
+ latestVersionName: latestVersionName,
1916
+ current: current,
1917
+ plannedDirectUpdate: plannedDirectUpdate
1918
+ )
1535
1919
  return
1536
1920
  }
1537
1921
  let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
@@ -1544,34 +1928,67 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1544
1928
  }
1545
1929
  if !delayConditionList.isEmpty {
1546
1930
  self.logger.info("Update delayed until delay conditions met")
1547
- self.endBackGroundTaskWithNotif(msg: "Update delayed until delay conditions met", latestVersionName: latestVersionName, current: next, error: false)
1931
+ self.endBackGroundTaskWithNotif(
1932
+ msg: "Update delayed until delay conditions met",
1933
+ latestVersionName: latestVersionName,
1934
+ current: next,
1935
+ error: false,
1936
+ plannedDirectUpdate: plannedDirectUpdate
1937
+ )
1548
1938
  return
1549
1939
  }
1550
- if self.directUpdateMode == "onLaunch" {
1551
- self.onLaunchDirectUpdateUsed = true
1552
- self.directUpdate = false
1940
+ if self.implementation.set(bundle: next) && self._reload() {
1941
+ self.notifyBundleSet(next)
1942
+ self.endBackGroundTaskWithNotif(
1943
+ msg: "update installed",
1944
+ latestVersionName: latestVersionName,
1945
+ current: next,
1946
+ error: false,
1947
+ plannedDirectUpdate: plannedDirectUpdate
1948
+ )
1949
+ } else {
1950
+ self.endBackGroundTaskWithNotif(
1951
+ msg: "Update install failed",
1952
+ latestVersionName: latestVersionName,
1953
+ current: next,
1954
+ plannedDirectUpdate: plannedDirectUpdate
1955
+ )
1553
1956
  }
1554
- _ = self.implementation.set(bundle: next)
1555
- _ = self._reload()
1556
- self.endBackGroundTaskWithNotif(msg: "update installed", latestVersionName: latestVersionName, current: next, error: false)
1557
1957
  } else {
1558
1958
  if plannedDirectUpdate && !directUpdateAllowed {
1559
1959
  self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
1560
1960
  }
1561
1961
  self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
1562
1962
  _ = self.implementation.setNextBundle(next: next.getId())
1563
- self.endBackGroundTaskWithNotif(msg: "update downloaded, will install next background", latestVersionName: latestVersionName, current: current, error: false)
1963
+ self.endBackGroundTaskWithNotif(
1964
+ msg: "update downloaded, will install next background",
1965
+ latestVersionName: latestVersionName,
1966
+ current: current,
1967
+ error: false,
1968
+ plannedDirectUpdate: plannedDirectUpdate
1969
+ )
1564
1970
  }
1565
1971
  return
1566
1972
  } catch {
1567
1973
  self.logger.error("Error downloading file \(error.localizedDescription)")
1568
1974
  let current: BundleInfo = self.implementation.getCurrentBundle()
1569
- self.endBackGroundTaskWithNotif(msg: "Error downloading file", latestVersionName: latestVersionName, current: current)
1975
+ self.endBackGroundTaskWithNotif(
1976
+ msg: "Error downloading file",
1977
+ latestVersionName: latestVersionName,
1978
+ current: current,
1979
+ plannedDirectUpdate: plannedDirectUpdate
1980
+ )
1570
1981
  return
1571
1982
  }
1572
1983
  } else {
1573
1984
  self.logger.info("No need to update, \(current.getId()) is the latest bundle.")
1574
- self.endBackGroundTaskWithNotif(msg: "No need to update, \(current.getId()) is the latest bundle.", latestVersionName: latestVersionName, current: current, error: false)
1985
+ self.endBackGroundTaskWithNotif(
1986
+ msg: "No need to update, \(current.getId()) is the latest bundle.",
1987
+ latestVersionName: latestVersionName,
1988
+ current: current,
1989
+ error: false,
1990
+ plannedDirectUpdate: plannedDirectUpdate
1991
+ )
1575
1992
  return
1576
1993
  }
1577
1994
  }
@@ -1595,6 +2012,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1595
2012
  logger.info("Next bundle is: \(next!.toString())")
1596
2013
  if self.implementation.set(bundle: next!) && self._reload() {
1597
2014
  logger.info("Updated to bundle: \(next!.toString())")
2015
+ self.notifyBundleSet(next!)
1598
2016
  _ = self.implementation.setNextBundle(next: Optional<String>.none)
1599
2017
  } else {
1600
2018
  logger.error("Update to bundle: \(next!.toString()) Failed!")