@capgo/capacitor-updater 8.46.3 → 8.47.1

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.3"
82
+ private let pluginVersion: String = "8.47.1"
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,9 +1300,23 @@ 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
@@ -1055,6 +1350,50 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1055
1350
  }
1056
1351
  }
1057
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
+
1058
1397
  public func triggerBackgroundUpdateCheck() -> String {
1059
1398
  guard !self.updateUrl.isEmpty, URL(string: self.updateUrl) != nil else {
1060
1399
  logger.error("Error no url or wrong format")