@capgo/capacitor-updater 8.45.10 → 8.46.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/Package.swift +1 -1
- package/README.md +114 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +703 -60
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +85 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +27 -3
- package/dist/docs.json +213 -5
- package/dist/esm/definitions.d.ts +128 -19
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +2 -1
- package/dist/esm/web.js +4 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +4 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +4 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AppHealthTracker.swift +82 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +78 -2
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +353 -100
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +681 -300
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +32 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +20 -3
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
- package/package.json +12 -2
|
@@ -47,8 +47,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
47
47
|
CAPPluginMethod(name: "setMultiDelay", returnType: CAPPluginReturnPromise),
|
|
48
48
|
CAPPluginMethod(name: "cancelDelay", returnType: CAPPluginReturnPromise),
|
|
49
49
|
CAPPluginMethod(name: "getLatest", returnType: CAPPluginReturnPromise),
|
|
50
|
+
CAPPluginMethod(name: "triggerUpdateCheck", returnType: CAPPluginReturnPromise),
|
|
50
51
|
CAPPluginMethod(name: "setChannel", returnType: CAPPluginReturnPromise),
|
|
51
52
|
CAPPluginMethod(name: "unsetChannel", returnType: CAPPluginReturnPromise),
|
|
53
|
+
CAPPluginMethod(name: "reportWebViewError", returnType: CAPPluginReturnPromise),
|
|
52
54
|
CAPPluginMethod(name: "getChannel", returnType: CAPPluginReturnPromise),
|
|
53
55
|
CAPPluginMethod(name: "listChannels", returnType: CAPPluginReturnPromise),
|
|
54
56
|
CAPPluginMethod(name: "setCustomId", returnType: CAPPluginReturnPromise),
|
|
@@ -64,6 +66,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
64
66
|
CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise),
|
|
65
67
|
CAPPluginMethod(name: "setShakeChannelSelector", returnType: CAPPluginReturnPromise),
|
|
66
68
|
CAPPluginMethod(name: "isShakeChannelSelectorEnabled", returnType: CAPPluginReturnPromise),
|
|
69
|
+
CAPPluginMethod(name: "getAppId", returnType: CAPPluginReturnPromise),
|
|
70
|
+
CAPPluginMethod(name: "setAppId", returnType: CAPPluginReturnPromise),
|
|
67
71
|
// App Store update methods
|
|
68
72
|
CAPPluginMethod(name: "getAppUpdateInfo", returnType: CAPPluginReturnPromise),
|
|
69
73
|
CAPPluginMethod(name: "openAppStore", returnType: CAPPluginReturnPromise),
|
|
@@ -72,7 +76,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
72
76
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
73
77
|
]
|
|
74
78
|
public var implementation = CapgoUpdater()
|
|
75
|
-
private let pluginVersion: String = "8.
|
|
79
|
+
private let pluginVersion: String = "8.46.0"
|
|
76
80
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
77
81
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
78
82
|
static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
|
|
@@ -128,6 +132,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
128
132
|
private var persistModifyUrl = false
|
|
129
133
|
private var allowManualBundleError = false
|
|
130
134
|
private var keepUrlPathFlagLastValue: Bool?
|
|
135
|
+
private var appHealthTracker: AppHealthTracker?
|
|
136
|
+
private var webViewStatsReporter: WebViewStatsReporter?
|
|
131
137
|
public var shakeMenuEnabled = false
|
|
132
138
|
public var shakeChannelSelectorEnabled = false
|
|
133
139
|
let semaphoreReady = DispatchSemaphore(value: 0)
|
|
@@ -143,6 +149,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
143
149
|
} else {
|
|
144
150
|
logger.error("Failed to get webView for logging")
|
|
145
151
|
}
|
|
152
|
+
let webViewStatsReporter = WebViewStatsReporter(implementation: implementation)
|
|
153
|
+
self.webViewStatsReporter = webViewStatsReporter
|
|
154
|
+
webViewStatsReporter.install(on: self.bridge?.webView)
|
|
146
155
|
#if targetEnvironment(simulator)
|
|
147
156
|
logger.info("::::: SIMULATOR :::::")
|
|
148
157
|
logger.info("Application directory: \(NSHomeDirectory())")
|
|
@@ -223,17 +232,21 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
223
232
|
resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
|
|
224
233
|
shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
|
|
225
234
|
shakeChannelSelectorEnabled = getConfig().getBoolean("allowShakeChannelSelector", false)
|
|
226
|
-
|
|
227
|
-
if periodCheckDelayValue >= 0 && periodCheckDelayValue > 600 {
|
|
228
|
-
periodCheckDelay = 600
|
|
229
|
-
} else {
|
|
230
|
-
periodCheckDelay = periodCheckDelayValue
|
|
231
|
-
}
|
|
235
|
+
periodCheckDelay = Self.normalizedPeriodCheckDelaySeconds(getConfig().getInt("periodCheckDelay", 0))
|
|
232
236
|
|
|
233
237
|
implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
|
|
234
238
|
implementation.notifyDownloadRaw = notifyDownload
|
|
235
239
|
implementation.notifyListeners = { [weak self] eventName, data in
|
|
236
|
-
|
|
240
|
+
let emit = {
|
|
241
|
+
self?.notifyListeners(eventName, data: data)
|
|
242
|
+
}
|
|
243
|
+
if Thread.isMainThread {
|
|
244
|
+
emit()
|
|
245
|
+
} else {
|
|
246
|
+
DispatchQueue.main.async {
|
|
247
|
+
emit()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
237
250
|
}
|
|
238
251
|
implementation.pluginVersion = self.pluginVersion
|
|
239
252
|
|
|
@@ -278,12 +291,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
278
291
|
implementation.defaultChannel = getConfig().getString("defaultChannel", "")!
|
|
279
292
|
}
|
|
280
293
|
self.implementation.autoReset()
|
|
294
|
+
let appHealthTracker = AppHealthTracker(implementation: self.implementation)
|
|
295
|
+
self.appHealthTracker = appHealthTracker
|
|
296
|
+
appHealthTracker.reportPreviousUncleanForegroundExit()
|
|
297
|
+
appHealthTracker.startSession()
|
|
281
298
|
|
|
282
|
-
// Check if app was recently installed/updated BEFORE
|
|
299
|
+
// Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
|
|
283
300
|
self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
|
|
284
301
|
|
|
285
302
|
if resetWhenUpdate {
|
|
286
|
-
self.
|
|
303
|
+
let didResetCurrentBundle = self.resetCurrentBundleForNativeBuildChangeIfNeeded()
|
|
304
|
+
self.cleanupObsoleteVersions(didResetCurrentBundle: didResetCurrentBundle)
|
|
287
305
|
}
|
|
288
306
|
|
|
289
307
|
// Load the server
|
|
@@ -300,6 +318,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
300
318
|
let nc = NotificationCenter.default
|
|
301
319
|
nc.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
302
320
|
nc.addObserver(self, selector: #selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
321
|
+
nc.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, object: nil)
|
|
322
|
+
nc.addObserver(self, selector: #selector(appDidReceiveMemoryWarning), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
|
|
303
323
|
|
|
304
324
|
// Check for 'kill' delay condition on app launch
|
|
305
325
|
// This handles cases where the app was killed (willTerminateNotification is not reliable for system kills)
|
|
@@ -356,6 +376,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
356
376
|
}
|
|
357
377
|
}
|
|
358
378
|
|
|
379
|
+
@objc private func appWillTerminate() {
|
|
380
|
+
appHealthTracker?.markForeground(false)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
@objc private func appDidReceiveMemoryWarning() {
|
|
384
|
+
appHealthTracker?.reportMemoryWarning()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@objc func reportWebViewError(_ call: CAPPluginCall) {
|
|
388
|
+
guard let webViewStatsReporter = webViewStatsReporter else {
|
|
389
|
+
call.resolve()
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
webViewStatsReporter.reportError(call)
|
|
393
|
+
}
|
|
394
|
+
|
|
359
395
|
private func initialLoad() -> Bool {
|
|
360
396
|
guard let bridge = self.bridge else { return false }
|
|
361
397
|
if keepUrlPathAfterReload {
|
|
@@ -399,7 +435,29 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
399
435
|
semaphoreReady.signal()
|
|
400
436
|
}
|
|
401
437
|
|
|
402
|
-
|
|
438
|
+
func storedNativeBuildVersion() -> String {
|
|
439
|
+
UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
func hasNativeBuildVersionChanged() -> Bool {
|
|
443
|
+
let previous = self.storedNativeBuildVersion()
|
|
444
|
+
return previous != "0" && self.currentBuildVersion != previous
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
@discardableResult
|
|
448
|
+
func resetCurrentBundleForNativeBuildChangeIfNeeded() -> Bool {
|
|
449
|
+
let previous = self.storedNativeBuildVersion()
|
|
450
|
+
guard previous != "0" && self.currentBuildVersion != previous else {
|
|
451
|
+
return false
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Reset startup state synchronously so initialLoad() boots from the builtin bundle.
|
|
455
|
+
self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting startup bundle to builtin.")
|
|
456
|
+
self.implementation.reset(isInternal: true)
|
|
457
|
+
return true
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private func cleanupObsoleteVersions(didResetCurrentBundle: Bool = false) {
|
|
403
461
|
cleanupThread = Thread {
|
|
404
462
|
self.cleanupLock.lock()
|
|
405
463
|
defer {
|
|
@@ -434,9 +492,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
434
492
|
// 1. Write "LatestVersionNative" - this fixes the part 1 of this bug
|
|
435
493
|
// 2. Compare both keys. If any is not equal to "currentBuildVersion", then revert to builtin version. This fixes the part 2 of this bug
|
|
436
494
|
|
|
437
|
-
let previous =
|
|
495
|
+
let previous = self.storedNativeBuildVersion()
|
|
438
496
|
if previous != "0" && self.currentBuildVersion != previous {
|
|
439
|
-
|
|
497
|
+
if !didResetCurrentBundle {
|
|
498
|
+
self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting current bundle to builtin.")
|
|
499
|
+
self.implementation.reset(isInternal: true)
|
|
500
|
+
}
|
|
440
501
|
let res = self.implementation.list()
|
|
441
502
|
for version in res {
|
|
442
503
|
// Check if thread was cancelled
|
|
@@ -496,11 +557,88 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
496
557
|
logger.info("Cleanup finished, proceeding with download")
|
|
497
558
|
}
|
|
498
559
|
|
|
560
|
+
private func resolveCall(_ call: CAPPluginCall, data: PluginCallResultData? = nil) {
|
|
561
|
+
let resolve = {
|
|
562
|
+
let savedCall = self.bridge?.savedCall(withID: call.callbackId)
|
|
563
|
+
let targetCall = savedCall ?? call
|
|
564
|
+
|
|
565
|
+
if let data {
|
|
566
|
+
targetCall.resolve(data)
|
|
567
|
+
} else {
|
|
568
|
+
targetCall.resolve()
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if savedCall != nil {
|
|
572
|
+
self.bridge?.releaseCall(withID: call.callbackId)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if Thread.isMainThread {
|
|
577
|
+
resolve()
|
|
578
|
+
} else {
|
|
579
|
+
DispatchQueue.main.async {
|
|
580
|
+
resolve()
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private func rejectCall(_ call: CAPPluginCall, message: String, code: String? = nil, error: Error? = nil, data: PluginCallResultData? = nil) {
|
|
586
|
+
let reject = {
|
|
587
|
+
let savedCall = self.bridge?.savedCall(withID: call.callbackId)
|
|
588
|
+
let targetCall = savedCall ?? call
|
|
589
|
+
|
|
590
|
+
targetCall.reject(message, code, error, data)
|
|
591
|
+
|
|
592
|
+
if savedCall != nil {
|
|
593
|
+
self.bridge?.releaseCall(withID: call.callbackId)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if Thread.isMainThread {
|
|
598
|
+
reject()
|
|
599
|
+
} else {
|
|
600
|
+
DispatchQueue.main.async {
|
|
601
|
+
reject()
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private func saveCallForAsyncHandling(_ call: CAPPluginCall) {
|
|
607
|
+
bridge?.saveCall(call)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private func notifyListenersOnMain(_ eventName: String, data: JSObject) {
|
|
611
|
+
let notify = {
|
|
612
|
+
self.notifyListeners(eventName, data: data)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if Thread.isMainThread {
|
|
616
|
+
notify()
|
|
617
|
+
} else {
|
|
618
|
+
DispatchQueue.main.async {
|
|
619
|
+
notify()
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private func bundlePayload(_ bundleInfo: BundleInfo) -> JSObject {
|
|
625
|
+
var payload: JSObject = [:]
|
|
626
|
+
for (key, value) in bundleInfo.toJSON() {
|
|
627
|
+
payload[key] = value
|
|
628
|
+
}
|
|
629
|
+
return payload
|
|
630
|
+
}
|
|
631
|
+
|
|
499
632
|
@objc func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false, bundle: BundleInfo? = nil) {
|
|
500
633
|
let bundleInfo = bundle ?? self.implementation.getBundleInfo(id: id)
|
|
501
|
-
|
|
634
|
+
var downloadPayload: JSObject = [:]
|
|
635
|
+
downloadPayload["percent"] = percent
|
|
636
|
+
downloadPayload["bundle"] = bundlePayload(bundleInfo)
|
|
637
|
+
self.notifyListenersOnMain("download", data: downloadPayload)
|
|
502
638
|
if percent == 100 {
|
|
503
|
-
|
|
639
|
+
var downloadCompletePayload: JSObject = [:]
|
|
640
|
+
downloadCompletePayload["bundle"] = bundlePayload(bundleInfo)
|
|
641
|
+
self.notifyListenersOnMain("downloadComplete", data: downloadCompletePayload)
|
|
504
642
|
self.implementation.sendStats(action: "download_complete", versionName: bundleInfo.getVersionName())
|
|
505
643
|
} else if percent.isMultiple(of: 10) || ignoreMultipleOfTen {
|
|
506
644
|
self.implementation.sendStats(action: "download_\(percent)", versionName: bundleInfo.getVersionName())
|
|
@@ -597,6 +735,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
597
735
|
let manifestArray = call.getArray("manifest")
|
|
598
736
|
let url = URL(string: urlString)
|
|
599
737
|
logger.info("Downloading \(String(describing: url))")
|
|
738
|
+
self.saveCallForAsyncHandling(call)
|
|
600
739
|
DispatchQueue.global(qos: .background).async {
|
|
601
740
|
do {
|
|
602
741
|
let next: BundleInfo
|
|
@@ -644,13 +783,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
644
783
|
} else {
|
|
645
784
|
self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
|
|
646
785
|
}
|
|
647
|
-
|
|
648
|
-
|
|
786
|
+
var updateAvailablePayload: JSObject = [:]
|
|
787
|
+
updateAvailablePayload["bundle"] = self.bundlePayload(next)
|
|
788
|
+
self.notifyListenersOnMain("updateAvailable", data: updateAvailablePayload)
|
|
789
|
+
self.resolveCall(call, data: next.toJSON())
|
|
649
790
|
} catch {
|
|
650
791
|
self.logger.error("Failed to download from: \(String(describing: url)) \(error.localizedDescription)")
|
|
651
|
-
|
|
792
|
+
var downloadFailedPayload: JSObject = [:]
|
|
793
|
+
downloadFailedPayload["version"] = version
|
|
794
|
+
self.notifyListenersOnMain("downloadFailed", data: downloadFailedPayload)
|
|
652
795
|
self.implementation.sendStats(action: "download_fail")
|
|
653
|
-
|
|
796
|
+
self.rejectCall(call, message: "Failed to download from: \(url!) - \(error.localizedDescription)")
|
|
654
797
|
}
|
|
655
798
|
}
|
|
656
799
|
}
|
|
@@ -876,25 +1019,68 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
876
1019
|
|
|
877
1020
|
@objc func getLatest(_ call: CAPPluginCall) {
|
|
878
1021
|
let channel = call.getString("channel")
|
|
879
|
-
|
|
1022
|
+
self.saveCallForAsyncHandling(call)
|
|
1023
|
+
runGetLatestWork {
|
|
880
1024
|
let res = self.implementation.getLatest(url: URL(string: self.updateUrl)!, channel: channel)
|
|
881
|
-
if res.error
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1025
|
+
if let error = res.error, !error.isEmpty {
|
|
1026
|
+
let responseKind = self.updateResponseKind(kind: res.kind)
|
|
1027
|
+
res.kind = responseKind
|
|
1028
|
+
if responseKind == "failed" {
|
|
1029
|
+
self.rejectCall(call, message: error)
|
|
1030
|
+
} else {
|
|
1031
|
+
if res.version.isEmpty {
|
|
1032
|
+
res.version = self.implementation.getCurrentBundle().getVersionName()
|
|
1033
|
+
}
|
|
1034
|
+
self.resolveCall(call, data: res.toDict())
|
|
1035
|
+
}
|
|
1036
|
+
} else if let kind = res.kind, !kind.isEmpty {
|
|
1037
|
+
let responseKind = self.updateResponseKind(kind: kind)
|
|
1038
|
+
res.kind = responseKind
|
|
1039
|
+
if responseKind != "failed" {
|
|
1040
|
+
if res.version.isEmpty {
|
|
1041
|
+
res.version = self.implementation.getCurrentBundle().getVersionName()
|
|
1042
|
+
}
|
|
1043
|
+
self.resolveCall(call, data: res.toDict())
|
|
1044
|
+
} else {
|
|
1045
|
+
self.rejectCall(call, message: res.message ?? "server did not provide a message")
|
|
1046
|
+
}
|
|
1047
|
+
} else if let message = res.message, !message.isEmpty {
|
|
1048
|
+
self.rejectCall(call, message: message)
|
|
885
1049
|
} else {
|
|
886
|
-
|
|
1050
|
+
self.resolveCall(call, data: res.toDict())
|
|
887
1051
|
}
|
|
888
1052
|
}
|
|
889
1053
|
}
|
|
890
1054
|
|
|
1055
|
+
public func triggerBackgroundUpdateCheck() -> String {
|
|
1056
|
+
guard !self.updateUrl.isEmpty, URL(string: self.updateUrl) != nil else {
|
|
1057
|
+
logger.error("Error no url or wrong format")
|
|
1058
|
+
return "unavailable"
|
|
1059
|
+
}
|
|
1060
|
+
if self.isDownloadStuckOrTimedOut() {
|
|
1061
|
+
logger.info("Download already in progress, skipping duplicate download request")
|
|
1062
|
+
return "already_running"
|
|
1063
|
+
}
|
|
1064
|
+
self.backgroundDownload()
|
|
1065
|
+
return "queued"
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
@objc func triggerUpdateCheck(_ call: CAPPluginCall) {
|
|
1069
|
+
let status = self.triggerBackgroundUpdateCheck()
|
|
1070
|
+
call.resolve([
|
|
1071
|
+
"status": status,
|
|
1072
|
+
"queued": status == "queued"
|
|
1073
|
+
])
|
|
1074
|
+
}
|
|
1075
|
+
|
|
891
1076
|
@objc func unsetChannel(_ call: CAPPluginCall) {
|
|
892
1077
|
let triggerAutoUpdate = call.getBool("triggerAutoUpdate", false)
|
|
893
|
-
|
|
1078
|
+
self.saveCallForAsyncHandling(call)
|
|
1079
|
+
DispatchQueue.global(qos: .utility).async {
|
|
894
1080
|
let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
|
|
895
1081
|
let res = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
|
|
896
1082
|
if res.error != "" {
|
|
897
|
-
|
|
1083
|
+
self.rejectCall(call, message: res.error, code: "UNSETCHANNEL_FAILED", data: [
|
|
898
1084
|
"message": res.error,
|
|
899
1085
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
900
1086
|
])
|
|
@@ -908,7 +1094,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
908
1094
|
self.logger.info("Download already in progress, skipping duplicate download request")
|
|
909
1095
|
}
|
|
910
1096
|
}
|
|
911
|
-
|
|
1097
|
+
self.resolveCall(call, data: res.toDict())
|
|
912
1098
|
}
|
|
913
1099
|
}
|
|
914
1100
|
}
|
|
@@ -923,17 +1109,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
923
1109
|
return
|
|
924
1110
|
}
|
|
925
1111
|
let triggerAutoUpdate = call.getBool("triggerAutoUpdate") ?? false
|
|
926
|
-
|
|
1112
|
+
self.saveCallForAsyncHandling(call)
|
|
1113
|
+
DispatchQueue.global(qos: .utility).async {
|
|
927
1114
|
let res = self.implementation.setChannel(channel: channel, defaultChannelKey: self.defaultChannelDefaultsKey, allowSetDefaultChannel: self.allowSetDefaultChannel)
|
|
928
1115
|
if res.error != "" {
|
|
929
1116
|
// Fire channelPrivate event if channel doesn't allow self-assignment
|
|
930
1117
|
if res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed") {
|
|
931
|
-
self.
|
|
1118
|
+
self.notifyListenersOnMain("channelPrivate", data: [
|
|
932
1119
|
"channel": channel,
|
|
933
1120
|
"message": res.error
|
|
934
1121
|
])
|
|
935
1122
|
}
|
|
936
|
-
|
|
1123
|
+
self.rejectCall(call, message: res.error, code: "SETCHANNEL_FAILED", data: [
|
|
937
1124
|
"message": res.error,
|
|
938
1125
|
"error": res.error.contains("Channel URL") ? "missing_config" : (res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed")) ? "channel_private" : "request_failed"
|
|
939
1126
|
])
|
|
@@ -947,35 +1134,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
947
1134
|
self.logger.info("Download already in progress, skipping duplicate download request")
|
|
948
1135
|
}
|
|
949
1136
|
}
|
|
950
|
-
|
|
1137
|
+
self.resolveCall(call, data: res.toDict())
|
|
951
1138
|
}
|
|
952
1139
|
}
|
|
953
1140
|
}
|
|
954
1141
|
|
|
955
1142
|
@objc func getChannel(_ call: CAPPluginCall) {
|
|
956
|
-
|
|
1143
|
+
self.saveCallForAsyncHandling(call)
|
|
1144
|
+
DispatchQueue.global(qos: .utility).async {
|
|
957
1145
|
let res = self.implementation.getChannel()
|
|
958
1146
|
if res.error != "" {
|
|
959
|
-
|
|
1147
|
+
self.rejectCall(call, message: res.error, code: "GETCHANNEL_FAILED", data: [
|
|
960
1148
|
"message": res.error,
|
|
961
1149
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
962
1150
|
])
|
|
963
1151
|
} else {
|
|
964
|
-
|
|
1152
|
+
self.resolveCall(call, data: res.toDict())
|
|
965
1153
|
}
|
|
966
1154
|
}
|
|
967
1155
|
}
|
|
968
1156
|
|
|
969
1157
|
@objc func listChannels(_ call: CAPPluginCall) {
|
|
970
|
-
|
|
1158
|
+
self.saveCallForAsyncHandling(call)
|
|
1159
|
+
DispatchQueue.global(qos: .utility).async {
|
|
971
1160
|
let res = self.implementation.listChannels()
|
|
972
1161
|
if res.error != "" {
|
|
973
|
-
|
|
1162
|
+
self.rejectCall(call, message: res.error, code: "LISTCHANNELS_FAILED", data: [
|
|
974
1163
|
"message": res.error,
|
|
975
1164
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
976
1165
|
])
|
|
977
1166
|
} else {
|
|
978
|
-
|
|
1167
|
+
var payload: JSObject = [:]
|
|
1168
|
+
payload["channels"] = res.channels
|
|
1169
|
+
self.resolveCall(call, data: payload)
|
|
979
1170
|
}
|
|
980
1171
|
}
|
|
981
1172
|
}
|
|
@@ -1102,6 +1293,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1102
1293
|
let bundle = self.implementation.getCurrentBundle()
|
|
1103
1294
|
self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
|
|
1104
1295
|
logger.info("Current bundle loaded successfully. [notifyAppReady was called] \(bundle.toString())")
|
|
1296
|
+
|
|
1105
1297
|
call.resolve(["bundle": bundle.toJSON()])
|
|
1106
1298
|
}
|
|
1107
1299
|
|
|
@@ -1191,7 +1383,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1191
1383
|
|
|
1192
1384
|
logger.info("Current bundle is: \(current.toString())")
|
|
1193
1385
|
|
|
1194
|
-
if BundleStatus.SUCCESS.
|
|
1386
|
+
if BundleStatus.SUCCESS.storedValue != current.getStatus() {
|
|
1195
1387
|
logger.error("notifyAppReady was not called, roll back current bundle: \(current.toString())")
|
|
1196
1388
|
logger.error("Did you forget to call 'notifyAppReady()' in your Capacitor App code?")
|
|
1197
1389
|
self.notifyListeners("updateFailed", data: [
|
|
@@ -1572,6 +1764,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1572
1764
|
plannedDirectUpdate && directUpdateMode == "onLaunch"
|
|
1573
1765
|
}
|
|
1574
1766
|
|
|
1767
|
+
static func normalizedPeriodCheckDelaySeconds(_ value: Int) -> Int {
|
|
1768
|
+
guard value > 0 else {
|
|
1769
|
+
return 0
|
|
1770
|
+
}
|
|
1771
|
+
return max(600, value)
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1575
1774
|
private func getOnLaunchDirectUpdateUsed() -> Bool {
|
|
1576
1775
|
self.onLaunchDirectUpdateStateLock.lock()
|
|
1577
1776
|
defer { self.onLaunchDirectUpdateStateLock.unlock() }
|
|
@@ -1601,6 +1800,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1601
1800
|
self.updateUrl = updateUrl
|
|
1602
1801
|
}
|
|
1603
1802
|
|
|
1803
|
+
func setCurrentBuildVersionForTesting(_ currentBuildVersion: String) {
|
|
1804
|
+
self.currentBuildVersion = currentBuildVersion
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1604
1807
|
func shouldUseDirectUpdateForTesting() -> Bool {
|
|
1605
1808
|
self.shouldUseDirectUpdate()
|
|
1606
1809
|
}
|
|
@@ -1618,6 +1821,56 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1618
1821
|
self.notifyListeners("majorAvailable", data: payload)
|
|
1619
1822
|
}
|
|
1620
1823
|
|
|
1824
|
+
static func normalizedUpdateResponseKind(kind: String?) -> String {
|
|
1825
|
+
if let kind, ["up_to_date", "blocked", "failed"].contains(kind) {
|
|
1826
|
+
return kind
|
|
1827
|
+
}
|
|
1828
|
+
return "failed"
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
private func updateResponseKind(kind: String?) -> String {
|
|
1832
|
+
Self.normalizedUpdateResponseKind(kind: kind)
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
private func endBackgroundDownloadAfterLatestError(
|
|
1836
|
+
backendError: String,
|
|
1837
|
+
res: AppVersion,
|
|
1838
|
+
current: BundleInfo,
|
|
1839
|
+
plannedDirectUpdate: Bool
|
|
1840
|
+
) {
|
|
1841
|
+
let statusCode = res.statusCode
|
|
1842
|
+
let responseKind = self.updateResponseKind(kind: res.kind)
|
|
1843
|
+
let responseMessage = res.message?.isEmpty == false ? res.message : nil
|
|
1844
|
+
let message = responseMessage ?? (backendError.isEmpty ? "server did not provide a message" : backendError)
|
|
1845
|
+
let latestVersionName = res.version.isEmpty ? current.getVersionName() : res.version
|
|
1846
|
+
self.notifyListeners("updateCheckResult", data: [
|
|
1847
|
+
"kind": responseKind,
|
|
1848
|
+
"error": backendError,
|
|
1849
|
+
"message": message,
|
|
1850
|
+
"statusCode": statusCode,
|
|
1851
|
+
"version": latestVersionName,
|
|
1852
|
+
"bundle": current.toJSON()
|
|
1853
|
+
])
|
|
1854
|
+
|
|
1855
|
+
if responseKind == "up_to_date" {
|
|
1856
|
+
self.logger.info("No new version available")
|
|
1857
|
+
} else if responseKind == "blocked" {
|
|
1858
|
+
self.logger.info("Update check blocked with error: \(backendError)")
|
|
1859
|
+
} else {
|
|
1860
|
+
self.logger.error("getLatest failed with error: \(backendError)")
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
let isFailure = responseKind == "failed"
|
|
1864
|
+
self.endBackGroundTaskWithNotif(
|
|
1865
|
+
msg: message,
|
|
1866
|
+
latestVersionName: latestVersionName,
|
|
1867
|
+
current: current,
|
|
1868
|
+
error: isFailure,
|
|
1869
|
+
plannedDirectUpdate: plannedDirectUpdate,
|
|
1870
|
+
sendStats: isFailure
|
|
1871
|
+
)
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1621
1874
|
func endBackGroundTaskWithNotif(
|
|
1622
1875
|
msg: String,
|
|
1623
1876
|
latestVersionName: String,
|
|
@@ -1672,6 +1925,26 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1672
1925
|
}
|
|
1673
1926
|
|
|
1674
1927
|
func runBackgroundDownloadWork(_ work: @escaping () -> Void) {
|
|
1928
|
+
// Live update checks/downloads are user-visible work. Using `.background`
|
|
1929
|
+
// lets the scheduler starve them for minutes while the app is active.
|
|
1930
|
+
DispatchQueue.global(qos: .utility).async(execute: work)
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
private func beginDownloadBackgroundTask() {
|
|
1934
|
+
let registerTask = {
|
|
1935
|
+
self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Download Tasks") {
|
|
1936
|
+
self.endBackGroundTask()
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
if Thread.isMainThread {
|
|
1941
|
+
registerTask()
|
|
1942
|
+
} else {
|
|
1943
|
+
DispatchQueue.main.sync(execute: registerTask)
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
func runGetLatestWork(_ work: @escaping () -> Void) {
|
|
1675
1948
|
DispatchQueue.global(qos: .background).async(execute: work)
|
|
1676
1949
|
}
|
|
1677
1950
|
|
|
@@ -1697,26 +1970,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1697
1970
|
self.runBackgroundDownloadWork {
|
|
1698
1971
|
// Wait for cleanup to complete before starting download
|
|
1699
1972
|
self.waitForCleanupIfNeeded()
|
|
1700
|
-
self.
|
|
1701
|
-
// End the task if time expires.
|
|
1702
|
-
self.endBackGroundTask()
|
|
1703
|
-
}
|
|
1973
|
+
self.beginDownloadBackgroundTask()
|
|
1704
1974
|
self.logger.info("Check for update via \(self.updateUrl)")
|
|
1705
1975
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
1706
1976
|
let current = self.implementation.getCurrentBundle()
|
|
1707
1977
|
|
|
1708
1978
|
// Handle network errors and other failures first
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
latestVersionName: res.version,
|
|
1979
|
+
let backendError = res.error ?? ""
|
|
1980
|
+
let backendKind = res.kind ?? ""
|
|
1981
|
+
if !backendError.isEmpty || !backendKind.isEmpty {
|
|
1982
|
+
self.endBackgroundDownloadAfterLatestError(
|
|
1983
|
+
backendError: backendError,
|
|
1984
|
+
res: res,
|
|
1716
1985
|
current: current,
|
|
1717
|
-
|
|
1718
|
-
plannedDirectUpdate: plannedDirectUpdate,
|
|
1719
|
-
sendStats: !responseIsOk
|
|
1986
|
+
plannedDirectUpdate: plannedDirectUpdate
|
|
1720
1987
|
)
|
|
1721
1988
|
return
|
|
1722
1989
|
}
|
|
@@ -1942,6 +2209,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1942
2209
|
}
|
|
1943
2210
|
|
|
1944
2211
|
@objc func appMovedToForeground() {
|
|
2212
|
+
appHealthTracker?.markForeground(true)
|
|
1945
2213
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
1946
2214
|
self.implementation.sendStats(action: "app_moved_to_foreground", versionName: current.getVersionName())
|
|
1947
2215
|
self.delayUpdateUtils.checkCancelDelay(source: .foreground)
|
|
@@ -1987,7 +2255,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1987
2255
|
timer.invalidate()
|
|
1988
2256
|
return
|
|
1989
2257
|
}
|
|
1990
|
-
DispatchQueue.global(qos: .
|
|
2258
|
+
DispatchQueue.global(qos: .utility).async {
|
|
1991
2259
|
let res = self.implementation.getLatest(url: url, channel: nil)
|
|
1992
2260
|
let current = self.implementation.getCurrentBundle()
|
|
1993
2261
|
|
|
@@ -2008,6 +2276,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2008
2276
|
@objc func appMovedToBackground() {
|
|
2009
2277
|
// Reset timeout flag at start of each background cycle
|
|
2010
2278
|
self.autoSplashscreenTimedOut = false
|
|
2279
|
+
appHealthTracker?.markForeground(false)
|
|
2011
2280
|
|
|
2012
2281
|
let current: BundleInfo = self.implementation.getCurrentBundle()
|
|
2013
2282
|
self.implementation.sendStats(action: "app_moved_to_background", versionName: current.getVersionName())
|
|
@@ -2139,29 +2408,30 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2139
2408
|
|
|
2140
2409
|
logger.info("Getting App Store update info for \(bundleId) in country \(country)")
|
|
2141
2410
|
|
|
2411
|
+
self.saveCallForAsyncHandling(call)
|
|
2142
2412
|
DispatchQueue.global(qos: .background).async {
|
|
2143
2413
|
let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)&country=\(country)"
|
|
2144
2414
|
guard let url = URL(string: urlString) else {
|
|
2145
|
-
|
|
2415
|
+
self.rejectCall(call, message: "Invalid URL for App Store lookup")
|
|
2146
2416
|
return
|
|
2147
2417
|
}
|
|
2148
2418
|
|
|
2149
2419
|
let task = URLSession.shared.dataTask(with: url) { data, _, error in
|
|
2150
2420
|
if let error = error {
|
|
2151
2421
|
self.logger.error("App Store lookup failed: \(error.localizedDescription)")
|
|
2152
|
-
|
|
2422
|
+
self.rejectCall(call, message: "App Store lookup failed: \(error.localizedDescription)")
|
|
2153
2423
|
return
|
|
2154
2424
|
}
|
|
2155
2425
|
|
|
2156
2426
|
guard let data = data else {
|
|
2157
|
-
|
|
2427
|
+
self.rejectCall(call, message: "No data received from App Store")
|
|
2158
2428
|
return
|
|
2159
2429
|
}
|
|
2160
2430
|
|
|
2161
2431
|
do {
|
|
2162
2432
|
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
2163
2433
|
let resultCount = json["resultCount"] as? Int else {
|
|
2164
|
-
|
|
2434
|
+
self.rejectCall(call, message: "Invalid response from App Store")
|
|
2165
2435
|
return
|
|
2166
2436
|
}
|
|
2167
2437
|
|
|
@@ -2218,10 +2488,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2218
2488
|
self.logger.info("App not found in App Store for bundleId: \(bundleId)")
|
|
2219
2489
|
}
|
|
2220
2490
|
|
|
2221
|
-
|
|
2491
|
+
self.resolveCall(call, data: result)
|
|
2222
2492
|
} catch {
|
|
2223
2493
|
self.logger.error("Failed to parse App Store response: \(error.localizedDescription)")
|
|
2224
|
-
|
|
2494
|
+
self.rejectCall(call, message: "Failed to parse App Store response: \(error.localizedDescription)")
|
|
2225
2495
|
}
|
|
2226
2496
|
}
|
|
2227
2497
|
task.resume()
|
|
@@ -2230,37 +2500,48 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2230
2500
|
|
|
2231
2501
|
@objc func openAppStore(_ call: CAPPluginCall) {
|
|
2232
2502
|
let appId = call.getString("appId")
|
|
2503
|
+
let bundleId = implementation.appId
|
|
2504
|
+
self.saveCallForAsyncHandling(call)
|
|
2233
2505
|
|
|
2234
|
-
|
|
2235
|
-
// Open App Store with provided app ID
|
|
2236
|
-
let urlString = "https://apps.apple.com/app/id\(appId)"
|
|
2506
|
+
func openAppStorePage(urlString: String, invalidMessage: String = "Invalid App Store URL", failureMessage: String = "Failed to open App Store") {
|
|
2237
2507
|
guard let url = URL(string: urlString) else {
|
|
2238
|
-
|
|
2508
|
+
self.rejectCall(call, message: invalidMessage)
|
|
2239
2509
|
return
|
|
2240
2510
|
}
|
|
2241
2511
|
DispatchQueue.main.async {
|
|
2242
2512
|
UIApplication.shared.open(url) { success in
|
|
2243
2513
|
if success {
|
|
2244
|
-
|
|
2514
|
+
self.resolveCall(call)
|
|
2245
2515
|
} else {
|
|
2246
|
-
|
|
2516
|
+
self.rejectCall(call, message: failureMessage)
|
|
2247
2517
|
}
|
|
2248
2518
|
}
|
|
2249
2519
|
}
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
func openFallbackAppStorePage() {
|
|
2523
|
+
guard let encodedBundleId = bundleId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
|
|
2524
|
+
self.rejectCall(call, message: "Failed to build App Store fallback URL")
|
|
2525
|
+
return
|
|
2526
|
+
}
|
|
2527
|
+
openAppStorePage(urlString: "https://apps.apple.com/app/\(encodedBundleId)")
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
if let appId = appId {
|
|
2531
|
+
openAppStorePage(urlString: "https://apps.apple.com/app/id\(appId)")
|
|
2250
2532
|
} else {
|
|
2251
|
-
// Look up app ID using bundle identifier
|
|
2252
|
-
let bundleId = implementation.appId
|
|
2253
2533
|
let lookupUrl = "https://itunes.apple.com/lookup?bundleId=\(bundleId)"
|
|
2254
2534
|
|
|
2255
2535
|
DispatchQueue.global(qos: .background).async {
|
|
2256
2536
|
guard let url = URL(string: lookupUrl) else {
|
|
2257
|
-
|
|
2537
|
+
openFallbackAppStorePage()
|
|
2258
2538
|
return
|
|
2259
2539
|
}
|
|
2260
2540
|
|
|
2261
2541
|
let task = URLSession.shared.dataTask(with: url) { data, _, error in
|
|
2262
2542
|
if let error = error {
|
|
2263
|
-
|
|
2543
|
+
self.logger.error("App Store lookup failed: \(error.localizedDescription)")
|
|
2544
|
+
openFallbackAppStorePage()
|
|
2264
2545
|
return
|
|
2265
2546
|
}
|
|
2266
2547
|
|
|
@@ -2269,39 +2550,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2269
2550
|
let results = json["results"] as? [[String: Any]],
|
|
2270
2551
|
let appInfo = results.first,
|
|
2271
2552
|
let trackId = appInfo["trackId"] as? Int else {
|
|
2272
|
-
|
|
2273
|
-
let fallbackUrlString = "https://apps.apple.com/app/\(bundleId)"
|
|
2274
|
-
guard let fallbackUrl = URL(string: fallbackUrlString) else {
|
|
2275
|
-
call.reject("Failed to find app in App Store and fallback URL is invalid")
|
|
2276
|
-
return
|
|
2277
|
-
}
|
|
2278
|
-
DispatchQueue.main.async {
|
|
2279
|
-
UIApplication.shared.open(fallbackUrl) { success in
|
|
2280
|
-
if success {
|
|
2281
|
-
call.resolve()
|
|
2282
|
-
} else {
|
|
2283
|
-
call.reject("Failed to open App Store")
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
return
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
let appStoreUrl = "https://apps.apple.com/app/id\(trackId)"
|
|
2291
|
-
guard let url = URL(string: appStoreUrl) else {
|
|
2292
|
-
call.reject("Invalid App Store URL")
|
|
2553
|
+
openFallbackAppStorePage()
|
|
2293
2554
|
return
|
|
2294
2555
|
}
|
|
2295
2556
|
|
|
2296
|
-
|
|
2297
|
-
UIApplication.shared.open(url) { success in
|
|
2298
|
-
if success {
|
|
2299
|
-
call.resolve()
|
|
2300
|
-
} else {
|
|
2301
|
-
call.reject("Failed to open App Store")
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2557
|
+
openAppStorePage(urlString: "https://apps.apple.com/app/id\(trackId)")
|
|
2305
2558
|
}
|
|
2306
2559
|
task.resume()
|
|
2307
2560
|
}
|