@capgo/capacitor-updater 8.46.1 → 8.47.0

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.
@@ -37,6 +37,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
37
37
  CAPPluginMethod(name: "setStatsUrl", returnType: CAPPluginReturnPromise),
38
38
  CAPPluginMethod(name: "setChannelUrl", returnType: CAPPluginReturnPromise),
39
39
  CAPPluginMethod(name: "set", returnType: CAPPluginReturnPromise),
40
+ CAPPluginMethod(name: "startPreviewSession", returnType: CAPPluginReturnPromise),
40
41
  CAPPluginMethod(name: "list", returnType: CAPPluginReturnPromise),
41
42
  CAPPluginMethod(name: "delete", returnType: CAPPluginReturnPromise),
42
43
  CAPPluginMethod(name: "setBundleError", returnType: CAPPluginReturnPromise),
@@ -47,6 +48,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
47
48
  CAPPluginMethod(name: "setMultiDelay", returnType: CAPPluginReturnPromise),
48
49
  CAPPluginMethod(name: "cancelDelay", returnType: CAPPluginReturnPromise),
49
50
  CAPPluginMethod(name: "getLatest", returnType: CAPPluginReturnPromise),
51
+ CAPPluginMethod(name: "getMissingBundleFiles", returnType: CAPPluginReturnPromise),
52
+ CAPPluginMethod(name: "getBundleDownloadSize", returnType: CAPPluginReturnPromise),
50
53
  CAPPluginMethod(name: "triggerUpdateCheck", returnType: CAPPluginReturnPromise),
51
54
  CAPPluginMethod(name: "setChannel", returnType: CAPPluginReturnPromise),
52
55
  CAPPluginMethod(name: "unsetChannel", returnType: CAPPluginReturnPromise),
@@ -76,7 +79,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
76
79
  CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
77
80
  ]
78
81
  public var implementation = CapgoUpdater()
79
- private let pluginVersion: String = "8.46.1"
82
+ private let pluginVersion: String = "8.47.0"
80
83
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
81
84
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
82
85
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -87,6 +90,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
87
90
  private let channelUrlDefaultsKey = "CapacitorUpdater.channelUrl"
88
91
  private let defaultChannelDefaultsKey = "CapacitorUpdater.defaultChannel"
89
92
  private let lastFailedBundleDefaultsKey = "CapacitorUpdater.lastFailedBundle"
93
+ private let previewSessionDefaultsKey = "CapacitorUpdater.previewSession"
94
+ private let previewPreviousShakeMenuDefaultsKey = "CapacitorUpdater.previewPreviousShakeMenu"
95
+ private let previewPreviousShakeChannelSelectorDefaultsKey = "CapacitorUpdater.previewPreviousShakeChannelSelector"
96
+ private let previewPreviousNextBundleDefaultsKey = "CapacitorUpdater.previewPreviousNextBundle"
97
+ private let previewPreviousAppIdDefaultsKey = "CapacitorUpdater.previewPreviousAppId"
98
+ private let previewAppIdDefaultsKey = "CapacitorUpdater.previewAppId"
90
99
  // Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
91
100
  private var updateUrl = ""
92
101
  private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
@@ -131,11 +140,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
131
140
  private var persistCustomId = false
132
141
  private var persistModifyUrl = false
133
142
  private var allowManualBundleError = false
143
+ private var allowPreview = false
134
144
  private var keepUrlPathFlagLastValue: Bool?
135
145
  private var appHealthTracker: AppHealthTracker?
136
146
  private var webViewStatsReporter: WebViewStatsReporter?
137
147
  public var shakeMenuEnabled = false
138
148
  public var shakeChannelSelectorEnabled = false
149
+ public var previewSessionEnabled = false
150
+ private var previewSessionAlertPending = false
139
151
  let semaphoreReady = DispatchSemaphore(value: 0)
140
152
 
141
153
  private var delayUpdateUtils: DelayUpdateUtils!
@@ -171,6 +183,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
171
183
  }
172
184
  persistModifyUrl = getConfig().getBoolean("persistModifyUrl", false)
173
185
  allowManualBundleError = getConfig().getBoolean("allowManualBundleError", false)
186
+ allowPreview = getConfig().getBoolean("allowPreview", false)
174
187
  logger.info("init for device \(self.implementation.deviceID)")
175
188
  guard let versionName = getConfig().getString("version", Bundle.main.versionName) else {
176
189
  logger.error("Cannot get version name")
@@ -232,6 +245,14 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
232
245
  resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
233
246
  shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
234
247
  shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
248
+ let storedPreviewSessionEnabled = UserDefaults.standard.bool(forKey: previewSessionDefaultsKey)
249
+ let shouldClearPreviewSessionBecauseDisabled = !allowPreview && storedPreviewSessionEnabled
250
+ previewSessionEnabled = allowPreview && storedPreviewSessionEnabled
251
+ implementation.previewSession = previewSessionEnabled
252
+ if previewSessionEnabled {
253
+ shakeMenuEnabled = true
254
+ shakeChannelSelectorEnabled = false
255
+ }
235
256
  periodCheckDelay = Self.normalizedPeriodCheckDelaySeconds(getConfig().getInt("periodCheckDelay", 0))
236
257
 
237
258
  implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
@@ -269,6 +290,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
269
290
  // crash the app on purpose it should not happen
270
291
  fatalError("appId is missing in capacitor.config.json or plugin config, and cannot be retrieved from the native app, please add it globally or in the plugin config")
271
292
  }
293
+ if shouldClearPreviewSessionBecauseDisabled {
294
+ clearPreviewSessionBecauseDisabled()
295
+ }
296
+ if previewSessionEnabled,
297
+ let previewAppId = UserDefaults.standard.string(forKey: previewAppIdDefaultsKey),
298
+ !previewAppId.isEmpty {
299
+ implementation.appId = previewAppId
300
+ logger.info("Using preview appId \(previewAppId)")
301
+ }
272
302
  logger.info("appId \(implementation.appId)")
273
303
  implementation.statsUrl = getConfig().getString("statsUrl", CapacitorUpdaterPlugin.statsUrlDefault)!
274
304
  implementation.channelUrl = getConfig().getString("channelUrl", CapacitorUpdaterPlugin.channelUrlDefault)!
@@ -298,6 +328,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
298
328
 
299
329
  // Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
300
330
  self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
331
+ let nativeBuildVersionChanged = self.hasNativeBuildVersionChanged()
332
+ if nativeBuildVersionChanged {
333
+ self.clearPreviewSessionForNativeBuildChange()
334
+ }
301
335
 
302
336
  if resetWhenUpdate {
303
337
  let didResetCurrentBundle = self.resetCurrentBundleForNativeBuildChangeIfNeeded()
@@ -718,6 +752,23 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
718
752
  call.resolve(["version": self.pluginVersion])
719
753
  }
720
754
 
755
+ private func manifestEntries(from manifestArray: [Any]?) -> [ManifestEntry]? {
756
+ guard let manifestArray = manifestArray else {
757
+ return nil
758
+ }
759
+ var manifestEntries: [ManifestEntry] = []
760
+ for item in manifestArray {
761
+ if let manifestDict = item as? [String: Any] {
762
+ manifestEntries.append(ManifestEntry(
763
+ file_name: manifestDict["file_name"] as? String,
764
+ file_hash: manifestDict["file_hash"] as? String,
765
+ download_url: manifestDict["download_url"] as? String
766
+ ))
767
+ }
768
+ }
769
+ return manifestEntries
770
+ }
771
+
721
772
  @objc func download(_ call: CAPPluginCall) {
722
773
  guard let urlString = call.getString("url") else {
723
774
  logger.error("Download called without url")
@@ -739,19 +790,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
739
790
  DispatchQueue.global(qos: .background).async {
740
791
  do {
741
792
  let next: BundleInfo
742
- if let manifestArray = manifestArray {
743
- // Convert JSArray to [ManifestEntry]
744
- var manifestEntries: [ManifestEntry] = []
745
- for item in manifestArray {
746
- if let manifestDict = item as? [String: Any] {
747
- let entry = ManifestEntry(
748
- file_name: manifestDict["file_name"] as? String,
749
- file_hash: manifestDict["file_hash"] as? String,
750
- download_url: manifestDict["download_url"] as? String
751
- )
752
- manifestEntries.append(entry)
753
- }
754
- }
793
+ if let manifestEntries = self.manifestEntries(from: manifestArray) {
755
794
  next = try self.implementation.downloadManifest(manifest: manifestEntries, version: version, sessionKey: sessionKey)
756
795
  } else {
757
796
  next = try self.implementation.download(url: url!, version: version, sessionKey: sessionKey)
@@ -908,6 +947,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
908
947
  }
909
948
  self.notifyBundleSet(next)
910
949
  _ = self.implementation.setNextBundle(next: Optional<String>.none)
950
+ self.showPreviewSessionNoticeIfNeeded()
911
951
  call.resolve()
912
952
  return
913
953
  }
@@ -919,6 +959,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
919
959
  }
920
960
 
921
961
  if self._reload() {
962
+ self.showPreviewSessionNoticeIfNeeded()
922
963
  call.resolve()
923
964
  } else {
924
965
  logger.error("Reload failed")
@@ -956,10 +997,250 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
956
997
  call.reject("Reload failed after setting bundle \(id)")
957
998
  } else {
958
999
  self.notifyBundleSet(self.implementation.getBundleInfo(id: id))
1000
+ self.showPreviewSessionNoticeIfNeeded()
959
1001
  call.resolve()
960
1002
  }
961
1003
  }
962
1004
 
1005
+ @objc func startPreviewSession(_ call: CAPPluginCall) {
1006
+ guard self.allowPreview else {
1007
+ logger.error("startPreviewSession called without allowPreview")
1008
+ call.reject("startPreviewSession not allowed. Set allowPreview to true in your config to enable it.")
1009
+ return
1010
+ }
1011
+ let previewAppId = self.normalizedPreviewAppId(call.getString("appId"))
1012
+
1013
+ if !self.previewSessionEnabled {
1014
+ let current = self.implementation.getCurrentBundle()
1015
+ guard self.implementation.setPreviewFallbackBundle(fallback: current.getId()) else {
1016
+ logger.error("Could not save current bundle as preview fallback")
1017
+ call.reject("Could not save current bundle as preview fallback")
1018
+ return
1019
+ }
1020
+
1021
+ if let previousNext = self.implementation.getNextBundle(),
1022
+ !previousNext.isDeleted(),
1023
+ !previousNext.isErrorStatus() {
1024
+ UserDefaults.standard.set(previousNext.getId(), forKey: self.previewPreviousNextBundleDefaultsKey)
1025
+ } else {
1026
+ UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
1027
+ }
1028
+
1029
+ UserDefaults.standard.set(self.implementation.appId, forKey: self.previewPreviousAppIdDefaultsKey)
1030
+ UserDefaults.standard.set(self.shakeMenuEnabled, forKey: self.previewPreviousShakeMenuDefaultsKey)
1031
+ UserDefaults.standard.set(self.shakeChannelSelectorEnabled, forKey: self.previewPreviousShakeChannelSelectorDefaultsKey)
1032
+ logger.info("Preview session started with fallback bundle: \(current.toString())")
1033
+ }
1034
+
1035
+ if let previewAppId = previewAppId, !previewAppId.isEmpty {
1036
+ self.implementation.appId = previewAppId
1037
+ UserDefaults.standard.set(previewAppId, forKey: self.previewAppIdDefaultsKey)
1038
+ logger.info("Preview session using appId: \(previewAppId)")
1039
+ }
1040
+
1041
+ self.previewSessionEnabled = true
1042
+ self.previewSessionAlertPending = true
1043
+ self.implementation.previewSession = true
1044
+ self.shakeMenuEnabled = true
1045
+ self.shakeChannelSelectorEnabled = false
1046
+ UserDefaults.standard.set(true, forKey: self.previewSessionDefaultsKey)
1047
+ UserDefaults.standard.synchronize()
1048
+ call.resolve()
1049
+ }
1050
+
1051
+ func leavePreviewSessionFromShakeMenu() -> Bool {
1052
+ let previewBundle = self.implementation.getCurrentBundle()
1053
+ let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
1054
+
1055
+ let didReset = self.resetToPreviewFallbackBundle()
1056
+ guard didReset else {
1057
+ return false
1058
+ }
1059
+
1060
+ _ = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
1061
+ let previewFallbackBundle = self.implementation.getPreviewFallbackBundle()
1062
+ self.endPreviewSession()
1063
+ let restoredNextBundle = self.implementation.getNextBundle()
1064
+ if !previewBundle.isBuiltin() &&
1065
+ previewFallbackBundle?.getId() != previewBundle.getId() &&
1066
+ restoredNextBundle?.getId() != previewBundle.getId() {
1067
+ _ = self.implementation.delete(id: previewBundle.getId(), removeInfo: false)
1068
+ }
1069
+ return true
1070
+ }
1071
+
1072
+ func reloadPreviewSessionFromShakeMenu() -> Bool {
1073
+ self._reload()
1074
+ }
1075
+
1076
+ func hasActivePreviewSession() -> Bool {
1077
+ self.previewSessionEnabled
1078
+ }
1079
+
1080
+ private func resetToPreviewFallbackBundle() -> Bool {
1081
+ guard self.canPerformResetTransition() else { return false }
1082
+ guard let fallback = self.implementation.getPreviewFallbackBundle(), !fallback.isErrorStatus() else {
1083
+ logger.error("No preview fallback bundle available")
1084
+ return false
1085
+ }
1086
+ guard self.implementation.canSet(bundle: fallback) else {
1087
+ logger.error("Preview fallback bundle is not installable")
1088
+ return false
1089
+ }
1090
+
1091
+ let previousState = self.implementation.captureResetState()
1092
+ let previousBundleName = self.implementation.getCurrentBundle().getVersionName()
1093
+ logger.info("Resetting to preview fallback bundle: \(fallback.toString())")
1094
+ if self.implementation.stagePreviewFallbackReload(bundle: fallback) && self._reload() {
1095
+ self.implementation.finalizeResetTransition(previousBundleName: previousBundleName, isInternal: false)
1096
+ self.notifyBundleSet(fallback)
1097
+ return true
1098
+ }
1099
+ self.implementation.restoreResetState(previousState)
1100
+ self.restoreLiveBundleStateAfterFailedReload()
1101
+ return false
1102
+ }
1103
+
1104
+ private func endPreviewSession() {
1105
+ let previousShakeMenuEnabled = UserDefaults.standard.object(forKey: self.previewPreviousShakeMenuDefaultsKey) as? Bool
1106
+ ?? getConfig().getBoolean("shakeMenu", false)
1107
+ let previousShakeChannelSelectorEnabled = UserDefaults.standard.object(forKey: self.previewPreviousShakeChannelSelectorDefaultsKey) as? Bool
1108
+ ?? getConfig().getBoolean("allowShakeChannelSelector", false)
1109
+ self.restorePreviewPreviousNextBundle()
1110
+ self.restorePreviewPreviousAppId()
1111
+
1112
+ self.previewSessionEnabled = false
1113
+ self.previewSessionAlertPending = false
1114
+ self.implementation.previewSession = false
1115
+ self.shakeMenuEnabled = previousShakeMenuEnabled
1116
+ self.shakeChannelSelectorEnabled = previousShakeChannelSelectorEnabled
1117
+ _ = self.implementation.setPreviewFallbackBundle(fallback: nil)
1118
+ self.clearPreviewSessionPreferences()
1119
+ logger.info("Preview session ended")
1120
+ }
1121
+
1122
+ private func clearPreviewSessionBecauseDisabled() {
1123
+ logger.info("Preview session disabled by config; restoring preview fallback")
1124
+ let fallback = self.implementation.getPreviewFallbackBundle()
1125
+ let bundleToRestore: BundleInfo
1126
+ if let fallback, !fallback.isErrorStatus() {
1127
+ bundleToRestore = fallback
1128
+ } else {
1129
+ bundleToRestore = self.implementation.getBundleInfo(id: BundleInfo.ID_BUILTIN)
1130
+ }
1131
+
1132
+ if self.implementation.canSet(bundle: bundleToRestore) {
1133
+ _ = self.implementation.stagePreviewFallbackReload(bundle: bundleToRestore)
1134
+ } else {
1135
+ logger.warn("Could not restore preview fallback while disabling preview")
1136
+ }
1137
+
1138
+ self.restorePreviewPreviousNextBundle()
1139
+ self.restorePreviewPreviousAppId()
1140
+ self.previewSessionEnabled = false
1141
+ self.previewSessionAlertPending = false
1142
+ self.implementation.previewSession = false
1143
+ self.shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
1144
+ self.shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
1145
+ self.clearPreviewSessionPreferences()
1146
+ }
1147
+
1148
+ private func clearPreviewSessionPreferences() {
1149
+ _ = self.implementation.setPreviewFallbackBundle(fallback: nil)
1150
+ UserDefaults.standard.removeObject(forKey: self.previewSessionDefaultsKey)
1151
+ UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeMenuDefaultsKey)
1152
+ UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeChannelSelectorDefaultsKey)
1153
+ UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
1154
+ UserDefaults.standard.removeObject(forKey: self.previewPreviousAppIdDefaultsKey)
1155
+ UserDefaults.standard.removeObject(forKey: self.previewAppIdDefaultsKey)
1156
+ UserDefaults.standard.synchronize()
1157
+ }
1158
+
1159
+ private func restorePreviewPreviousAppId() {
1160
+ guard let previousAppId = UserDefaults.standard.string(forKey: self.previewPreviousAppIdDefaultsKey),
1161
+ !previousAppId.isEmpty else {
1162
+ return
1163
+ }
1164
+ self.implementation.appId = previousAppId
1165
+ logger.info("Restored appId after preview: \(previousAppId)")
1166
+ }
1167
+
1168
+ private func normalizedPreviewAppId(_ rawAppId: String?) -> String? {
1169
+ guard let rawAppId else {
1170
+ return nil
1171
+ }
1172
+
1173
+ let appId = rawAppId.trimmingCharacters(in: .whitespacesAndNewlines)
1174
+ guard !appId.isEmpty else {
1175
+ return nil
1176
+ }
1177
+
1178
+ let lowercasedAppId = appId.lowercased()
1179
+ if lowercasedAppId == "undefined" || lowercasedAppId == "null" {
1180
+ return nil
1181
+ }
1182
+
1183
+ return appId
1184
+ }
1185
+
1186
+ private func clearPreviewSessionForNativeBuildChange() {
1187
+ guard self.previewSessionEnabled || self.implementation.getPreviewFallbackBundle() != nil else {
1188
+ return
1189
+ }
1190
+ logger.info("Native build changed; clearing preview session state")
1191
+ self.previewSessionEnabled = false
1192
+ self.previewSessionAlertPending = false
1193
+ self.implementation.previewSession = false
1194
+ self.shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
1195
+ self.shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
1196
+ self.restorePreviewPreviousAppId()
1197
+ _ = self.implementation.setPreviewFallbackBundle(fallback: nil)
1198
+ _ = self.implementation.setNextBundle(next: Optional<String>.none)
1199
+ let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
1200
+ _ = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
1201
+ self.clearPreviewSessionPreferences()
1202
+ }
1203
+
1204
+ private func restorePreviewPreviousNextBundle() {
1205
+ guard let previousNextBundleId = UserDefaults.standard.string(forKey: self.previewPreviousNextBundleDefaultsKey),
1206
+ !previousNextBundleId.isEmpty else {
1207
+ _ = self.implementation.setNextBundle(next: Optional<String>.none)
1208
+ return
1209
+ }
1210
+ if !self.implementation.setNextBundle(next: previousNextBundleId) {
1211
+ logger.warn("Could not restore pre-preview next bundle: \(previousNextBundleId)")
1212
+ _ = self.implementation.setNextBundle(next: Optional<String>.none)
1213
+ }
1214
+ }
1215
+
1216
+ private func showPreviewSessionNoticeIfNeeded() {
1217
+ guard self.previewSessionEnabled && self.previewSessionAlertPending else {
1218
+ return
1219
+ }
1220
+ self.previewSessionAlertPending = false
1221
+
1222
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(600)) {
1223
+ guard self.previewSessionEnabled else {
1224
+ return
1225
+ }
1226
+ if let topVC = UIApplication.topViewController(),
1227
+ topVC.isKind(of: UIAlertController.self) {
1228
+ self.previewSessionAlertPending = true
1229
+ return
1230
+ }
1231
+
1232
+ let alert = UIAlertController(
1233
+ title: "Preview started",
1234
+ message: "Shake your device anytime to reload or leave the test app.",
1235
+ preferredStyle: .alert
1236
+ )
1237
+ alert.addAction(UIAlertAction(title: "Got it", style: .default))
1238
+ if let topVC = UIApplication.topViewController() {
1239
+ topVC.present(alert, animated: true)
1240
+ }
1241
+ }
1242
+ }
1243
+
963
1244
  @objc func delete(_ call: CAPPluginCall) {
964
1245
  guard let id = call.getString("id") else {
965
1246
  logger.error("Delete called without version")
@@ -1019,12 +1300,27 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1019
1300
 
1020
1301
  @objc func getLatest(_ call: CAPPluginCall) {
1021
1302
  let channel = call.getString("channel")
1303
+ let includeBundleSize = call.getBool("includeBundleSize", false)
1304
+ let appId = self.normalizedPreviewAppId(call.getString("appId"))
1305
+ if appId != nil && !self.allowPreview {
1306
+ logger.error("getLatest preview override called without allowPreview")
1307
+ call.reject("getLatest preview override not allowed. Set allowPreview to true in your config to enable it.")
1308
+ return
1309
+ }
1022
1310
  self.saveCallForAsyncHandling(call)
1023
1311
  runGetLatestWork {
1024
- let res = self.implementation.getLatest(url: URL(string: self.updateUrl)!, channel: channel)
1312
+ let res = self.implementation.getLatest(
1313
+ url: URL(string: self.updateUrl)!,
1314
+ channel: channel,
1315
+ appIdOverride: appId
1316
+ )
1317
+ if includeBundleSize {
1318
+ self.attachBundleSize(to: res)
1319
+ }
1025
1320
  if let error = res.error, !error.isEmpty {
1026
1321
  let responseKind = self.updateResponseKind(kind: res.kind)
1027
1322
  res.kind = responseKind
1323
+ self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
1028
1324
  if responseKind == "failed" {
1029
1325
  self.rejectCall(call, message: error)
1030
1326
  } else {
@@ -1036,6 +1332,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1036
1332
  } else if let kind = res.kind, !kind.isEmpty {
1037
1333
  let responseKind = self.updateResponseKind(kind: kind)
1038
1334
  res.kind = responseKind
1335
+ self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
1039
1336
  if responseKind != "failed" {
1040
1337
  if res.version.isEmpty {
1041
1338
  res.version = self.implementation.getCurrentBundle().getVersionName()
@@ -1045,6 +1342,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1045
1342
  self.rejectCall(call, message: res.message ?? "server did not provide a message")
1046
1343
  }
1047
1344
  } else if let message = res.message, !message.isEmpty {
1345
+ self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
1048
1346
  self.rejectCall(call, message: message)
1049
1347
  } else {
1050
1348
  self.resolveCall(call, data: res.toDict())
@@ -1052,6 +1350,50 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1052
1350
  }
1053
1351
  }
1054
1352
 
1353
+ private func attachBundleSize(to res: AppVersion) {
1354
+ guard let manifest = res.manifest, !manifest.isEmpty, let updateUrl = URL(string: self.updateUrl) else {
1355
+ return
1356
+ }
1357
+ let missing = self.implementation.getMissingBundleFiles(manifest: manifest, sessionKey: res.sessionKey ?? "")
1358
+ res.missing = [
1359
+ "missing": missing.map { $0.toDict() },
1360
+ "total": manifest.count,
1361
+ "missingCount": missing.count,
1362
+ "reusableCount": manifest.count - missing.count
1363
+ ]
1364
+ res.downloadSize = self.implementation.getBundleDownloadSize(updateUrl: updateUrl, version: res.version, manifest: missing)
1365
+ }
1366
+
1367
+ @objc func getMissingBundleFiles(_ call: CAPPluginCall) {
1368
+ guard let manifest = manifestEntries(from: call.getArray("manifest")) else {
1369
+ call.reject("getMissingBundleFiles called without manifest")
1370
+ return
1371
+ }
1372
+ let sessionKey = call.getString("sessionKey", "")
1373
+ self.saveCallForAsyncHandling(call)
1374
+ DispatchQueue.global(qos: .utility).async {
1375
+ let res = self.implementation.missingBundleFilesResult(manifest: manifest, sessionKey: sessionKey)
1376
+ self.resolveCall(call, data: res)
1377
+ }
1378
+ }
1379
+
1380
+ @objc func getBundleDownloadSize(_ call: CAPPluginCall) {
1381
+ guard let manifest = manifestEntries(from: call.getArray("manifest")) else {
1382
+ call.reject("getBundleDownloadSize called without manifest")
1383
+ return
1384
+ }
1385
+ guard let updateUrl = URL(string: self.updateUrl) else {
1386
+ call.reject("getBundleDownloadSize called without valid updateUrl")
1387
+ return
1388
+ }
1389
+ let version = call.getString("version")
1390
+ self.saveCallForAsyncHandling(call)
1391
+ DispatchQueue.global(qos: .utility).async {
1392
+ let res = self.implementation.getBundleDownloadSize(updateUrl: updateUrl, version: version, manifest: manifest)
1393
+ self.resolveCall(call, data: res)
1394
+ }
1395
+ }
1396
+
1055
1397
  public func triggerBackgroundUpdateCheck() -> String {
1056
1398
  guard !self.updateUrl.isEmpty, URL(string: self.updateUrl) != nil else {
1057
1399
  logger.error("Error no url or wrong format")
@@ -1821,6 +2163,21 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1821
2163
  self.notifyListeners("majorAvailable", data: payload)
1822
2164
  }
1823
2165
 
2166
+ private func shouldNotifyBreakingEvents(response: AppVersion) -> Bool {
2167
+ if response.breaking == true {
2168
+ return true
2169
+ }
2170
+
2171
+ return response.error == "disable_auto_update_to_major" || response.message == "store_update_required"
2172
+ }
2173
+
2174
+ private func notifyBreakingEventsIfNeeded(response: AppVersion, version: String) {
2175
+ if self.shouldNotifyBreakingEvents(response: response) {
2176
+ let eventVersion = version.isEmpty ? self.implementation.getCurrentBundle().getVersionName() : version
2177
+ self.notifyBreakingEvents(version: eventVersion)
2178
+ }
2179
+ }
2180
+
1824
2181
  static func normalizedUpdateResponseKind(kind: String?) -> String {
1825
2182
  if let kind, ["up_to_date", "blocked", "failed"].contains(kind) {
1826
2183
  return kind
@@ -1851,6 +2208,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1851
2208
  "version": latestVersionName,
1852
2209
  "bundle": current.toJSON()
1853
2210
  ])
2211
+ self.notifyBreakingEventsIfNeeded(response: res, version: res.version)
1854
2212
 
1855
2213
  if responseKind == "up_to_date" {
1856
2214
  self.logger.info("No new version available")
@@ -2017,17 +2375,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2017
2375
  return
2018
2376
  }
2019
2377
  let sessionKey = res.sessionKey ?? ""
2378
+ let latestVersionName = res.version
2020
2379
  guard let downloadUrl = URL(string: res.url) else {
2380
+ self.notifyBreakingEventsIfNeeded(response: res, version: latestVersionName)
2021
2381
  self.logger.error("Error no url or wrong format")
2022
2382
  self.endBackGroundTaskWithNotif(
2023
2383
  msg: "Error no url or wrong format",
2024
- latestVersionName: res.version,
2384
+ latestVersionName: latestVersionName,
2025
2385
  current: current,
2026
2386
  plannedDirectUpdate: plannedDirectUpdate
2027
2387
  )
2028
2388
  return
2029
2389
  }
2030
- let latestVersionName = res.version
2031
2390
  if latestVersionName != "" && current.getVersionName() != latestVersionName {
2032
2391
  do {
2033
2392
  self.logger.info("New bundle: \(latestVersionName) found. Current is: \(current.getVersionName()). \(messageUpdate)")