@capgo/capacitor-updater 6.42.3 → 6.43.3

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.
@@ -579,10 +579,15 @@ import UIKit
579
579
 
580
580
  for entry in manifest {
581
581
  guard let fileName = entry.file_name,
582
- var fileHash = entry.file_hash,
583
582
  let downloadUrl = entry.download_url else {
584
583
  continue
585
584
  }
585
+ guard let entryFileHash = entry.file_hash, !entryFileHash.isEmpty else {
586
+ logger.error("Missing file_hash for manifest entry: \(entry.file_name ?? "unknown")")
587
+ hasError.value = true
588
+ continue
589
+ }
590
+ var fileHash = entryFileHash
586
591
 
587
592
  // Decrypt checksum if needed (done before creating operation)
588
593
  if !self.publicKey.isEmpty && !sessionKey.isEmpty {
@@ -749,16 +754,14 @@ import UIKit
749
754
  // Write to destination
750
755
  try finalData.write(to: destFilePath)
751
756
 
752
- // Verify checksum if encryption is enabled
753
- if !self.publicKey.isEmpty && !sessionKey.isEmpty {
754
- let calculatedChecksum = CryptoCipher.calcChecksum(filePath: destFilePath)
755
- CryptoCipher.logChecksumInfo(label: "Calculated checksum", hexChecksum: calculatedChecksum)
756
- CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: fileHash)
757
- if calculatedChecksum != fileHash {
758
- try? FileManager.default.removeItem(at: destFilePath)
759
- self.sendStats(action: "download_manifest_checksum_fail", versionName: "\(version):\(destFileName)")
760
- throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
761
- }
757
+ // Always verify checksum when file_hash is present
758
+ let calculatedChecksum = CryptoCipher.calcChecksum(filePath: destFilePath)
759
+ CryptoCipher.logChecksumInfo(label: "Calculated checksum", hexChecksum: calculatedChecksum)
760
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: fileHash)
761
+ if calculatedChecksum != fileHash {
762
+ try? FileManager.default.removeItem(at: destFilePath)
763
+ self.sendStats(action: "download_manifest_checksum_fail", versionName: "\(version):\(destFileName)")
764
+ throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
762
765
  }
763
766
 
764
767
  // Save to cache
@@ -1554,6 +1557,15 @@ import UIKit
1554
1557
  if let responseValue = response.value {
1555
1558
  if let error = responseValue.error {
1556
1559
  setChannel.error = error
1560
+ } else if responseValue.unset == true {
1561
+ // Server requested to unset channel (public channel was requested)
1562
+ // Clear persisted defaultChannel and revert to config value
1563
+ UserDefaults.standard.removeObject(forKey: defaultChannelKey)
1564
+ UserDefaults.standard.synchronize()
1565
+ self.logger.info("Public channel requested, channel override removed")
1566
+
1567
+ setChannel.status = responseValue.status ?? "ok"
1568
+ setChannel.message = responseValue.message ?? "Public channel requested, channel override removed. Device will use public channel automatically."
1557
1569
  } else {
1558
1570
  // Success - persist defaultChannel
1559
1571
  self.defaultChannel = channel
@@ -23,6 +23,7 @@ struct SetChannelDec: Decodable {
23
23
  let status: String?
24
24
  let error: String?
25
25
  let message: String?
26
+ let unset: Bool?
26
27
  }
27
28
  public class SetChannel: NSObject {
28
29
  var status: String = ""
@@ -37,11 +37,16 @@ extension UIWindow {
37
37
  return
38
38
  }
39
39
 
40
- showShakeMenu(plugin: plugin, bridge: bridge)
40
+ // Check if channel selector mode is enabled
41
+ if plugin.shakeChannelSelectorEnabled {
42
+ showChannelSelector(plugin: plugin, bridge: bridge)
43
+ } else {
44
+ showDefaultMenu(plugin: plugin, bridge: bridge)
45
+ }
41
46
  }
42
47
  }
43
48
 
44
- private func showShakeMenu(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) {
49
+ private func showDefaultMenu(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) {
45
50
  // Prevent multiple alerts from showing
46
51
  if let topVC = UIApplication.topViewController(),
47
52
  topVC.isKind(of: UIAlertController.self) {
@@ -110,4 +115,327 @@ extension UIWindow {
110
115
  }
111
116
  }
112
117
  }
118
+
119
+ private func showChannelSelector(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) {
120
+ // Prevent multiple alerts from showing
121
+ if let topVC = UIApplication.topViewController(),
122
+ topVC.isKind(of: UIAlertController.self) {
123
+ plugin.logger.info("UIAlertController is already presented")
124
+ return
125
+ }
126
+
127
+ let updater = plugin.implementation
128
+
129
+ // Show loading indicator
130
+ let loadingAlert = UIAlertController(title: "Loading Channels...", message: nil, preferredStyle: .alert)
131
+ var didCancel = false
132
+ let loadingIndicator = UIActivityIndicatorView(style: .medium)
133
+ loadingIndicator.translatesAutoresizingMaskIntoConstraints = false
134
+ loadingIndicator.startAnimating()
135
+ loadingAlert.view.addSubview(loadingIndicator)
136
+
137
+ NSLayoutConstraint.activate([
138
+ loadingIndicator.centerXAnchor.constraint(equalTo: loadingAlert.view.centerXAnchor),
139
+ loadingIndicator.bottomAnchor.constraint(equalTo: loadingAlert.view.bottomAnchor, constant: -20)
140
+ ])
141
+
142
+ // Add cancel button to loading alert
143
+ loadingAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
144
+ didCancel = true
145
+ })
146
+
147
+ DispatchQueue.main.async {
148
+ if let topVC = UIApplication.topViewController() {
149
+ topVC.present(loadingAlert, animated: true) {
150
+ // Fetch channels in background
151
+ DispatchQueue.global(qos: .userInitiated).async {
152
+ let result = updater.listChannels()
153
+
154
+ DispatchQueue.main.async {
155
+ loadingAlert.dismiss(animated: true) {
156
+ guard !didCancel else { return }
157
+ if !result.error.isEmpty {
158
+ self.showError(message: "Failed to load channels: \(result.error)", plugin: plugin)
159
+ } else if result.channels.isEmpty {
160
+ self.showError(message: "No channels available for self-assignment", plugin: plugin)
161
+ } else {
162
+ self.presentChannelPicker(channels: result.channels, plugin: plugin, bridge: bridge)
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ private func presentChannelPicker(channels: [[String: Any]], plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) {
173
+ let alert = UIAlertController(title: "Select Channel", message: "Choose a channel to switch to", preferredStyle: .actionSheet)
174
+
175
+ // Get channel names
176
+ let channelNames = channels.compactMap { $0["name"] as? String }
177
+
178
+ // Show first 5 channels as actions
179
+ let channelsToShow = Array(channelNames.prefix(5))
180
+
181
+ for channelName in channelsToShow {
182
+ alert.addAction(UIAlertAction(title: channelName, style: .default) { [weak self] _ in
183
+ self?.selectChannel(name: channelName, plugin: plugin, bridge: bridge)
184
+ })
185
+ }
186
+
187
+ // If there are more channels, add a "More..." option
188
+ if channelNames.count > 5 {
189
+ alert.addAction(UIAlertAction(title: "More channels...", style: .default) { [weak self] _ in
190
+ self?.showSearchableChannelPicker(channels: channels, plugin: plugin, bridge: bridge)
191
+ })
192
+ }
193
+
194
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
195
+
196
+ // For iPad support
197
+ if let popoverController = alert.popoverPresentationController {
198
+ popoverController.sourceView = self
199
+ popoverController.sourceRect = CGRect(x: self.bounds.midX, y: self.bounds.midY, width: 0, height: 0)
200
+ popoverController.permittedArrowDirections = []
201
+ }
202
+
203
+ DispatchQueue.main.async {
204
+ if let topVC = UIApplication.topViewController() {
205
+ topVC.present(alert, animated: true)
206
+ }
207
+ }
208
+ }
209
+
210
+ private func showSearchableChannelPicker(channels: [[String: Any]], plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) {
211
+ let alert = UIAlertController(title: "Search Channels", message: "Enter channel name to search", preferredStyle: .alert)
212
+
213
+ alert.addTextField { textField in
214
+ textField.placeholder = "Channel name..."
215
+ }
216
+
217
+ let channelNames = channels.compactMap { $0["name"] as? String }
218
+
219
+ alert.addAction(UIAlertAction(title: "Search", style: .default) { [weak self, weak alert] _ in
220
+ guard let searchText = alert?.textFields?.first?.text?.lowercased(), !searchText.isEmpty else {
221
+ // If empty, show first 5
222
+ self?.presentChannelPicker(channels: channels, plugin: plugin, bridge: bridge)
223
+ return
224
+ }
225
+
226
+ // Filter channels
227
+ let filtered = channelNames.filter { $0.lowercased().contains(searchText) }
228
+
229
+ if filtered.isEmpty {
230
+ self?.showError(message: "No channels found matching '\(searchText)'", plugin: plugin)
231
+ } else if filtered.count == 1 {
232
+ // Directly select if only one match
233
+ self?.selectChannel(name: filtered[0], plugin: plugin, bridge: bridge)
234
+ } else {
235
+ // Show filtered results
236
+ let filteredChannels = channels.filter { channel in
237
+ if let name = channel["name"] as? String {
238
+ return name.lowercased().contains(searchText)
239
+ }
240
+ return false
241
+ }
242
+ self?.presentChannelPicker(channels: filteredChannels, plugin: plugin, bridge: bridge)
243
+ }
244
+ })
245
+
246
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
247
+
248
+ DispatchQueue.main.async {
249
+ if let topVC = UIApplication.topViewController() {
250
+ topVC.present(alert, animated: true)
251
+ }
252
+ }
253
+ }
254
+
255
+ private func selectChannel(name: String, plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) {
256
+ let updater = plugin.implementation
257
+
258
+ // Show progress indicator
259
+ let progressAlert = UIAlertController(title: "Switching to \(name)", message: "Setting channel...", preferredStyle: .alert)
260
+ let indicator = UIActivityIndicatorView(style: .medium)
261
+ indicator.translatesAutoresizingMaskIntoConstraints = false
262
+ indicator.startAnimating()
263
+ progressAlert.view.addSubview(indicator)
264
+
265
+ NSLayoutConstraint.activate([
266
+ indicator.centerXAnchor.constraint(equalTo: progressAlert.view.centerXAnchor),
267
+ indicator.bottomAnchor.constraint(equalTo: progressAlert.view.bottomAnchor, constant: -20)
268
+ ])
269
+
270
+ DispatchQueue.main.async {
271
+ if let topVC = UIApplication.topViewController() {
272
+ topVC.present(progressAlert, animated: true) {
273
+ DispatchQueue.global(qos: .userInitiated).async {
274
+ // Set the channel - respect plugin's allowSetDefaultChannel config
275
+ let setResult = updater.setChannel(
276
+ channel: name,
277
+ defaultChannelKey: "CapacitorUpdater.defaultChannel",
278
+ allowSetDefaultChannel: plugin.allowSetDefaultChannel
279
+ )
280
+
281
+ if !setResult.error.isEmpty {
282
+ DispatchQueue.main.async {
283
+ progressAlert.dismiss(animated: true) {
284
+ self.showError(message: "Failed to set channel: \(setResult.error)", plugin: plugin)
285
+ }
286
+ }
287
+ return
288
+ }
289
+
290
+ // Update progress message
291
+ DispatchQueue.main.async {
292
+ progressAlert.message = "Checking for updates..."
293
+ }
294
+
295
+ // Check for updates with the new channel
296
+ let pluginUpdateUrl = plugin.getUpdateUrl()
297
+ let updateUrlStr = pluginUpdateUrl.isEmpty ? CapacitorUpdaterPlugin.updateUrlDefault : pluginUpdateUrl
298
+ guard let updateUrl = URL(string: updateUrlStr) else {
299
+ DispatchQueue.main.async {
300
+ progressAlert.dismiss(animated: true) {
301
+ self.showError(
302
+ message: "Channel set to \(name). Invalid update URL, could not check for updates.",
303
+ plugin: plugin
304
+ )
305
+ }
306
+ }
307
+ return
308
+ }
309
+
310
+ let latest = updater.getLatest(url: updateUrl, channel: name)
311
+
312
+ // Handle update errors first (before "no new version" check)
313
+ if let error = latest.error, !error.isEmpty && error != "no_new_version_available" {
314
+ DispatchQueue.main.async {
315
+ progressAlert.dismiss(animated: true) {
316
+ self.showError(message: "Channel set to \(name). Update check failed: \(error)", plugin: plugin)
317
+ }
318
+ }
319
+ return
320
+ }
321
+
322
+ // Check if there's an actual update available
323
+ if latest.error == "no_new_version_available" || latest.url.isEmpty {
324
+ DispatchQueue.main.async {
325
+ progressAlert.dismiss(animated: true) {
326
+ self.showSuccess(message: "Channel set to \(name). Already on latest version.", plugin: plugin)
327
+ }
328
+ }
329
+ return
330
+ }
331
+
332
+ // Update message
333
+ DispatchQueue.main.async {
334
+ progressAlert.message = "Downloading update \(latest.version)..."
335
+ }
336
+
337
+ // Download the update
338
+ do {
339
+ let bundle: BundleInfo
340
+ if let manifest = latest.manifest, !manifest.isEmpty {
341
+ bundle = try updater.downloadManifest(
342
+ manifest: manifest,
343
+ version: latest.version,
344
+ sessionKey: latest.sessionKey ?? ""
345
+ )
346
+ } else {
347
+ // Safe unwrap URL
348
+ guard let downloadUrl = URL(string: latest.url) else {
349
+ DispatchQueue.main.async {
350
+ progressAlert.dismiss(animated: true) {
351
+ self.showError(message: "Failed to download update: invalid update URL.", plugin: plugin)
352
+ }
353
+ }
354
+ return
355
+ }
356
+ bundle = try updater.download(
357
+ url: downloadUrl,
358
+ version: latest.version,
359
+ sessionKey: latest.sessionKey ?? ""
360
+ )
361
+ }
362
+
363
+ // Set as next bundle
364
+ _ = updater.setNextBundle(next: bundle.getId())
365
+
366
+ DispatchQueue.main.async {
367
+ progressAlert.dismiss(animated: true) {
368
+ self.showSuccessWithReload(
369
+ message: "Update downloaded! Reload to apply version \(latest.version)?",
370
+ plugin: plugin,
371
+ bridge: bridge,
372
+ onReload: { [weak plugin] in
373
+ _ = updater.set(bundle: bundle)
374
+ _ = plugin?._reload()
375
+ }
376
+ )
377
+ }
378
+ }
379
+ } catch {
380
+ DispatchQueue.main.async {
381
+ progressAlert.dismiss(animated: true) {
382
+ self.showError(message: "Failed to download update: \(error.localizedDescription)", plugin: plugin)
383
+ }
384
+ }
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ private func showError(message: String, plugin: CapacitorUpdaterPlugin) {
393
+ plugin.logger.error(message)
394
+ let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
395
+ alert.addAction(UIAlertAction(title: "OK", style: .default))
396
+
397
+ DispatchQueue.main.async {
398
+ if let topVC = UIApplication.topViewController() {
399
+ topVC.present(alert, animated: true)
400
+ }
401
+ }
402
+ }
403
+
404
+ private func showSuccess(message: String, plugin: CapacitorUpdaterPlugin) {
405
+ plugin.logger.info(message)
406
+ let alert = UIAlertController(title: "Success", message: message, preferredStyle: .alert)
407
+ alert.addAction(UIAlertAction(title: "OK", style: .default))
408
+
409
+ DispatchQueue.main.async {
410
+ if let topVC = UIApplication.topViewController() {
411
+ topVC.present(alert, animated: true)
412
+ }
413
+ }
414
+ }
415
+
416
+ private func showSuccessWithReload(
417
+ message: String,
418
+ plugin: CapacitorUpdaterPlugin,
419
+ bridge: CAPBridgeProtocol,
420
+ onReload: (() -> Void)? = nil
421
+ ) {
422
+ plugin.logger.info(message)
423
+ let alert = UIAlertController(title: "Update Ready", message: message, preferredStyle: .alert)
424
+ alert.addAction(UIAlertAction(title: "Later", style: .cancel))
425
+ alert.addAction(UIAlertAction(title: "Reload Now", style: .default) { _ in
426
+ if let onReload = onReload {
427
+ onReload()
428
+ } else {
429
+ DispatchQueue.main.async {
430
+ bridge.webView?.reload()
431
+ }
432
+ }
433
+ })
434
+
435
+ DispatchQueue.main.async {
436
+ if let topVC = UIApplication.topViewController() {
437
+ topVC.present(alert, animated: true)
438
+ }
439
+ }
440
+ }
113
441
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "6.42.3",
3
+ "version": "6.43.3",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",
@@ -57,7 +57,8 @@
57
57
  "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
58
58
  "clean": "rimraf ./dist",
59
59
  "watch": "tsc --watch",
60
- "prepublishOnly": "npm run build"
60
+ "prepublishOnly": "npm run build",
61
+ "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs"
61
62
  },
62
63
  "devDependencies": {
63
64
  "@capacitor/android": "^6.1.2",