@capgo/capacitor-updater 8.47.5 → 8.47.7

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.
@@ -79,7 +79,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
79
79
  CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
80
80
  ]
81
81
  public var implementation = CapgoUpdater()
82
- private let pluginVersion: String = "8.47.5"
82
+ private let pluginVersion: String = "8.47.7"
83
83
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
84
84
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
85
85
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -89,6 +89,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
89
89
  static let autoUpdateModeLaunch = "onLaunch"
90
90
  static let autoUpdateModeAlways = "always"
91
91
  static let autoUpdateModeOnlyDownload = "onlyDownload"
92
+ private static let previewLoaderTimeoutMs = 60000
92
93
  private let keepUrlPathFlagKey = "__capgo_keep_url_path_after_reload"
93
94
  private let customIdDefaultsKey = "CapacitorUpdater.customId"
94
95
  private let updateUrlDefaultsKey = "CapacitorUpdater.updateUrl"
@@ -131,6 +132,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
131
132
  private var autoSplashscreenTimeoutWorkItem: DispatchWorkItem?
132
133
  private var splashscreenLoaderView: UIActivityIndicatorView?
133
134
  private var splashscreenLoaderContainer: UIView?
135
+ private var previewTransitionLoaderView: UIActivityIndicatorView?
136
+ private var previewTransitionLoaderContainer: UIView?
137
+ private var previewTransitionLoaderTimeoutWorkItem: DispatchWorkItem?
138
+ private var previewTransitionLoaderRequested = false
134
139
  private let splashscreenPluginName = "SplashScreen"
135
140
  private let splashscreenRetryDelayMilliseconds = 100
136
141
  private let splashscreenMaxRetries = 20
@@ -165,6 +170,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
165
170
  public var previewSessionEnabled = false
166
171
  private var previewSessionAlertPending = false
167
172
  private var isLeavingPreviewForIncomingLink = false
173
+ private var previewTransitionClearWorkItem: DispatchWorkItem?
168
174
  let semaphoreReady = DispatchSemaphore(value: 0)
169
175
 
170
176
  private var delayUpdateUtils: DelayUpdateUtils!
@@ -242,7 +248,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
242
248
  if previewSessionEnabled {
243
249
  previewSessionAlertPending = UserDefaults.standard.object(forKey: previewSessionAlertPendingDefaultsKey) as? Bool ?? true
244
250
  shakeMenuEnabled = true
245
- shakeChannelSelectorEnabled = false
251
+ shakeChannelSelectorEnabled = UserDefaults.standard.object(forKey: previewPreviousShakeChannelSelectorDefaultsKey) as? Bool
252
+ ?? shakeChannelSelectorEnabled
246
253
  }
247
254
  periodCheckDelay = Self.normalizedPeriodCheckDelaySeconds(getConfig().getInt("periodCheckDelay", 0))
248
255
 
@@ -986,7 +993,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
986
993
  let current: BundleInfo = self.implementation.getCurrentBundle()
987
994
  let next: BundleInfo? = self.implementation.getNextBundle()
988
995
 
989
- if let next = next, !next.isErrorStatus(), next.getId() != current.getId() {
996
+ if !self.isPreviewSessionStateActive(),
997
+ let next = next,
998
+ !next.isErrorStatus(),
999
+ next.getId() != current.getId() {
990
1000
  let previousState = self.implementation.captureResetState()
991
1001
  let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
992
1002
  logger.info("Applying pending bundle before reload: \(next.toString())")
@@ -1025,6 +1035,28 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1025
1035
  }
1026
1036
  }
1027
1037
 
1038
+ private func applyDownloadedBundleForDirectUpdate(_ next: BundleInfo) -> Bool {
1039
+ let previousState = self.implementation.captureResetState()
1040
+ let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
1041
+
1042
+ guard self.implementation.stagePendingReload(bundle: next) else {
1043
+ self.implementation.restoreResetState(previousState)
1044
+ logger.error("Direct update failed to stage downloaded bundle: \(next.toString())")
1045
+ return false
1046
+ }
1047
+
1048
+ if self._reload() {
1049
+ self.implementation.finalizePendingReload(bundle: next, previousBundleName: previousBundleName)
1050
+ _ = self.implementation.setNextBundle(next: Optional<String>.none)
1051
+ return true
1052
+ }
1053
+
1054
+ self.implementation.restoreResetState(previousState)
1055
+ self.restoreLiveBundleStateAfterFailedReload()
1056
+ logger.error("Direct update reload failed after staging bundle: \(next.toString())")
1057
+ return false
1058
+ }
1059
+
1028
1060
  @objc func next(_ call: CAPPluginCall) {
1029
1061
  guard let id = call.getString("id") else {
1030
1062
  logger.error("Next called without id")
@@ -1060,8 +1092,40 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1060
1092
  }
1061
1093
  }
1062
1094
 
1095
+ private func isPreviewSessionStateActive() -> Bool {
1096
+ self.previewSessionEnabled || self.isLeavingPreviewForIncomingLink || self.implementation.previewSession
1097
+ }
1098
+
1099
+ private func shouldBlockAutoUpdateForPreviewSession() -> Bool {
1100
+ guard self.isPreviewSessionStateActive() else {
1101
+ return false
1102
+ }
1103
+
1104
+ logger.info("Preview session is active. Skipping normal auto-update work.")
1105
+ return true
1106
+ }
1107
+
1108
+ private func clearIncomingPreviewTransition() {
1109
+ self.previewTransitionClearWorkItem?.cancel()
1110
+ self.previewTransitionClearWorkItem = nil
1111
+ self.isLeavingPreviewForIncomingLink = false
1112
+ if !self.previewSessionEnabled {
1113
+ self.implementation.previewSession = false
1114
+ }
1115
+ }
1116
+
1117
+ private func scheduleIncomingPreviewTransitionFallbackClear() {
1118
+ self.previewTransitionClearWorkItem?.cancel()
1119
+ let workItem = DispatchWorkItem { [weak self] in
1120
+ self?.clearIncomingPreviewTransition()
1121
+ }
1122
+ self.previewTransitionClearWorkItem = workItem
1123
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.appReadyTimeout), execute: workItem)
1124
+ }
1125
+
1063
1126
  @objc func startPreviewSession(_ call: CAPPluginCall) {
1064
1127
  guard self.allowPreview else {
1128
+ self.hidePreviewTransitionLoader(reason: "preview-session-not-allowed")
1065
1129
  logger.error("startPreviewSession called without allowPreview")
1066
1130
  call.reject("startPreviewSession not allowed. Set allowPreview to true in your config to enable it.")
1067
1131
  return
@@ -1070,6 +1134,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1070
1134
  let rawPayloadUrl = call.getString("payloadUrl")
1071
1135
  let previewPayloadUrl = self.normalizedPreviewPayloadUrl(rawPayloadUrl)
1072
1136
  if let rawPayloadUrl = rawPayloadUrl, !rawPayloadUrl.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, previewPayloadUrl == nil {
1137
+ self.hidePreviewTransitionLoader(reason: "preview-session-invalid-payload")
1073
1138
  logger.error("startPreviewSession called with invalid payloadUrl")
1074
1139
  call.reject("Invalid preview payloadUrl")
1075
1140
  return
@@ -1078,6 +1143,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1078
1143
  if !self.previewSessionEnabled {
1079
1144
  let current = self.implementation.getCurrentBundle()
1080
1145
  guard self.implementation.setPreviewFallbackBundle(fallback: current.getId()) else {
1146
+ self.hidePreviewTransitionLoader(reason: "preview-session-fallback-failed")
1081
1147
  logger.error("Could not save current bundle as preview fallback")
1082
1148
  call.reject("Could not save current bundle as preview fallback")
1083
1149
  return
@@ -1117,11 +1183,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1117
1183
  UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
1118
1184
  }
1119
1185
 
1186
+ self.clearIncomingPreviewTransition()
1187
+ self.hidePreviewTransitionLoader(reason: "preview-session-started")
1120
1188
  self.previewSessionEnabled = true
1121
1189
  self.previewSessionAlertPending = true
1122
1190
  self.implementation.previewSession = true
1123
1191
  self.shakeMenuEnabled = true
1124
- self.shakeChannelSelectorEnabled = false
1125
1192
  UserDefaults.standard.set(true, forKey: self.previewSessionDefaultsKey)
1126
1193
  UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
1127
1194
  UserDefaults.standard.synchronize()
@@ -1131,8 +1198,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1131
1198
  func leavePreviewSessionFromShakeMenu() -> Bool {
1132
1199
  let previewBundle = self.implementation.getCurrentBundle()
1133
1200
 
1201
+ self.showPreviewTransitionLoader(reason: "leave-preview-session")
1134
1202
  let didReset = self.resetToPreviewFallbackBundle()
1135
1203
  guard didReset else {
1204
+ self.hidePreviewTransitionLoader(reason: "leave-preview-session-failed")
1136
1205
  return false
1137
1206
  }
1138
1207
 
@@ -1152,14 +1221,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1152
1221
  }
1153
1222
 
1154
1223
  self.isLeavingPreviewForIncomingLink = true
1224
+ self.showPreviewTransitionLoader(reason: "preview-launch-deeplink")
1155
1225
  logger.info("Preview deeplink launch detected while preview session is active; restoring fallback before initial load")
1156
1226
  if !self.leavePreviewSessionWithoutReload() {
1157
1227
  logger.error("Could not leave preview session before initial preview deeplink routing")
1158
1228
  self.isLeavingPreviewForIncomingLink = false
1229
+ self.hidePreviewTransitionLoader(reason: "preview-launch-deeplink-failed")
1159
1230
  }
1160
1231
  }
1161
1232
 
1162
- private func leavePreviewSessionWithoutReload() -> Bool {
1233
+ private func leavePreviewSessionWithoutReload(keepPreviewGuard: Bool = false) -> Bool {
1163
1234
  let previewBundle = self.implementation.getCurrentBundle()
1164
1235
  guard let previewFallbackBundle = self.implementation.getPreviewFallbackBundle(), !previewFallbackBundle.isErrorStatus() else {
1165
1236
  logger.error("No preview fallback bundle available")
@@ -1174,12 +1245,55 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1174
1245
  return false
1175
1246
  }
1176
1247
 
1177
- self.endPreviewSession()
1248
+ self.endPreviewSession(keepPreviewGuard: keepPreviewGuard)
1178
1249
  let restoredNextBundle = self.implementation.getNextBundle()
1179
1250
  self.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle: previewFallbackBundle, restoredNextBundle: restoredNextBundle)
1180
1251
  return true
1181
1252
  }
1182
1253
 
1254
+ private func leavePreviewSessionForIncomingPreviewLink() -> Bool {
1255
+ self.showPreviewTransitionLoader(reason: "incoming-preview-deeplink")
1256
+ let previewBundle = self.implementation.getCurrentBundle()
1257
+ guard let previewFallbackBundle = self.implementation.getPreviewFallbackBundle(), !previewFallbackBundle.isErrorStatus() else {
1258
+ logger.error("No preview fallback bundle available")
1259
+ self.clearIncomingPreviewTransition()
1260
+ self.hidePreviewTransitionLoader(reason: "incoming-preview-deeplink-failed")
1261
+ return false
1262
+ }
1263
+ guard self.implementation.canSet(bundle: previewFallbackBundle) else {
1264
+ logger.error("Preview fallback bundle is not installable")
1265
+ self.clearIncomingPreviewTransition()
1266
+ self.hidePreviewTransitionLoader(reason: "incoming-preview-deeplink-failed")
1267
+ return false
1268
+ }
1269
+
1270
+ let previousState = self.implementation.captureResetState()
1271
+ guard self.implementation.stagePreviewFallbackReload(bundle: previewFallbackBundle) else {
1272
+ logger.error("Could not stage preview fallback bundle")
1273
+ self.clearIncomingPreviewTransition()
1274
+ self.hidePreviewTransitionLoader(reason: "incoming-preview-deeplink-failed")
1275
+ return false
1276
+ }
1277
+
1278
+ let didReload = self._reload()
1279
+ if didReload {
1280
+ self.endPreviewSession(keepPreviewGuard: true)
1281
+ let restoredNextBundle = self.implementation.getNextBundle()
1282
+ self.deletePreviewBundleIfUnused(
1283
+ previewBundle,
1284
+ previewFallbackBundle: previewFallbackBundle,
1285
+ restoredNextBundle: restoredNextBundle
1286
+ )
1287
+ self.scheduleIncomingPreviewTransitionFallbackClear()
1288
+ } else {
1289
+ self.implementation.restoreResetState(previousState)
1290
+ self.restoreLiveBundleStateAfterFailedReload()
1291
+ self.clearIncomingPreviewTransition()
1292
+ self.hidePreviewTransitionLoader(reason: "incoming-preview-deeplink-reload-failed")
1293
+ }
1294
+ return didReload
1295
+ }
1296
+
1183
1297
  private func deletePreviewBundleIfUnused(
1184
1298
  _ previewBundle: BundleInfo,
1185
1299
  previewFallbackBundle: BundleInfo?,
@@ -1193,11 +1307,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1193
1307
  }
1194
1308
 
1195
1309
  func reloadPreviewSessionFromShakeMenu() -> Bool {
1310
+ self.showPreviewTransitionLoader(reason: "reload-preview-session")
1311
+ let didReload: Bool
1196
1312
  if let payloadUrl = self.storedPreviewPayloadUrl() {
1197
- return self.refreshPreviewSessionFromPayloadUrl(payloadUrl)
1313
+ didReload = self.refreshPreviewSessionFromPayloadUrl(payloadUrl)
1314
+ } else {
1315
+ didReload = self._reload()
1198
1316
  }
1199
1317
 
1200
- return self._reload()
1318
+ if !didReload {
1319
+ self.hidePreviewTransitionLoader(reason: "reload-preview-session-failed")
1320
+ }
1321
+ return didReload
1201
1322
  }
1202
1323
 
1203
1324
  func hasActivePreviewSession() -> Bool {
@@ -1228,7 +1349,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1228
1349
  return false
1229
1350
  }
1230
1351
 
1231
- private func endPreviewSession() {
1352
+ private func endPreviewSession(keepPreviewGuard: Bool = false) {
1232
1353
  let previousShakeMenuEnabled = UserDefaults.standard.object(forKey: self.previewPreviousShakeMenuDefaultsKey) as? Bool
1233
1354
  ?? getConfig().getBoolean("shakeMenu", false)
1234
1355
  let previousShakeChannelSelectorEnabled = UserDefaults.standard.object(forKey: self.previewPreviousShakeChannelSelectorDefaultsKey) as? Bool
@@ -1239,8 +1360,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1239
1360
 
1240
1361
  self.previewSessionEnabled = false
1241
1362
  self.previewSessionAlertPending = false
1242
- self.isLeavingPreviewForIncomingLink = false
1243
- self.implementation.previewSession = false
1363
+ if keepPreviewGuard {
1364
+ self.implementation.previewSession = true
1365
+ } else {
1366
+ self.clearIncomingPreviewTransition()
1367
+ }
1244
1368
  self.shakeMenuEnabled = previousShakeMenuEnabled
1245
1369
  self.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled
1246
1370
  _ = self.implementation.setPreviewFallbackBundle(fallback: nil)
@@ -1271,6 +1395,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1271
1395
  self.previewSessionAlertPending = false
1272
1396
  self.isLeavingPreviewForIncomingLink = false
1273
1397
  self.implementation.previewSession = false
1398
+ self.hidePreviewTransitionLoader(reason: "preview-session-disabled")
1274
1399
  self.shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
1275
1400
  self.shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
1276
1401
  self.clearPreviewSessionPreferences()
@@ -1393,12 +1518,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1393
1518
  }
1394
1519
 
1395
1520
  self.isLeavingPreviewForIncomingLink = true
1521
+ self.showPreviewTransitionLoader(reason: "incoming-preview-deeplink")
1396
1522
  logger.info("Preview deeplink received while preview session is active; restoring fallback before routing")
1397
1523
  DispatchQueue.global(qos: .userInitiated).async {
1398
- let didLeave = self.leavePreviewSessionFromShakeMenu()
1524
+ let didLeave = self.leavePreviewSessionForIncomingPreviewLink()
1399
1525
  if !didLeave {
1400
1526
  self.logger.error("Could not leave preview session before routing incoming preview deeplink")
1401
1527
  self.isLeavingPreviewForIncomingLink = false
1528
+ self.hidePreviewTransitionLoader(reason: "incoming-preview-deeplink-failed")
1402
1529
  }
1403
1530
  }
1404
1531
  }
@@ -1701,6 +1828,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1701
1828
  logger.error("Error no url or wrong format")
1702
1829
  return "unavailable"
1703
1830
  }
1831
+ if self.shouldBlockAutoUpdateForPreviewSession() {
1832
+ return "preview_session"
1833
+ }
1704
1834
  if self.isDownloadStuckOrTimedOut() {
1705
1835
  logger.info("Download already in progress, skipping duplicate download request")
1706
1836
  return "already_running"
@@ -1937,6 +2067,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1937
2067
  let bundle = self.implementation.getCurrentBundle()
1938
2068
  self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
1939
2069
  logger.info("Current bundle loaded successfully. [notifyAppReady was called] \(bundle.toString())")
2070
+ self.clearIncomingPreviewTransition()
2071
+ self.hidePreviewTransitionLoader(reason: "notify-app-ready")
1940
2072
 
1941
2073
  call.resolve(["bundle": bundle.toJSON()])
1942
2074
  }
@@ -1987,6 +2119,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1987
2119
  // Note: _checkCancelDelay method has been moved to DelayUpdateUtils class
1988
2120
 
1989
2121
  private func _isAutoUpdateEnabled() -> Bool {
2122
+ if self.isPreviewSessionStateActive() {
2123
+ return false
2124
+ }
1990
2125
  let instanceDescriptor = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor()
1991
2126
  if instanceDescriptor?.serverURL != nil {
1992
2127
  logger.warn("AutoUpdate is automatic disabled when serverUrl is set.")
@@ -2024,6 +2159,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2024
2159
  logger.info("Built-in bundle is active. We skip the check for notifyAppReady.")
2025
2160
  return
2026
2161
  }
2162
+ if self.isPreviewSessionStateActive() {
2163
+ logger.info("Preview session is active. We skip the check for notifyAppReady.")
2164
+ return
2165
+ }
2027
2166
 
2028
2167
  logger.info("Current bundle is: \(current.toString())")
2029
2168
 
@@ -2076,6 +2215,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2076
2215
  if self.autoSplashscreen {
2077
2216
  self.hideSplashscreen()
2078
2217
  }
2218
+ self.hidePreviewTransitionLoader(reason: "app-ready")
2079
2219
  }
2080
2220
  }
2081
2221
 
@@ -2243,6 +2383,51 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2243
2383
  }
2244
2384
  }
2245
2385
 
2386
+ private func createLoaderOverlay(
2387
+ backgroundColor: UIColor,
2388
+ isUserInteractionEnabled: Bool,
2389
+ indicatorColor: UIColor?
2390
+ ) -> (container: UIView, indicator: UIActivityIndicatorView) {
2391
+ let container = UIView()
2392
+ container.translatesAutoresizingMaskIntoConstraints = false
2393
+ container.backgroundColor = backgroundColor
2394
+ container.isUserInteractionEnabled = isUserInteractionEnabled
2395
+
2396
+ let indicatorStyle: UIActivityIndicatorView.Style
2397
+ if #available(iOS 13.0, *) {
2398
+ indicatorStyle = .large
2399
+ } else {
2400
+ indicatorStyle = .whiteLarge
2401
+ }
2402
+
2403
+ let indicator = UIActivityIndicatorView(style: indicatorStyle)
2404
+ indicator.translatesAutoresizingMaskIntoConstraints = false
2405
+ indicator.hidesWhenStopped = false
2406
+ if let indicatorColor = indicatorColor {
2407
+ indicator.color = indicatorColor
2408
+ }
2409
+ indicator.startAnimating()
2410
+
2411
+ return (container, indicator)
2412
+ }
2413
+
2414
+ private func attachLoaderOverlay(
2415
+ _ overlay: (container: UIView, indicator: UIActivityIndicatorView),
2416
+ to rootView: UIView
2417
+ ) {
2418
+ overlay.container.addSubview(overlay.indicator)
2419
+ rootView.addSubview(overlay.container)
2420
+
2421
+ NSLayoutConstraint.activate([
2422
+ overlay.container.leadingAnchor.constraint(equalTo: rootView.leadingAnchor),
2423
+ overlay.container.trailingAnchor.constraint(equalTo: rootView.trailingAnchor),
2424
+ overlay.container.topAnchor.constraint(equalTo: rootView.topAnchor),
2425
+ overlay.container.bottomAnchor.constraint(equalTo: rootView.bottomAnchor),
2426
+ overlay.indicator.centerXAnchor.constraint(equalTo: overlay.container.centerXAnchor),
2427
+ overlay.indicator.centerYAnchor.constraint(equalTo: overlay.container.centerYAnchor)
2428
+ ])
2429
+ }
2430
+
2246
2431
  private func addSplashscreenLoaderIfNeeded() {
2247
2432
  guard self.autoSplashscreenLoader else {
2248
2433
  return
@@ -2257,40 +2442,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2257
2442
  return
2258
2443
  }
2259
2444
 
2260
- let container = UIView()
2261
- container.translatesAutoresizingMaskIntoConstraints = false
2262
- container.backgroundColor = UIColor.clear
2263
- container.isUserInteractionEnabled = false
2264
-
2265
- let indicatorStyle: UIActivityIndicatorView.Style
2445
+ let indicatorColor: UIColor?
2266
2446
  if #available(iOS 13.0, *) {
2267
- indicatorStyle = .large
2447
+ indicatorColor = UIColor.label
2268
2448
  } else {
2269
- indicatorStyle = .whiteLarge
2270
- }
2271
-
2272
- let indicator = UIActivityIndicatorView(style: indicatorStyle)
2273
- indicator.translatesAutoresizingMaskIntoConstraints = false
2274
- indicator.hidesWhenStopped = false
2275
- if #available(iOS 13.0, *) {
2276
- indicator.color = UIColor.label
2449
+ indicatorColor = nil
2277
2450
  }
2278
- indicator.startAnimating()
2279
-
2280
- container.addSubview(indicator)
2281
- rootView.addSubview(container)
2282
-
2283
- NSLayoutConstraint.activate([
2284
- container.leadingAnchor.constraint(equalTo: rootView.leadingAnchor),
2285
- container.trailingAnchor.constraint(equalTo: rootView.trailingAnchor),
2286
- container.topAnchor.constraint(equalTo: rootView.topAnchor),
2287
- container.bottomAnchor.constraint(equalTo: rootView.bottomAnchor),
2288
- indicator.centerXAnchor.constraint(equalTo: container.centerXAnchor),
2289
- indicator.centerYAnchor.constraint(equalTo: container.centerYAnchor)
2290
- ])
2291
-
2292
- self.splashscreenLoaderContainer = container
2293
- self.splashscreenLoaderView = indicator
2451
+ let overlay = self.createLoaderOverlay(
2452
+ backgroundColor: UIColor.clear,
2453
+ isUserInteractionEnabled: false,
2454
+ indicatorColor: indicatorColor
2455
+ )
2456
+ self.attachLoaderOverlay(overlay, to: rootView)
2457
+ self.splashscreenLoaderContainer = overlay.container
2458
+ self.splashscreenLoaderView = overlay.indicator
2294
2459
  }
2295
2460
 
2296
2461
  if Thread.isMainThread {
@@ -2319,6 +2484,97 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2319
2484
  }
2320
2485
  }
2321
2486
 
2487
+ private func showPreviewTransitionLoader(reason: String) {
2488
+ self.previewTransitionLoaderRequested = true
2489
+ let showLoader = {
2490
+ guard self.previewTransitionLoaderRequested else {
2491
+ return
2492
+ }
2493
+
2494
+ if let container = self.previewTransitionLoaderContainer {
2495
+ self.previewTransitionLoaderTimeoutWorkItem?.cancel()
2496
+ self.schedulePreviewTransitionLoaderTimeout()
2497
+ container.superview?.bringSubviewToFront(container)
2498
+ return
2499
+ }
2500
+
2501
+ guard let rootView = self.bridge?.viewController?.view else {
2502
+ self.logger.warn("Preview transition loader unavailable: root view missing for \(reason)")
2503
+ self.previewTransitionLoaderRequested = false
2504
+ return
2505
+ }
2506
+
2507
+ self.previewTransitionLoaderTimeoutWorkItem?.cancel()
2508
+ self.schedulePreviewTransitionLoaderTimeout()
2509
+
2510
+ let indicatorColor: UIColor?
2511
+ if #available(iOS 13.0, *) {
2512
+ indicatorColor = UIColor.white
2513
+ } else {
2514
+ indicatorColor = nil
2515
+ }
2516
+ let overlay = self.createLoaderOverlay(
2517
+ backgroundColor: UIColor.black.withAlphaComponent(0.18),
2518
+ isUserInteractionEnabled: true,
2519
+ indicatorColor: indicatorColor
2520
+ )
2521
+ self.attachLoaderOverlay(overlay, to: rootView)
2522
+ self.previewTransitionLoaderContainer = overlay.container
2523
+ self.previewTransitionLoaderView = overlay.indicator
2524
+ self.logger.info("Preview transition loader shown: \(reason)")
2525
+ }
2526
+
2527
+ if Thread.isMainThread {
2528
+ showLoader()
2529
+ } else {
2530
+ DispatchQueue.main.async {
2531
+ showLoader()
2532
+ }
2533
+ }
2534
+ }
2535
+
2536
+ private func hidePreviewTransitionLoader(reason: String) {
2537
+ if !self.previewTransitionLoaderRequested &&
2538
+ self.previewTransitionLoaderContainer == nil &&
2539
+ self.previewTransitionLoaderTimeoutWorkItem == nil {
2540
+ return
2541
+ }
2542
+
2543
+ let hideLoader = {
2544
+ self.previewTransitionLoaderRequested = false
2545
+ self.previewTransitionLoaderTimeoutWorkItem?.cancel()
2546
+ self.previewTransitionLoaderTimeoutWorkItem = nil
2547
+ guard self.previewTransitionLoaderContainer != nil else {
2548
+ return
2549
+ }
2550
+ self.previewTransitionLoaderView?.stopAnimating()
2551
+ self.previewTransitionLoaderContainer?.removeFromSuperview()
2552
+ self.previewTransitionLoaderView = nil
2553
+ self.previewTransitionLoaderContainer = nil
2554
+ self.logger.info("Preview transition loader hidden: \(reason)")
2555
+ }
2556
+
2557
+ if Thread.isMainThread {
2558
+ hideLoader()
2559
+ } else {
2560
+ DispatchQueue.main.async {
2561
+ hideLoader()
2562
+ }
2563
+ }
2564
+ }
2565
+
2566
+ private func schedulePreviewTransitionLoaderTimeout() {
2567
+ self.previewTransitionLoaderTimeoutWorkItem?.cancel()
2568
+ let workItem = DispatchWorkItem { [weak self] in
2569
+ self?.hidePreviewTransitionLoader(reason: "preview-transition-timeout")
2570
+ }
2571
+ self.previewTransitionLoaderTimeoutWorkItem = workItem
2572
+ DispatchQueue.main.asyncAfter(
2573
+ deadline: .now() + .milliseconds(Self.previewLoaderTimeoutMs),
2574
+ execute: workItem
2575
+ )
2576
+ }
2577
+
2322
2578
  private func scheduleSplashscreenTimeout() {
2323
2579
  guard self.autoSplashscreenTimeout > 0 else {
2324
2580
  return
@@ -2693,6 +2949,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2693
2949
  return true
2694
2950
  }
2695
2951
 
2952
+ private func clearDownloadInProgressState() {
2953
+ downloadLock.lock()
2954
+ defer { downloadLock.unlock() }
2955
+ downloadInProgress = false
2956
+ downloadStartTime = nil
2957
+ }
2958
+
2696
2959
  func runBackgroundDownloadWork(_ work: @escaping () -> Void) {
2697
2960
  // Live update checks/downloads are user-visible work. Using `.background`
2698
2961
  // lets the scheduler starve them for minutes while the app is active.
@@ -2718,6 +2981,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2718
2981
  }
2719
2982
 
2720
2983
  func backgroundDownload() {
2984
+ if self.shouldBlockAutoUpdateForPreviewSession() {
2985
+ return
2986
+ }
2721
2987
  // Set download in progress flag (thread-safe)
2722
2988
  downloadLock.lock()
2723
2989
  downloadInProgress = true
@@ -2746,10 +3012,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2746
3012
  self.runBackgroundDownloadWork {
2747
3013
  // Wait for cleanup to complete before starting download
2748
3014
  self.waitForCleanupIfNeeded()
3015
+ if self.shouldBlockAutoUpdateForPreviewSession() {
3016
+ self.clearDownloadInProgressState()
3017
+ return
3018
+ }
2749
3019
  self.beginDownloadBackgroundTask()
2750
3020
  self.logger.info("Check for update via \(self.updateUrl)")
2751
3021
  let res = self.implementation.getLatest(url: url, channel: nil)
2752
3022
  let current = self.implementation.getCurrentBundle()
3023
+ if self.shouldBlockAutoUpdateForPreviewSession() {
3024
+ self.clearDownloadInProgressState()
3025
+ self.endBackGroundTask()
3026
+ return
3027
+ }
2753
3028
 
2754
3029
  // Handle network errors and other failures first
2755
3030
  let backendError = res.error ?? ""
@@ -2861,6 +3136,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2861
3136
  )
2862
3137
  return
2863
3138
  }
3139
+ if self.shouldBlockAutoUpdateForPreviewSession() {
3140
+ self.clearDownloadInProgressState()
3141
+ self.endBackGroundTask()
3142
+ return
3143
+ }
2864
3144
  res.checksum = try CryptoCipher.decryptChecksum(checksum: res.checksum, publicKey: self.implementation.publicKey)
2865
3145
  CryptoCipher.logChecksumInfo(label: "Bundle checksum", hexChecksum: next.getChecksum())
2866
3146
  CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: res.checksum)
@@ -2880,6 +3160,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2880
3160
  )
2881
3161
  return
2882
3162
  }
3163
+ if self.shouldBlockAutoUpdateForPreviewSession() {
3164
+ self.clearDownloadInProgressState()
3165
+ self.endBackGroundTask()
3166
+ return
3167
+ }
2883
3168
  let directUpdateAllowed = plannedDirectUpdate && !self.autoSplashscreenTimedOut
2884
3169
  if directUpdateAllowed {
2885
3170
  let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
@@ -2899,7 +3184,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2899
3184
  )
2900
3185
  return
2901
3186
  }
2902
- if self.implementation.set(bundle: next) && self._reload() {
3187
+ if self.applyDownloadedBundleForDirectUpdate(next) {
2903
3188
  self.notifyBundleSet(next)
2904
3189
  self.endBackGroundTaskWithNotif(
2905
3190
  msg: "update installed",
@@ -2909,10 +3194,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2909
3194
  plannedDirectUpdate: plannedDirectUpdate
2910
3195
  )
2911
3196
  } else {
3197
+ _ = self.implementation.setNextBundle(next: next.getId())
3198
+ self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
2912
3199
  self.endBackGroundTaskWithNotif(
2913
- msg: "Update install failed",
3200
+ msg: "Direct update reload failed, update will install next background",
2914
3201
  latestVersionName: latestVersionName,
2915
- current: next,
3202
+ current: self.implementation.getCurrentBundle(),
3203
+ error: false,
2916
3204
  plannedDirectUpdate: plannedDirectUpdate
2917
3205
  )
2918
3206
  }
@@ -2968,6 +3256,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2968
3256
  }
2969
3257
 
2970
3258
  private func installNext() {
3259
+ if self.shouldBlockAutoUpdateForPreviewSession() {
3260
+ return
3261
+ }
2971
3262
  let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
2972
3263
  let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
2973
3264
  let kind: String = obj.value(forKey: "kind") as! String
@@ -3059,8 +3350,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
3059
3350
  return
3060
3351
  }
3061
3352
  DispatchQueue.global(qos: .utility).async {
3353
+ if self.shouldBlockAutoUpdateForPreviewSession() {
3354
+ return
3355
+ }
3062
3356
  let res = self.implementation.getLatest(url: url, channel: nil)
3063
3357
  let current = self.implementation.getCurrentBundle()
3358
+ if self.shouldBlockAutoUpdateForPreviewSession() {
3359
+ return
3360
+ }
3064
3361
 
3065
3362
  if res.version != current.getVersionName() {
3066
3363
  self.logger.info("New version found: \(res.version)")
@@ -2431,7 +2431,7 @@ import UIKit
2431
2431
  if let channels = responseValue.channels {
2432
2432
  listChannels.channels = channels.map { channel in
2433
2433
  var channelDict: [String: Any] = [:]
2434
- channelDict["id"] = channel.id ?? ""
2434
+ channelDict["id"] = channel.id
2435
2435
  channelDict["name"] = channel.name ?? ""
2436
2436
  channelDict["public"] = channel.public ?? false
2437
2437
  channelDict["allow_self_set"] = channel.allow_self_set ?? false