@capgo/capacitor-updater 8.51.24 → 8.52.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.
- package/CapgoCapacitorUpdater.podspec +0 -3
- package/Package.swift +3 -11
- package/README.md +4 -1
- package/android/build.gradle +0 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +183 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +70 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +0 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +80 -21
- package/android/src/main/java/ee/forgr/capacitor_updater/Version.java +306 -0
- package/dist/docs.json +16 -0
- package/dist/esm/definitions.d.ts +14 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +113 -41
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoSemanticVersion.swift +162 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +226 -72
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +3 -4
- package/ios/Sources/CapacitorUpdaterPlugin/RedirectPolicyDelegate.swift +53 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ZipArchiveReader.swift +247 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ZipCentralDirectory.swift +280 -0
- package/package.json +1 -1
|
@@ -8,7 +8,6 @@ import Foundation
|
|
|
8
8
|
import Capacitor
|
|
9
9
|
import UIKit
|
|
10
10
|
import WebKit
|
|
11
|
-
import Version
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Please read the Capacitor iOS Plugin Development Guide
|
|
@@ -96,7 +95,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
96
95
|
deinit {
|
|
97
96
|
implementation.shutdown()
|
|
98
97
|
}
|
|
99
|
-
private let pluginVersion: String = "8.
|
|
98
|
+
private let pluginVersion: String = "8.52.0"
|
|
100
99
|
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
101
100
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
102
101
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
@@ -147,7 +146,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
147
146
|
// Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
|
|
148
147
|
private var updateUrl = ""
|
|
149
148
|
private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
|
|
150
|
-
private var currentVersionNative:
|
|
149
|
+
private var currentVersionNative = CapgoSemanticVersion(major: 0, minor: 0, patch: 0)
|
|
151
150
|
private var currentBuildVersion: String = "0"
|
|
152
151
|
private var autoUpdate = false
|
|
153
152
|
private var autoUpdateMode = CapacitorUpdaterPlugin.autoUpdateModeOff
|
|
@@ -219,6 +218,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
219
218
|
// Best-effort flag: set before we expect notifyAppReady (load/reload).
|
|
220
219
|
// No lock — never held across waits; a rare race only mis-times one wait.
|
|
221
220
|
private var pendingNotifyAppReady = false
|
|
221
|
+
// Armed only after a reload. The next document stamps this generation into notifyAppReady.
|
|
222
|
+
// A call from the previous page has no matching generation and must not mark the new bundle successful.
|
|
223
|
+
private var readyGuardArmed = false
|
|
224
|
+
private var readyGeneration = 0
|
|
222
225
|
private let semaphoreWaitTestingLock = NSLock()
|
|
223
226
|
private var didEnterSemaphoreWaitForTesting = false
|
|
224
227
|
|
|
@@ -264,7 +267,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
264
267
|
fatalError("Cannot get version name")
|
|
265
268
|
}
|
|
266
269
|
do {
|
|
267
|
-
currentVersionNative = try
|
|
270
|
+
currentVersionNative = try CapgoSemanticVersion(versionName)
|
|
268
271
|
} catch {
|
|
269
272
|
logger.error("Cannot parse versionName \(versionName)")
|
|
270
273
|
}
|
|
@@ -288,6 +291,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
288
291
|
configureAutoUpdateModeFromConfig()
|
|
289
292
|
appReadyTimeout = max(1000, getConfig().getInt("appReadyTimeout", 10000)) // Minimum 1 second
|
|
290
293
|
implementation.timeout = Double(getConfig().getInt("responseTimeout", 20))
|
|
294
|
+
implementation.allowHttpsToHttpRedirect = getConfig().getBoolean("allowHttpsToHttpRedirect", false)
|
|
291
295
|
resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
|
|
292
296
|
shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
|
|
293
297
|
shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
@@ -308,16 +312,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
308
312
|
implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
|
|
309
313
|
implementation.notifyDownloadRaw = notifyDownload
|
|
310
314
|
implementation.notifyListeners = { [weak self] eventName, data in
|
|
311
|
-
|
|
312
|
-
self?.notifyListeners(eventName, data: data)
|
|
313
|
-
}
|
|
314
|
-
if Thread.isMainThread {
|
|
315
|
-
emit()
|
|
316
|
-
} else {
|
|
317
|
-
DispatchQueue.main.async {
|
|
318
|
-
emit()
|
|
319
|
-
}
|
|
320
|
-
}
|
|
315
|
+
self?.notifyListenersOnMain(eventName, data: data)
|
|
321
316
|
}
|
|
322
317
|
implementation.pluginVersion = self.pluginVersion
|
|
323
318
|
|
|
@@ -1244,9 +1239,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1244
1239
|
call.keepAlive = true
|
|
1245
1240
|
}
|
|
1246
1241
|
|
|
1247
|
-
|
|
1242
|
+
/// CAPPlugin's listener storage is not thread-safe (ionic-team/capacitor#8157), so every
|
|
1243
|
+
/// event this plugin emits goes through the main thread. Calls already on main stay synchronous.
|
|
1244
|
+
private func notifyListenersOnMain(_ eventName: String, data: [String: Any]?, retainUntilConsumed: Bool = false) {
|
|
1248
1245
|
let notify = {
|
|
1249
|
-
self.notifyListeners(eventName, data: data)
|
|
1246
|
+
self.notifyListeners(eventName, data: data, retainUntilConsumed: retainUntilConsumed)
|
|
1250
1247
|
}
|
|
1251
1248
|
|
|
1252
1249
|
if Thread.isMainThread {
|
|
@@ -1716,6 +1713,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1716
1713
|
self.logger.error("Cannot get capBridge")
|
|
1717
1714
|
return false
|
|
1718
1715
|
}
|
|
1716
|
+
self.armReadyGuard(webView: vc.webView)
|
|
1719
1717
|
if self.keepUrlPathAfterReload {
|
|
1720
1718
|
if let currentURL = vc.webView?.url {
|
|
1721
1719
|
capBridge.setServerBasePath(dest.path)
|
|
@@ -1767,7 +1765,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1767
1765
|
return false
|
|
1768
1766
|
}
|
|
1769
1767
|
self.checkAppReady()
|
|
1770
|
-
self.
|
|
1768
|
+
self.notifyListenersOnMain("appReloaded", data: [:])
|
|
1771
1769
|
return true
|
|
1772
1770
|
}
|
|
1773
1771
|
|
|
@@ -1790,7 +1788,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1790
1788
|
return false
|
|
1791
1789
|
}
|
|
1792
1790
|
self.checkAppReady()
|
|
1793
|
-
self.
|
|
1791
|
+
self.notifyListenersOnMain("appReloaded", data: [:])
|
|
1794
1792
|
return true
|
|
1795
1793
|
}
|
|
1796
1794
|
|
|
@@ -2705,12 +2703,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2705
2703
|
var response: URLResponse?
|
|
2706
2704
|
var responseError: Error?
|
|
2707
2705
|
|
|
2708
|
-
|
|
2706
|
+
self.implementation.startRawDataTask(request) { data, urlResponse, error in
|
|
2709
2707
|
responseData = data
|
|
2710
2708
|
response = urlResponse
|
|
2711
2709
|
responseError = error
|
|
2712
2710
|
semaphore.signal()
|
|
2713
|
-
}
|
|
2711
|
+
}
|
|
2714
2712
|
|
|
2715
2713
|
if semaphore.wait(timeout: .now() + 60) == .timedOut {
|
|
2716
2714
|
throw makePreviewError("Preview payload request timed out")
|
|
@@ -3025,11 +3023,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3025
3023
|
self.saveCallForAsyncHandling(call)
|
|
3026
3024
|
DispatchQueue.global(qos: .utility).async {
|
|
3027
3025
|
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
3028
|
-
let res = self.implementation.unsetChannel(
|
|
3026
|
+
let res = self.implementation.unsetChannel(
|
|
3027
|
+
defaultChannelKey: self.defaultChannelDefaultsKey,
|
|
3028
|
+
configDefaultChannel: configDefaultChannel,
|
|
3029
|
+
allowSetDefaultChannel: self.allowSetDefaultChannel
|
|
3030
|
+
)
|
|
3029
3031
|
if res.error != "" {
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
"
|
|
3032
|
+
let message = res.message.isEmpty ? res.error : res.message
|
|
3033
|
+
self.rejectCall(call, message: message, code: "UNSETCHANNEL_FAILED", data: [
|
|
3034
|
+
"message": message,
|
|
3035
|
+
"error": res.error
|
|
3033
3036
|
])
|
|
3034
3037
|
} else {
|
|
3035
3038
|
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
@@ -3077,9 +3080,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3077
3080
|
"message": res.error
|
|
3078
3081
|
])
|
|
3079
3082
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
+
let message = res.message.isEmpty ? res.error : res.message
|
|
3084
|
+
let errorCode: String
|
|
3085
|
+
if res.error == "disabled_by_config" {
|
|
3086
|
+
errorCode = res.error
|
|
3087
|
+
} else if res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed") {
|
|
3088
|
+
errorCode = "channel_private"
|
|
3089
|
+
} else if res.error.contains("Channel URL") {
|
|
3090
|
+
errorCode = "missing_config"
|
|
3091
|
+
} else {
|
|
3092
|
+
errorCode = "request_failed"
|
|
3093
|
+
}
|
|
3094
|
+
self.rejectCall(call, message: message, code: "SETCHANNEL_FAILED", data: [
|
|
3095
|
+
"message": message,
|
|
3096
|
+
"error": errorCode
|
|
3083
3097
|
])
|
|
3084
3098
|
} else {
|
|
3085
3099
|
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
@@ -3309,7 +3323,65 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3309
3323
|
)
|
|
3310
3324
|
}
|
|
3311
3325
|
|
|
3326
|
+
static func shouldAcceptReadyCall(guardArmed: Bool, expectedGeneration: Int, reportedGeneration: Int?) -> Bool {
|
|
3327
|
+
if !guardArmed {
|
|
3328
|
+
return true
|
|
3329
|
+
}
|
|
3330
|
+
guard let reportedGeneration else {
|
|
3331
|
+
return false
|
|
3332
|
+
}
|
|
3333
|
+
return reportedGeneration == expectedGeneration
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
static func readyGenerationScript(_ generation: Int) -> String {
|
|
3337
|
+
// Wrap Capacitor.nativePromise, not the plugin proxy. registerPlugin's get trap
|
|
3338
|
+
// ignores assignments to notifyAppReady. Each document keeps its own generation.
|
|
3339
|
+
return "(function(){window.__CAPGO_READY_GEN=\(generation);if(window.__capgoReadyBridge)return;function arm(){var cap=window.Capacitor;if(!cap||typeof cap.nativePromise!=='function'||cap.__capgoNativePromise)return false;var orig=cap.nativePromise.bind(cap);cap.nativePromise=function(pluginName,methodName,options){if(pluginName==='CapacitorUpdater'&&methodName==='notifyAppReady'){var next={};if(options&&typeof options==='object'){for(var k in options){if(Object.prototype.hasOwnProperty.call(options,k))next[k]=options[k];}}next.loadGeneration=window.__CAPGO_READY_GEN;options=next;}return orig(pluginName,methodName,options);};cap.__capgoNativePromise=true;window.__capgoReadyBridge=true;return true;}if(!arm()){var n=0;var t=setInterval(function(){if(arm()||++n>100)clearInterval(t);},20);}})();"
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
private func armReadyGuard(webView: WKWebView?) {
|
|
3343
|
+
let generation = self.readyGeneration + 1
|
|
3344
|
+
guard let webView else {
|
|
3345
|
+
logger.warn("Cannot stamp notifyAppReady generation without a webview")
|
|
3346
|
+
self.readyGuardArmed = false
|
|
3347
|
+
return
|
|
3348
|
+
}
|
|
3349
|
+
let userScript = WKUserScript(
|
|
3350
|
+
source: CapacitorUpdaterPlugin.readyGenerationScript(generation),
|
|
3351
|
+
injectionTime: .atDocumentStart,
|
|
3352
|
+
forMainFrameOnly: true
|
|
3353
|
+
)
|
|
3354
|
+
webView.configuration.userContentController.addUserScript(userScript)
|
|
3355
|
+
self.readyGeneration = generation
|
|
3356
|
+
self.readyGuardArmed = true
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
private func reportedReadyGeneration(_ call: CAPPluginCall) -> Int? {
|
|
3360
|
+
if let generation = call.getInt("loadGeneration") {
|
|
3361
|
+
return generation
|
|
3362
|
+
}
|
|
3363
|
+
// The bridge often boxes JS numbers as NSNumber, which getInt does not cast.
|
|
3364
|
+
if let number = call.getValue("loadGeneration") as? NSNumber {
|
|
3365
|
+
return number.intValue
|
|
3366
|
+
}
|
|
3367
|
+
return nil
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
private func acceptsReadyCall(_ call: CAPPluginCall) -> Bool {
|
|
3371
|
+
let reported = self.readyGuardArmed ? self.reportedReadyGeneration(call) : nil
|
|
3372
|
+
return CapacitorUpdaterPlugin.shouldAcceptReadyCall(
|
|
3373
|
+
guardArmed: self.readyGuardArmed,
|
|
3374
|
+
expectedGeneration: self.readyGeneration,
|
|
3375
|
+
reportedGeneration: reported
|
|
3376
|
+
)
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3312
3379
|
@objc func notifyAppReady(_ call: CAPPluginCall) {
|
|
3380
|
+
if !self.acceptsReadyCall(call) {
|
|
3381
|
+
logger.info("Ignoring notifyAppReady from a page that is no longer current")
|
|
3382
|
+
call.resolve(["bundle": self.implementation.getCurrentBundle().toJSON()])
|
|
3383
|
+
return
|
|
3384
|
+
}
|
|
3313
3385
|
self.semaphoreDown()
|
|
3314
3386
|
let bundle = self.implementation.getCurrentBundle()
|
|
3315
3387
|
self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
|
|
@@ -3417,7 +3489,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3417
3489
|
if BundleStatus.SUCCESS.storedValue != current.getStatus() {
|
|
3418
3490
|
logger.error("notifyAppReady was not called, roll back current bundle: \(current.toString())")
|
|
3419
3491
|
logger.error("Did you forget to call 'notifyAppReady()' in your Capacitor App code?")
|
|
3420
|
-
self.
|
|
3492
|
+
self.notifyListenersOnMain("updateFailed", data: [
|
|
3421
3493
|
"bundle": current.toJSON()
|
|
3422
3494
|
])
|
|
3423
3495
|
self.persistLastFailedBundle(current)
|
|
@@ -3459,7 +3531,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3459
3531
|
}
|
|
3460
3532
|
|
|
3461
3533
|
private func notifyBundleSet(_ bundle: BundleInfo) {
|
|
3462
|
-
self.
|
|
3534
|
+
self.notifyListenersOnMain("set", data: ["bundle": bundle.toJSON()], retainUntilConsumed: true)
|
|
3463
3535
|
}
|
|
3464
3536
|
|
|
3465
3537
|
func sendReadyToJs(current: BundleInfo, msg: String) {
|
|
@@ -3471,7 +3543,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3471
3543
|
if self.consumePendingNotifyAppReady() {
|
|
3472
3544
|
self.semaphoreWait(waitTime: self.appReadyTimeout)
|
|
3473
3545
|
}
|
|
3474
|
-
self.
|
|
3546
|
+
self.notifyListenersOnMain("appReady", data: ["bundle": current.toJSON(), "status": msg], retainUntilConsumed: true)
|
|
3475
3547
|
|
|
3476
3548
|
// Auto hide splashscreen if enabled
|
|
3477
3549
|
// We show it on background when conditions are met, so we should hide it on foreground regardless of update outcome
|
|
@@ -4247,8 +4319,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4247
4319
|
return
|
|
4248
4320
|
}
|
|
4249
4321
|
let payload: [String: Any] = ["version": version]
|
|
4250
|
-
self.
|
|
4251
|
-
self.
|
|
4322
|
+
self.notifyListenersOnMain("breakingAvailable", data: payload)
|
|
4323
|
+
self.notifyListenersOnMain("majorAvailable", data: payload)
|
|
4252
4324
|
}
|
|
4253
4325
|
|
|
4254
4326
|
private func shouldNotifyBreakingEvents(response: AppVersion) -> Bool {
|
|
@@ -4288,7 +4360,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4288
4360
|
let responseMessage = res.message?.isEmpty == false ? res.message : nil
|
|
4289
4361
|
let message = responseMessage ?? (backendError.isEmpty ? "server did not provide a message" : backendError)
|
|
4290
4362
|
let latestVersionName = res.version.isEmpty ? current.getVersionName() : res.version
|
|
4291
|
-
self.
|
|
4363
|
+
self.notifyListenersOnMain("updateCheckResult", data: [
|
|
4292
4364
|
"kind": responseKind,
|
|
4293
4365
|
"error": backendError,
|
|
4294
4366
|
"message": message,
|
|
@@ -4341,10 +4413,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4341
4413
|
if sendStats {
|
|
4342
4414
|
self.implementation.sendStats(action: failureAction, versionName: current.getVersionName())
|
|
4343
4415
|
}
|
|
4344
|
-
self.
|
|
4416
|
+
self.notifyListenersOnMain(failureEvent, data: ["version": latestVersionName])
|
|
4345
4417
|
}
|
|
4346
4418
|
if notifyNoNeedUpdate {
|
|
4347
|
-
self.
|
|
4419
|
+
self.notifyListenersOnMain("noNeedUpdate", data: ["bundle": current.toJSON()])
|
|
4348
4420
|
}
|
|
4349
4421
|
self.sendReadyToJs(current: current, msg: msg)
|
|
4350
4422
|
logger.info("endBackGroundTaskWithNotif \(msg) current: \(current.getVersionName()) latestVersionName: \(latestVersionName)")
|
|
@@ -4501,7 +4573,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4501
4573
|
let builtinUpdateAvailable = !current.isBuiltin()
|
|
4502
4574
|
if builtinUpdateAvailable {
|
|
4503
4575
|
let builtinBundle = self.implementation.getBundleInfo(id: BundleInfo.ID_BUILTIN)
|
|
4504
|
-
self.
|
|
4576
|
+
self.notifyListenersOnMain("updateAvailable", data: ["bundle": builtinBundle.toJSON()], retainUntilConsumed: true)
|
|
4505
4577
|
}
|
|
4506
4578
|
self.endBackGroundTaskWithNotif(
|
|
4507
4579
|
msg: "Latest version is builtin, autoUpdate onlyDownload",
|
|
@@ -4652,7 +4724,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4652
4724
|
plannedDirectUpdate: plannedDirectUpdate
|
|
4653
4725
|
)
|
|
4654
4726
|
} else if self.queueBundleForNextBackgroundInstall(next) {
|
|
4655
|
-
self.
|
|
4727
|
+
self.notifyListenersOnMain("updateAvailable", data: ["bundle": next.toJSON()])
|
|
4656
4728
|
self.endBackGroundTaskWithNotif(
|
|
4657
4729
|
msg: "Direct update reload failed, update will install next background",
|
|
4658
4730
|
latestVersionName: latestVersionName,
|
|
@@ -4673,7 +4745,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4673
4745
|
self.logger.info("Direct update skipped because splashscreen timeout occurred. Update will install on next app background.")
|
|
4674
4746
|
}
|
|
4675
4747
|
if self.queueBundleForNextBackgroundInstall(next) {
|
|
4676
|
-
self.
|
|
4748
|
+
self.notifyListenersOnMain("updateAvailable", data: ["bundle": next.toJSON()])
|
|
4677
4749
|
self.endBackGroundTaskWithNotif(
|
|
4678
4750
|
msg: "update downloaded, will install next background",
|
|
4679
4751
|
latestVersionName: latestVersionName,
|
|
@@ -4691,7 +4763,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
4691
4763
|
}
|
|
4692
4764
|
} else {
|
|
4693
4765
|
self.logger.info("autoUpdate is set to onlyDownload, downloaded update will not be set as next bundle")
|
|
4694
|
-
self.
|
|
4766
|
+
self.notifyListenersOnMain("updateAvailable", data: ["bundle": next.toJSON()], retainUntilConsumed: true)
|
|
4695
4767
|
self.endBackGroundTaskWithNotif(
|
|
4696
4768
|
msg: "update downloaded, autoUpdate onlyDownload",
|
|
4697
4769
|
latestVersionName: latestVersionName,
|
|
@@ -5036,8 +5108,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
5036
5108
|
// Determine update availability by comparing versions
|
|
5037
5109
|
if let availableVersion = availableVersion {
|
|
5038
5110
|
do {
|
|
5039
|
-
let currentVer = try
|
|
5040
|
-
let availableVer = try
|
|
5111
|
+
let currentVer = try CapgoSemanticVersion(currentVersionName)
|
|
5112
|
+
let availableVer = try CapgoSemanticVersion(availableVersion)
|
|
5041
5113
|
if availableVer > currentVer {
|
|
5042
5114
|
result["updateAvailability"] = AppUpdateAvailability.updateAvailable.rawValue
|
|
5043
5115
|
} else {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Foundation
|
|
8
|
+
|
|
9
|
+
/// Minimal version value used for native version comparisons.
|
|
10
|
+
///
|
|
11
|
+
/// Parsing and ordering intentionally mirror the lenient parser of the `Version` package
|
|
12
|
+
/// (mrackwitz/Version 0.8.0) that this type replaces, so existing comparisons keep the same results:
|
|
13
|
+
/// - `MAJOR[.MINOR[.PATCH]][-PRERELEASE][+BUILD]`, numeric components are ASCII digits (leading zeros allowed).
|
|
14
|
+
/// - Missing minor/patch compare as `0`.
|
|
15
|
+
/// - A version with a prerelease is lower than the same version without one.
|
|
16
|
+
/// - Prerelease identifiers are compared dot by dot: numerically when both are digits, lexically otherwise.
|
|
17
|
+
/// - Build metadata is ignored for equality and ordering.
|
|
18
|
+
public struct CapgoSemanticVersion: Comparable, Hashable, CustomStringConvertible {
|
|
19
|
+
public enum ParseError: Error {
|
|
20
|
+
case invalidComponents
|
|
21
|
+
case invalidMajorComponent
|
|
22
|
+
case invalidMinorComponent
|
|
23
|
+
case invalidPatchComponent
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public let major: Int
|
|
27
|
+
public let minor: Int?
|
|
28
|
+
public let patch: Int?
|
|
29
|
+
public let prerelease: String?
|
|
30
|
+
public let build: String?
|
|
31
|
+
|
|
32
|
+
public var canonicalMinor: Int { minor ?? 0 }
|
|
33
|
+
public var canonicalPatch: Int { patch ?? 0 }
|
|
34
|
+
|
|
35
|
+
// Same pattern as the lenient `Version` parser (including its build group quirk: no dots in build metadata).
|
|
36
|
+
private static let versionPattern =
|
|
37
|
+
"\\A([0-9]+)(?:\\.([0-9]+))?(?:\\.([0-9]+))?(?:-([0-9A-Za-z-.]+))?(?:\\+([0-9A-Za-z-]+))??\\z"
|
|
38
|
+
private static let versionRegex = try? NSRegularExpression(pattern: versionPattern)
|
|
39
|
+
|
|
40
|
+
public init(major: Int, minor: Int? = nil, patch: Int? = nil, prerelease: String? = nil, build: String? = nil) {
|
|
41
|
+
self.major = max(0, major)
|
|
42
|
+
self.minor = minor.map { max(0, $0) }
|
|
43
|
+
self.patch = patch.map { max(0, $0) }
|
|
44
|
+
self.prerelease = prerelease
|
|
45
|
+
self.build = build
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public init(_ string: String) throws {
|
|
49
|
+
guard let regex = Self.versionRegex else {
|
|
50
|
+
throw ParseError.invalidComponents
|
|
51
|
+
}
|
|
52
|
+
let nsString = string as NSString
|
|
53
|
+
guard let match = regex.firstMatch(in: string, options: [], range: NSRange(location: 0, length: nsString.length)),
|
|
54
|
+
match.numberOfRanges == 6 else {
|
|
55
|
+
throw ParseError.invalidComponents
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func group(_ index: Int) -> String? {
|
|
59
|
+
let range = match.range(at: index)
|
|
60
|
+
return range.location == NSNotFound ? nil : nsString.substring(with: range)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
guard let major = group(1).flatMap({ Int($0) }) else {
|
|
64
|
+
throw ParseError.invalidMajorComponent
|
|
65
|
+
}
|
|
66
|
+
let minorComponent = group(2)
|
|
67
|
+
let minor = minorComponent.flatMap { Int($0) }
|
|
68
|
+
if minorComponent != nil && minor == nil {
|
|
69
|
+
throw ParseError.invalidMinorComponent
|
|
70
|
+
}
|
|
71
|
+
let patchComponent = group(3)
|
|
72
|
+
let patch = patchComponent.flatMap { Int($0) }
|
|
73
|
+
if patchComponent != nil && patch == nil {
|
|
74
|
+
throw ParseError.invalidPatchComponent
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
self.major = major
|
|
78
|
+
self.minor = minor
|
|
79
|
+
self.patch = patch
|
|
80
|
+
self.prerelease = group(4)
|
|
81
|
+
self.build = group(5)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public var description: String {
|
|
85
|
+
var result = "\(major)"
|
|
86
|
+
if let minor {
|
|
87
|
+
result += ".\(minor)"
|
|
88
|
+
}
|
|
89
|
+
if let patch {
|
|
90
|
+
result += ".\(patch)"
|
|
91
|
+
}
|
|
92
|
+
if let prerelease {
|
|
93
|
+
result += "-\(prerelease)"
|
|
94
|
+
}
|
|
95
|
+
if let build {
|
|
96
|
+
result += "+\(build)"
|
|
97
|
+
}
|
|
98
|
+
return result
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public static func == (lhs: CapgoSemanticVersion, rhs: CapgoSemanticVersion) -> Bool {
|
|
102
|
+
return lhs.major == rhs.major
|
|
103
|
+
&& lhs.canonicalMinor == rhs.canonicalMinor
|
|
104
|
+
&& lhs.canonicalPatch == rhs.canonicalPatch
|
|
105
|
+
&& lhs.prerelease == rhs.prerelease
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public func hash(into hasher: inout Hasher) {
|
|
109
|
+
hasher.combine(major)
|
|
110
|
+
hasher.combine(canonicalMinor)
|
|
111
|
+
hasher.combine(canonicalPatch)
|
|
112
|
+
hasher.combine(prerelease)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public static func < (lhs: CapgoSemanticVersion, rhs: CapgoSemanticVersion) -> Bool {
|
|
116
|
+
if lhs.major != rhs.major {
|
|
117
|
+
return lhs.major < rhs.major
|
|
118
|
+
}
|
|
119
|
+
if lhs.canonicalMinor != rhs.canonicalMinor {
|
|
120
|
+
return lhs.canonicalMinor < rhs.canonicalMinor
|
|
121
|
+
}
|
|
122
|
+
if lhs.canonicalPatch != rhs.canonicalPatch {
|
|
123
|
+
return lhs.canonicalPatch < rhs.canonicalPatch
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
switch (lhs.prerelease, rhs.prerelease) {
|
|
127
|
+
case (.some, .none):
|
|
128
|
+
return true
|
|
129
|
+
case (.none, .some), (.none, .none):
|
|
130
|
+
return false
|
|
131
|
+
case let (.some(lpre), .some(rpre)):
|
|
132
|
+
return comparePrerelease(lpre, rpre) == .orderedAscending
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private static func isNumericIdentifier(_ value: String) -> Bool {
|
|
137
|
+
return !value.isEmpty && value.utf8.allSatisfy { $0 >= 0x30 && $0 <= 0x39 }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private static func comparePrerelease(_ lhs: String, _ rhs: String) -> ComparisonResult {
|
|
141
|
+
let lhsComponents = lhs.components(separatedBy: ".")
|
|
142
|
+
let rhsComponents = rhs.components(separatedBy: ".")
|
|
143
|
+
if let (left, right) = zip(lhsComponents, rhsComponents).first(where: { $0.0 != $0.1 }) {
|
|
144
|
+
if isNumericIdentifier(left) && isNumericIdentifier(right) {
|
|
145
|
+
let leftValue = Int(left) ?? 0
|
|
146
|
+
let rightValue = Int(right) ?? 0
|
|
147
|
+
if leftValue == rightValue {
|
|
148
|
+
return .orderedSame
|
|
149
|
+
}
|
|
150
|
+
return leftValue < rightValue ? .orderedAscending : .orderedDescending
|
|
151
|
+
}
|
|
152
|
+
if left == right {
|
|
153
|
+
return .orderedSame
|
|
154
|
+
}
|
|
155
|
+
return left < right ? .orderedAscending : .orderedDescending
|
|
156
|
+
}
|
|
157
|
+
if lhsComponents.count != rhsComponents.count {
|
|
158
|
+
return lhsComponents.count < rhsComponents.count ? .orderedAscending : .orderedDescending
|
|
159
|
+
}
|
|
160
|
+
return .orderedSame
|
|
161
|
+
}
|
|
162
|
+
}
|