@capgo/capacitor-updater 7.20.0 → 7.22.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.
@@ -7,6 +7,7 @@
7
7
  import Foundation
8
8
  import Capacitor
9
9
  import UIKit
10
+ import WebKit
10
11
  import Version
11
12
 
12
13
  /**
@@ -27,6 +28,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
27
28
  CAPPluginMethod(name: "set", returnType: CAPPluginReturnPromise),
28
29
  CAPPluginMethod(name: "list", returnType: CAPPluginReturnPromise),
29
30
  CAPPluginMethod(name: "delete", returnType: CAPPluginReturnPromise),
31
+ CAPPluginMethod(name: "setBundleError", returnType: CAPPluginReturnPromise),
30
32
  CAPPluginMethod(name: "reset", returnType: CAPPluginReturnPromise),
31
33
  CAPPluginMethod(name: "current", returnType: CAPPluginReturnPromise),
32
34
  CAPPluginMethod(name: "reload", returnType: CAPPluginReturnPromise),
@@ -47,18 +49,21 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
47
49
  CAPPluginMethod(name: "getBuiltinVersion", returnType: CAPPluginReturnPromise),
48
50
  CAPPluginMethod(name: "isAutoUpdateAvailable", returnType: CAPPluginReturnPromise),
49
51
  CAPPluginMethod(name: "getNextBundle", returnType: CAPPluginReturnPromise),
52
+ CAPPluginMethod(name: "getFailedUpdate", returnType: CAPPluginReturnPromise),
50
53
  CAPPluginMethod(name: "setShakeMenu", returnType: CAPPluginReturnPromise),
51
54
  CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise)
52
55
  ]
53
56
  public var implementation = CapgoUpdater()
54
- private let PLUGIN_VERSION: String = "7.20.0"
57
+ private let PLUGIN_VERSION: String = "7.22.0"
55
58
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
56
59
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
57
60
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
61
+ private let keepUrlPathFlagKey = "__capgo_keep_url_path_after_reload"
58
62
  private let customIdDefaultsKey = "CapacitorUpdater.customId"
59
63
  private let updateUrlDefaultsKey = "CapacitorUpdater.updateUrl"
60
64
  private let statsUrlDefaultsKey = "CapacitorUpdater.statsUrl"
61
65
  private let channelUrlDefaultsKey = "CapacitorUpdater.channelUrl"
66
+ private let lastFailedBundleDefaultsKey = "CapacitorUpdater.lastFailedBundle"
62
67
  // Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
63
68
  private var updateUrl = ""
64
69
  private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
@@ -86,6 +91,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
86
91
  private var periodCheckDelay = 0
87
92
  private var persistCustomId = false
88
93
  private var persistModifyUrl = false
94
+ private var allowManualBundleError = false
95
+ private var keepUrlPathFlagLastValue: Bool?
89
96
  public var shakeMenuEnabled = false
90
97
  let semaphoreReady = DispatchSemaphore(value: 0)
91
98
 
@@ -118,6 +125,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
118
125
  }
119
126
  }
120
127
  persistModifyUrl = getConfig().getBoolean("persistModifyUrl", false)
128
+ allowManualBundleError = getConfig().getBoolean("allowManualBundleError", false)
121
129
  logger.info("init for device \(self.implementation.deviceID)")
122
130
  guard let versionName = getConfig().getString("version", Bundle.main.versionName) else {
123
131
  logger.error("Cannot get version name")
@@ -135,6 +143,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
135
143
  autoDeleteFailed = getConfig().getBoolean("autoDeleteFailed", true)
136
144
  autoDeletePrevious = getConfig().getBoolean("autoDeletePrevious", true)
137
145
  keepUrlPathAfterReload = getConfig().getBoolean("keepUrlPathAfterReload", false)
146
+ syncKeepUrlPathFlag(enabled: keepUrlPathAfterReload)
138
147
 
139
148
  // Handle directUpdate configuration - support string values and backward compatibility
140
149
  if let directUpdateString = getConfig().getString("directUpdate") {
@@ -232,8 +241,58 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
232
241
  self.checkForUpdateAfterDelay()
233
242
  }
234
243
 
244
+ private func syncKeepUrlPathFlag(enabled: Bool) {
245
+ let script: String
246
+ if enabled {
247
+ script = "(function(){ try { localStorage.setItem('\(keepUrlPathFlagKey)', '1'); } catch (err) {} window.__capgoKeepUrlPathAfterReload = true; var evt; try { evt = new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', { detail: { enabled: true } }); } catch (e) { evt = document.createEvent('CustomEvent'); evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', false, false, { enabled: true }); } window.dispatchEvent(evt); })();"
248
+ } else {
249
+ script = "(function(){ try { localStorage.removeItem('\(keepUrlPathFlagKey)'); } catch (err) {} delete window.__capgoKeepUrlPathAfterReload; var evt; try { evt = new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', { detail: { enabled: false } }); } catch (e) { evt = document.createEvent('CustomEvent'); evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload', false, false, { enabled: false }); } window.dispatchEvent(evt); })();"
250
+ }
251
+ DispatchQueue.main.async { [weak self] in
252
+ guard let self = self, let webView = self.bridge?.webView else {
253
+ return
254
+ }
255
+ if self.keepUrlPathFlagLastValue != enabled {
256
+ let userScript = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: true)
257
+ webView.configuration.userContentController.addUserScript(userScript)
258
+ self.keepUrlPathFlagLastValue = enabled
259
+ }
260
+ webView.evaluateJavaScript(script, completionHandler: nil)
261
+ }
262
+ }
263
+
264
+ private func persistLastFailedBundle(_ bundle: BundleInfo?) {
265
+ if let bundle = bundle {
266
+ do {
267
+ try UserDefaults.standard.setObj(bundle, forKey: lastFailedBundleDefaultsKey)
268
+ } catch {
269
+ logger.error("Failed to persist failed bundle info \(error.localizedDescription)")
270
+ }
271
+ } else {
272
+ UserDefaults.standard.removeObject(forKey: lastFailedBundleDefaultsKey)
273
+ }
274
+ UserDefaults.standard.synchronize()
275
+ }
276
+
277
+ private func readLastFailedBundle() -> BundleInfo? {
278
+ do {
279
+ let bundle: BundleInfo = try UserDefaults.standard.getObj(forKey: lastFailedBundleDefaultsKey, castTo: BundleInfo.self)
280
+ return bundle
281
+ } catch ObjectSavableError.noValue {
282
+ return nil
283
+ } catch {
284
+ logger.error("Failed to read failed bundle info \(error.localizedDescription)")
285
+ UserDefaults.standard.removeObject(forKey: lastFailedBundleDefaultsKey)
286
+ UserDefaults.standard.synchronize()
287
+ return nil
288
+ }
289
+ }
290
+
235
291
  private func initialLoad() -> Bool {
236
292
  guard let bridge = self.bridge else { return false }
293
+ if keepUrlPathAfterReload {
294
+ syncKeepUrlPathFlag(enabled: true)
295
+ }
237
296
 
238
297
  let id = self.implementation.getCurrentBundleId()
239
298
  var dest: URL
@@ -460,34 +519,50 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
460
519
  dest = self.implementation.getBundleDirectory(id: id)
461
520
  }
462
521
  logger.info("Reloading \(id)")
463
- if let vc = bridge.viewController as? CAPBridgeViewController {
522
+
523
+ let performReload: () -> Bool = {
524
+ guard let vc = bridge.viewController as? CAPBridgeViewController else {
525
+ self.logger.error("Cannot get viewController")
526
+ return false
527
+ }
464
528
  guard let capBridge = vc.bridge else {
465
- logger.error("Cannot get capBridge")
529
+ self.logger.error("Cannot get capBridge")
466
530
  return false
467
531
  }
468
- if keepUrlPathAfterReload {
469
- DispatchQueue.main.async {
470
- guard let url = vc.webView?.url else {
471
- self.logger.error("vc.webView?.url is null?")
472
- return
473
- }
532
+ if self.keepUrlPathAfterReload {
533
+ if let currentURL = vc.webView?.url {
474
534
  capBridge.setServerBasePath(dest.path)
475
535
  var urlComponents = URLComponents(url: capBridge.config.serverURL, resolvingAgainstBaseURL: false)!
476
- urlComponents.path = url.path
536
+ urlComponents.path = currentURL.path
537
+ urlComponents.query = currentURL.query
538
+ urlComponents.fragment = currentURL.fragment
477
539
  if let finalUrl = urlComponents.url {
478
540
  _ = vc.webView?.load(URLRequest(url: finalUrl))
541
+ } else {
542
+ self.logger.error("Unable to build final URL when keeping path after reload; falling back to base path")
543
+ vc.setServerBasePath(path: dest.path)
479
544
  }
545
+ } else {
546
+ self.logger.error("vc.webView?.url is null? Falling back to base path reload.")
547
+ vc.setServerBasePath(path: dest.path)
480
548
  }
481
549
  } else {
482
550
  vc.setServerBasePath(path: dest.path)
483
-
484
551
  }
485
-
486
552
  self.checkAppReady()
487
553
  self.notifyListeners("appReloaded", data: [:])
488
554
  return true
489
555
  }
490
- return false
556
+
557
+ if Thread.isMainThread {
558
+ return performReload()
559
+ } else {
560
+ var result = false
561
+ DispatchQueue.main.sync {
562
+ result = performReload()
563
+ }
564
+ return result
565
+ }
491
566
  }
492
567
 
493
568
  @objc func reload(_ call: CAPPluginCall) {
@@ -545,6 +620,36 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
545
620
  }
546
621
  }
547
622
 
623
+ @objc func setBundleError(_ call: CAPPluginCall) {
624
+ if !allowManualBundleError {
625
+ logger.error("setBundleError called without allowManualBundleError")
626
+ call.reject("setBundleError not allowed. Set allowManualBundleError to true in your config to enable it.")
627
+ return
628
+ }
629
+ guard let id = call.getString("id") else {
630
+ logger.error("setBundleError called without id")
631
+ call.reject("setBundleError called without id")
632
+ return
633
+ }
634
+ let bundle = implementation.getBundleInfo(id: id)
635
+ if bundle.isUnknown() {
636
+ logger.error("setBundleError called with unknown bundle \(id)")
637
+ call.reject("Bundle \(id) does not exist")
638
+ return
639
+ }
640
+ if bundle.isBuiltin() {
641
+ logger.error("setBundleError called on builtin bundle")
642
+ call.reject("Cannot set builtin bundle to error state")
643
+ return
644
+ }
645
+ if self._isAutoUpdateEnabled() {
646
+ logger.warn("setBundleError used while autoUpdate is enabled; this method is intended for manual mode")
647
+ }
648
+ implementation.setError(bundle: bundle)
649
+ let updated = implementation.getBundleInfo(id: id)
650
+ call.resolve(["bundle": updated.toJSON()])
651
+ }
652
+
548
653
  @objc func list(_ call: CAPPluginCall) {
549
654
  let raw = call.getBool("raw", false)
550
655
  let res = implementation.list(raw: raw)
@@ -804,6 +909,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
804
909
  self.notifyListeners("updateFailed", data: [
805
910
  "bundle": current.toJSON()
806
911
  ])
912
+ self.persistLastFailedBundle(current)
807
913
  self.implementation.sendStats(action: "update_fail", versionName: current.getVersionName())
808
914
  self.implementation.setError(bundle: current)
809
915
  _ = self._reset(toLastSuccessful: true)
@@ -1085,6 +1191,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1085
1191
  }
1086
1192
  }
1087
1193
 
1194
+ private func notifyBreakingEvents(version: String) {
1195
+ guard !version.isEmpty else {
1196
+ return
1197
+ }
1198
+ let payload: [String: Any] = ["version": version]
1199
+ self.notifyListeners("breakingAvailable", data: payload)
1200
+ self.notifyListeners("majorAvailable", data: payload)
1201
+ }
1202
+
1088
1203
  func endBackGroundTaskWithNotif(
1089
1204
  msg: String,
1090
1205
  latestVersionName: String,
@@ -1144,8 +1259,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1144
1259
 
1145
1260
  if let message = res.message, !message.isEmpty {
1146
1261
  self.logger.info("API message: \(message)")
1147
- if res.major == true {
1148
- self.notifyListeners("majorAvailable", data: ["version": res.version])
1262
+ if res.breaking == true || res.major == true {
1263
+ self.notifyBreakingEvents(version: res.version)
1149
1264
  }
1150
1265
  self.endBackGroundTaskWithNotif(
1151
1266
  msg: message,
@@ -1410,6 +1525,19 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1410
1525
  call.resolve(bundle!.toJSON())
1411
1526
  }
1412
1527
 
1528
+ @objc func getFailedUpdate(_ call: CAPPluginCall) {
1529
+ let bundle = self.readLastFailedBundle()
1530
+ if bundle == nil || bundle?.isUnknown() == true {
1531
+ call.resolve()
1532
+ return
1533
+ }
1534
+
1535
+ self.persistLastFailedBundle(nil)
1536
+ call.resolve([
1537
+ "bundle": bundle!.toJSON()
1538
+ ])
1539
+ }
1540
+
1413
1541
  @objc func setShakeMenu(_ call: CAPPluginCall) {
1414
1542
  guard let enabled = call.getBool("enabled") else {
1415
1543
  logger.error("setShakeMenu called without enabled parameter")
@@ -289,6 +289,9 @@ import UIKit
289
289
  if let major = response.value?.major {
290
290
  latest.major = major
291
291
  }
292
+ if let breaking = response.value?.breaking {
293
+ latest.breaking = breaking
294
+ }
292
295
  if let error = response.value?.error {
293
296
  latest.error = error
294
297
  }
@@ -157,6 +157,7 @@ struct AppVersionDec: Decodable {
157
157
  let error: String?
158
158
  let session_key: String?
159
159
  let major: Bool?
160
+ let breaking: Bool?
160
161
  let data: [String: String]?
161
162
  let manifest: [ManifestEntry]?
162
163
  }
@@ -169,6 +170,7 @@ public class AppVersion: NSObject {
169
170
  var error: String?
170
171
  var sessionKey: String?
171
172
  var major: Bool?
173
+ var breaking: Bool?
172
174
  var data: [String: String]?
173
175
  var manifest: [ManifestEntry]?
174
176
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "7.20.0",
3
+ "version": "7.22.0",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",