@capgo/capacitor-updater 7.43.3 → 7.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 = "7.43.3"
75
+ private let pluginVersion: String = "7.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
@@ -228,6 +232,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
228
232
 
229
233
  implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
230
234
  implementation.notifyDownloadRaw = notifyDownload
235
+ implementation.notifyListeners = { [weak self] eventName, data in
236
+ self?.notifyListeners(eventName, data: data)
237
+ }
231
238
  implementation.pluginVersion = self.pluginVersion
232
239
 
233
240
  // Set logger for shared classes
@@ -272,11 +279,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
272
279
  }
273
280
  self.implementation.autoReset()
274
281
 
275
- // 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.
276
283
  self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
277
284
 
278
285
  if resetWhenUpdate {
279
- self.cleanupObsoleteVersions()
286
+ let didResetCurrentBundle = self.resetCurrentBundleForNativeBuildChangeIfNeeded()
287
+ self.cleanupObsoleteVersions(didResetCurrentBundle: didResetCurrentBundle)
280
288
  }
281
289
 
282
290
  // Load the server
@@ -392,7 +400,29 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
392
400
  semaphoreReady.signal()
393
401
  }
394
402
 
395
- 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) {
396
426
  cleanupThread = Thread {
397
427
  self.cleanupLock.lock()
398
428
  defer {
@@ -427,9 +457,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
427
457
  // 1. Write "LatestVersionNative" - this fixes the part 1 of this bug
428
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
429
459
 
430
- let previous = UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
460
+ let previous = self.storedNativeBuildVersion()
431
461
  if previous != "0" && self.currentBuildVersion != previous {
432
- _ = 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
+ }
433
466
  let res = self.implementation.list()
434
467
  for version in res {
435
468
  // Check if thread was cancelled
@@ -648,47 +681,77 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
648
681
  }
649
682
  }
650
683
 
651
- public func _reload() -> Bool {
652
- guard let bridge = self.bridge else { return false }
653
- self.semaphoreUp()
684
+ private func currentReloadDestination() -> URL {
654
685
  let id = self.implementation.getCurrentBundleId()
655
- let dest: URL
656
686
  if BundleInfo.ID_BUILTIN == id {
657
- dest = Bundle.main.resourceURL!.appendingPathComponent("public")
687
+ return Bundle.main.resourceURL!.appendingPathComponent("public")
658
688
  } else {
659
- dest = self.implementation.getBundleDirectory(id: id)
689
+ return self.implementation.getBundleDirectory(id: id)
660
690
  }
691
+ }
692
+
693
+ private func applyCurrentBundleToBridge(_ bridge: CAPBridgeProtocol) -> Bool {
694
+ let id = self.implementation.getCurrentBundleId()
695
+ let dest = self.currentReloadDestination()
661
696
  logger.info("Reloading \(id)")
662
697
 
663
- let performReload: () -> Bool = {
664
- guard let vc = bridge.viewController as? CAPBridgeViewController else {
665
- self.logger.error("Cannot get viewController")
666
- return false
667
- }
668
- guard let capBridge = vc.bridge else {
669
- self.logger.error("Cannot get capBridge")
670
- return false
671
- }
672
- if self.keepUrlPathAfterReload {
673
- if let currentURL = vc.webView?.url {
674
- capBridge.setServerBasePath(dest.path)
675
- var urlComponents = URLComponents(url: capBridge.config.serverURL, resolvingAgainstBaseURL: false)!
676
- urlComponents.path = currentURL.path
677
- urlComponents.query = currentURL.query
678
- urlComponents.fragment = currentURL.fragment
679
- if let finalUrl = urlComponents.url {
680
- _ = vc.webView?.load(URLRequest(url: finalUrl))
681
- } else {
682
- self.logger.error("Unable to build final URL when keeping path after reload; falling back to base path")
683
- vc.setServerBasePath(path: dest.path)
684
- }
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))
685
715
  } else {
686
- 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")
687
717
  vc.setServerBasePath(path: dest.path)
688
718
  }
689
719
  } else {
720
+ self.logger.error("vc.webView?.url is null? Falling back to base path reload.")
690
721
  vc.setServerBasePath(path: dest.path)
691
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
+ }
692
755
  self.checkAppReady()
693
756
  self.notifyListeners("appReloaded", data: [:])
694
757
  return true
@@ -706,6 +769,38 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
706
769
  }
707
770
 
708
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
+
709
804
  if self._reload() {
710
805
  call.resolve()
711
806
  } else {
@@ -740,8 +835,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
740
835
  if !res {
741
836
  logger.info("Bundle successfully set to: \(id) ")
742
837
  call.reject("Update failed, id \(id) doesn't exist")
838
+ } else if !self._reload() {
839
+ call.reject("Reload failed after setting bundle \(id)")
743
840
  } else {
744
- self.reload(call)
841
+ self.notifyBundleSet(self.implementation.getBundleInfo(id: id))
842
+ call.resolve()
745
843
  }
746
844
  }
747
845
 
@@ -804,12 +902,32 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
804
902
 
805
903
  @objc func getLatest(_ call: CAPPluginCall) {
806
904
  let channel = call.getString("channel")
807
- DispatchQueue.global(qos: .background).async {
905
+ runGetLatestWork {
808
906
  let res = self.implementation.getLatest(url: URL(string: self.updateUrl)!, channel: channel)
809
- if res.error != nil {
810
- call.reject( res.error!)
811
- } else if res.message != nil {
812
- 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)
813
931
  } else {
814
932
  call.resolve(res.toDict())
815
933
  }
@@ -926,32 +1044,90 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
926
1044
  call.resolve()
927
1045
  }
928
1046
 
929
- @objc func _reset(toLastSuccessful: Bool) -> Bool {
930
- 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
+ }
931
1050
 
932
- if (bridge.viewController as? CAPBridgeViewController) != nil {
933
- let fallback: BundleInfo = self.implementation.getFallbackBundle()
1051
+ func performReset(toLastSuccessful: Bool, usePendingBundle: Bool, isInternal: Bool) -> Bool {
1052
+ guard self.canPerformResetTransition() else { return false }
934
1053
 
935
- // If developer wants to reset to the last successful bundle, and that bundle is not
936
- // the built-in bundle, set it as the bundle to use and reload.
937
- if toLastSuccessful && !fallback.isBuiltin() {
938
- logger.info("Resetting to: \(fallback.toString())")
939
- return self.implementation.set(bundle: fallback) && self._reload()
940
- }
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()
941
1058
 
942
- 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
+ }
943
1086
 
944
- // Otherwise, reset back to the built-in bundle and reload.
945
- self.implementation.reset()
946
- 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
+ }
947
1107
  }
948
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
+ }
949
1119
  return false
950
1120
  }
951
1121
 
1122
+ func canPerformResetTransition() -> Bool {
1123
+ guard let bridge = self.bridge else { return false }
1124
+ return (bridge.viewController as? CAPBridgeViewController) != nil
1125
+ }
1126
+
952
1127
  @objc func reset(_ call: CAPPluginCall) {
953
1128
  let toLastSuccessful = call.getBool("toLastSuccessful") ?? false
954
- if self._reset(toLastSuccessful: toLastSuccessful) {
1129
+ let usePendingBundle = call.getBool("usePendingBundle") ?? false
1130
+ if self._reset(toLastSuccessful: toLastSuccessful, usePendingBundle: usePendingBundle) {
955
1131
  call.resolve()
956
1132
  } else {
957
1133
  logger.error("Reset failed")
@@ -1070,7 +1246,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1070
1246
  self.persistLastFailedBundle(current)
1071
1247
  self.implementation.sendStats(action: "update_fail", versionName: current.getVersionName())
1072
1248
  self.implementation.setError(bundle: current)
1073
- _ = self._reset(toLastSuccessful: true)
1249
+ _ = self.performReset(toLastSuccessful: true, usePendingBundle: false, isInternal: true)
1074
1250
  if self.autoDeleteFailed && !current.isBuiltin() {
1075
1251
  logger.info("Deleting failing bundle: \(current.toString())")
1076
1252
  let res = self.implementation.delete(id: current.getId(), removeInfo: false)
@@ -1095,6 +1271,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1095
1271
  self.backgroundTaskID = UIBackgroundTaskIdentifier.invalid
1096
1272
  }
1097
1273
 
1274
+ private func notifyBundleSet(_ bundle: BundleInfo) {
1275
+ self.notifyListeners("set", data: ["bundle": bundle.toJSON()], retainUntilConsumed: true)
1276
+ }
1277
+
1098
1278
  func sendReadyToJs(current: BundleInfo, msg: String) {
1099
1279
  logger.info("sendReadyToJs")
1100
1280
  DispatchQueue.global().async {
@@ -1122,32 +1302,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1122
1302
  private func performHideSplashscreen() {
1123
1303
  self.cancelSplashscreenTimeout()
1124
1304
  self.removeSplashscreenLoader()
1125
-
1126
- guard let bridge = self.bridge else {
1127
- self.logger.warn("Bridge not available for hiding splashscreen with autoSplashscreen")
1128
- return
1129
- }
1130
-
1131
- // Create a plugin call for the hide method
1132
- let call = CAPPluginCall(callbackId: "autoHideSplashscreen", options: [:], success: { (_, _) in
1133
- self.logger.info("Splashscreen hidden automatically")
1134
- }, error: { (_) in
1135
- self.logger.error("Failed to auto-hide splashscreen")
1136
- })
1137
-
1138
- // Try to call the SplashScreen hide method directly through the bridge
1139
- if let splashScreenPlugin = bridge.plugin(withName: "SplashScreen") {
1140
- // Use runtime method invocation to call hide method
1141
- let selector = NSSelectorFromString("hide:")
1142
- if splashScreenPlugin.responds(to: selector) {
1143
- _ = splashScreenPlugin.perform(selector, with: call)
1144
- self.logger.info("Called SplashScreen hide method")
1145
- } else {
1146
- self.logger.warn("autoSplashscreen: SplashScreen plugin does not respond to hide: method. Make sure @capacitor/splash-screen plugin is properly installed.")
1147
- }
1148
- } else {
1149
- self.logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.")
1150
- }
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
+ )
1151
1313
  }
1152
1314
 
1153
1315
  private func showSplashscreen() {
@@ -1163,35 +1325,132 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1163
1325
  private func performShowSplashscreen() {
1164
1326
  self.cancelSplashscreenTimeout()
1165
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
+ }
1166
1381
 
1167
1382
  guard let bridge = self.bridge else {
1168
- 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
+ )
1169
1391
  return
1170
1392
  }
1171
1393
 
1172
- // Create a plugin call for the show method
1173
- let call = CAPPluginCall(callbackId: "autoShowSplashscreen", options: [:], success: { (_, _) in
1174
- self.logger.info("Splashscreen shown automatically")
1175
- }, error: { (_) in
1176
- self.logger.error("Failed to auto-show splashscreen")
1177
- })
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
+ }
1178
1423
 
1179
- // Try to call the SplashScreen show method directly through the bridge
1180
- if let splashScreenPlugin = bridge.plugin(withName: "SplashScreen") {
1181
- // Use runtime method invocation to call show method
1182
- let selector = NSSelectorFromString("show:")
1183
- if splashScreenPlugin.responds(to: selector) {
1184
- _ = splashScreenPlugin.perform(selector, with: call)
1185
- 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)
1186
1435
  } else {
1187
- 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)
1188
1437
  }
1189
- } else {
1190
- self.logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.")
1438
+ return
1191
1439
  }
1192
1440
 
1193
- self.addSplashscreenLoaderIfNeeded()
1194
- 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
+ }
1195
1454
  }
1196
1455
 
1197
1456
  private func addSplashscreenLoaderIfNeeded() {
@@ -1345,7 +1604,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1345
1604
  }
1346
1605
  return false
1347
1606
  case "onLaunch":
1348
- if !self.onLaunchDirectUpdateUsed {
1607
+ if !self.getOnLaunchDirectUpdateUsed() {
1349
1608
  return true
1350
1609
  }
1351
1610
  return false
@@ -1355,6 +1614,51 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1355
1614
  }
1356
1615
  }
1357
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
+
1358
1662
  private func notifyBreakingEvents(version: String) {
1359
1663
  guard !version.isEmpty else {
1360
1664
  return
@@ -1364,11 +1668,58 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1364
1668
  self.notifyListeners("majorAvailable", data: payload)
1365
1669
  }
1366
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
+
1367
1717
  func endBackGroundTaskWithNotif(
1368
1718
  msg: String,
1369
1719
  latestVersionName: String,
1370
1720
  current: BundleInfo,
1371
1721
  error: Bool = true,
1722
+ plannedDirectUpdate: Bool = false,
1372
1723
  failureAction: String = "download_fail",
1373
1724
  failureEvent: String = "downloadFailed",
1374
1725
  sendStats: Bool = true
@@ -1380,6 +1731,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1380
1731
  downloadInProgress = false
1381
1732
  downloadStartTime = nil
1382
1733
 
1734
+ self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
1735
+
1383
1736
  if error {
1384
1737
  if sendStats {
1385
1738
  self.implementation.sendStats(action: failureAction, versionName: current.getVersionName())
@@ -1414,6 +1767,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1414
1767
  return true
1415
1768
  }
1416
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
+
1417
1778
  func backgroundDownload() {
1418
1779
  // Set download in progress flag (thread-safe)
1419
1780
  downloadLock.lock()
@@ -1433,7 +1794,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1433
1794
  return
1434
1795
  }
1435
1796
 
1436
- DispatchQueue.global(qos: .background).async {
1797
+ self.runBackgroundDownloadWork {
1437
1798
  // Wait for cleanup to complete before starting download
1438
1799
  self.waitForCleanupIfNeeded()
1439
1800
  self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Download Tasks") {
@@ -1445,16 +1806,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1445
1806
  let current = self.implementation.getCurrentBundle()
1446
1807
 
1447
1808
  // Handle network errors and other failures first
1448
- if let backendError = res.error, !backendError.isEmpty {
1449
- self.logger.error("getLatest failed with error: \(backendError)")
1450
- let statusCode = res.statusCode
1451
- let responseIsOk = statusCode >= 200 && statusCode < 300
1452
- self.endBackGroundTaskWithNotif(
1453
- msg: res.message ?? backendError,
1454
- 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,
1455
1815
  current: current,
1456
- error: true,
1457
- sendStats: !responseIsOk
1816
+ plannedDirectUpdate: plannedDirectUpdate
1458
1817
  )
1459
1818
  return
1460
1819
  }
@@ -1463,26 +1822,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1463
1822
  let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
1464
1823
  if directUpdateAllowed {
1465
1824
  self.logger.info("Direct update to builtin version")
1466
- if self.directUpdateMode == "onLaunch" {
1467
- self.onLaunchDirectUpdateUsed = true
1468
- self.directUpdate = false
1469
- }
1470
- _ = self._reset(toLastSuccessful: false)
1471
- 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
+ )
1472
1833
  } else {
1473
1834
  if plannedDirectUpdate && !directUpdateAllowed {
1474
1835
  self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will apply later.")
1475
1836
  }
1476
1837
  self.logger.info("Setting next bundle to builtin")
1477
1838
  _ = self.implementation.setNextBundle(next: BundleInfo.ID_BUILTIN)
1478
- 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
+ )
1479
1846
  }
1480
1847
  return
1481
1848
  }
1482
1849
  let sessionKey = res.sessionKey ?? ""
1483
1850
  guard let downloadUrl = URL(string: res.url) else {
1484
1851
  self.logger.error("Error no url or wrong format")
1485
- 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
+ )
1486
1858
  return
1487
1859
  }
1488
1860
  let latestVersionName = res.version
@@ -1500,6 +1872,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1500
1872
  self.logger.error("Failed to delete failed bundle: \(nextImpl!.toString())")
1501
1873
  }
1502
1874
  }
1875
+ self.consumeOnLaunchDirectUpdateAttempt(plannedDirectUpdate: plannedDirectUpdate)
1503
1876
  if res.manifest != nil {
1504
1877
  nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey, link: res.link, comment: res.comment)
1505
1878
  } else {
@@ -1508,12 +1881,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1508
1881
  }
1509
1882
  guard let next = nextImpl else {
1510
1883
  self.logger.error("Error downloading file")
1511
- 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
+ )
1512
1890
  return
1513
1891
  }
1514
1892
  if next.isErrorStatus() {
1515
1893
  self.logger.error("Latest bundle already exists and is in error state. Aborting update.")
1516
- 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
+ )
1517
1900
  return
1518
1901
  }
1519
1902
  res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
@@ -1527,7 +1910,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1527
1910
  if !resDel {
1528
1911
  self.logger.error("Delete failed, id \(id) doesn't exist")
1529
1912
  }
1530
- 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
+ )
1531
1919
  return
1532
1920
  }
1533
1921
  let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
@@ -1540,34 +1928,67 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1540
1928
  }
1541
1929
  if !delayConditionList.isEmpty {
1542
1930
  self.logger.info("Update delayed until delay conditions met")
1543
- 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
+ )
1544
1938
  return
1545
1939
  }
1546
- if self.directUpdateMode == "onLaunch" {
1547
- self.onLaunchDirectUpdateUsed = true
1548
- 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
+ )
1549
1956
  }
1550
- _ = self.implementation.set(bundle: next)
1551
- _ = self._reload()
1552
- self.endBackGroundTaskWithNotif(msg: "update installed", latestVersionName: latestVersionName, current: next, error: false)
1553
1957
  } else {
1554
1958
  if plannedDirectUpdate && !directUpdateAllowed {
1555
1959
  self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
1556
1960
  }
1557
1961
  self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
1558
1962
  _ = self.implementation.setNextBundle(next: next.getId())
1559
- 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
+ )
1560
1970
  }
1561
1971
  return
1562
1972
  } catch {
1563
1973
  self.logger.error("Error downloading file \(error.localizedDescription)")
1564
1974
  let current: BundleInfo = self.implementation.getCurrentBundle()
1565
- 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
+ )
1566
1981
  return
1567
1982
  }
1568
1983
  } else {
1569
1984
  self.logger.info("No need to update, \(current.getId()) is the latest bundle.")
1570
- 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
+ )
1571
1992
  return
1572
1993
  }
1573
1994
  }
@@ -1591,6 +2012,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1591
2012
  logger.info("Next bundle is: \(next!.toString())")
1592
2013
  if self.implementation.set(bundle: next!) && self._reload() {
1593
2014
  logger.info("Updated to bundle: \(next!.toString())")
2015
+ self.notifyBundleSet(next!)
1594
2016
  _ = self.implementation.setNextBundle(next: Optional<String>.none)
1595
2017
  } else {
1596
2018
  logger.error("Update to bundle: \(next!.toString()) Failed!")