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