@capgo/capacitor-updater 8.45.10 → 8.45.11

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.
@@ -64,6 +64,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
64
64
  CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise),
65
65
  CAPPluginMethod(name: "setShakeChannelSelector", returnType: CAPPluginReturnPromise),
66
66
  CAPPluginMethod(name: "isShakeChannelSelectorEnabled", returnType: CAPPluginReturnPromise),
67
+ CAPPluginMethod(name: "getAppId", returnType: CAPPluginReturnPromise),
68
+ CAPPluginMethod(name: "setAppId", returnType: CAPPluginReturnPromise),
67
69
  // App Store update methods
68
70
  CAPPluginMethod(name: "getAppUpdateInfo", returnType: CAPPluginReturnPromise),
69
71
  CAPPluginMethod(name: "openAppStore", returnType: CAPPluginReturnPromise),
@@ -72,7 +74,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
72
74
  CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
73
75
  ]
74
76
  public var implementation = CapgoUpdater()
75
- private let pluginVersion: String = "8.45.10"
77
+ private let pluginVersion: String = "8.45.11"
76
78
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
77
79
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
78
80
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -233,7 +235,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
233
235
  implementation.setPublicKey(getConfig().getString("publicKey") ?? "")
234
236
  implementation.notifyDownloadRaw = notifyDownload
235
237
  implementation.notifyListeners = { [weak self] eventName, data in
236
- self?.notifyListeners(eventName, data: data)
238
+ let emit = {
239
+ self?.notifyListeners(eventName, data: data)
240
+ }
241
+ if Thread.isMainThread {
242
+ emit()
243
+ } else {
244
+ DispatchQueue.main.async {
245
+ emit()
246
+ }
247
+ }
237
248
  }
238
249
  implementation.pluginVersion = self.pluginVersion
239
250
 
@@ -279,11 +290,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
279
290
  }
280
291
  self.implementation.autoReset()
281
292
 
282
- // Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
293
+ // Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
283
294
  self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
284
295
 
285
296
  if resetWhenUpdate {
286
- self.cleanupObsoleteVersions()
297
+ let didResetCurrentBundle = self.resetCurrentBundleForNativeBuildChangeIfNeeded()
298
+ self.cleanupObsoleteVersions(didResetCurrentBundle: didResetCurrentBundle)
287
299
  }
288
300
 
289
301
  // Load the server
@@ -399,7 +411,29 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
399
411
  semaphoreReady.signal()
400
412
  }
401
413
 
402
- private func cleanupObsoleteVersions() {
414
+ func storedNativeBuildVersion() -> String {
415
+ UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
416
+ }
417
+
418
+ func hasNativeBuildVersionChanged() -> Bool {
419
+ let previous = self.storedNativeBuildVersion()
420
+ return previous != "0" && self.currentBuildVersion != previous
421
+ }
422
+
423
+ @discardableResult
424
+ func resetCurrentBundleForNativeBuildChangeIfNeeded() -> Bool {
425
+ let previous = self.storedNativeBuildVersion()
426
+ guard previous != "0" && self.currentBuildVersion != previous else {
427
+ return false
428
+ }
429
+
430
+ // Reset startup state synchronously so initialLoad() boots from the builtin bundle.
431
+ self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting startup bundle to builtin.")
432
+ self.implementation.reset(isInternal: true)
433
+ return true
434
+ }
435
+
436
+ private func cleanupObsoleteVersions(didResetCurrentBundle: Bool = false) {
403
437
  cleanupThread = Thread {
404
438
  self.cleanupLock.lock()
405
439
  defer {
@@ -434,9 +468,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
434
468
  // 1. Write "LatestVersionNative" - this fixes the part 1 of this bug
435
469
  // 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
470
 
437
- let previous = UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
471
+ let previous = self.storedNativeBuildVersion()
438
472
  if previous != "0" && self.currentBuildVersion != previous {
439
- _ = self._reset(toLastSuccessful: false, usePendingBundle: false)
473
+ if !didResetCurrentBundle {
474
+ self.logger.info("Native build version changed from \(previous) to \(self.currentBuildVersion). Resetting current bundle to builtin.")
475
+ self.implementation.reset(isInternal: true)
476
+ }
440
477
  let res = self.implementation.list()
441
478
  for version in res {
442
479
  // Check if thread was cancelled
@@ -496,11 +533,88 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
496
533
  logger.info("Cleanup finished, proceeding with download")
497
534
  }
498
535
 
536
+ private func resolveCall(_ call: CAPPluginCall, data: PluginCallResultData? = nil) {
537
+ let resolve = {
538
+ let savedCall = self.bridge?.savedCall(withID: call.callbackId)
539
+ let targetCall = savedCall ?? call
540
+
541
+ if let data {
542
+ targetCall.resolve(data)
543
+ } else {
544
+ targetCall.resolve()
545
+ }
546
+
547
+ if savedCall != nil {
548
+ self.bridge?.releaseCall(withID: call.callbackId)
549
+ }
550
+ }
551
+
552
+ if Thread.isMainThread {
553
+ resolve()
554
+ } else {
555
+ DispatchQueue.main.async {
556
+ resolve()
557
+ }
558
+ }
559
+ }
560
+
561
+ private func rejectCall(_ call: CAPPluginCall, message: String, code: String? = nil, error: Error? = nil, data: PluginCallResultData? = nil) {
562
+ let reject = {
563
+ let savedCall = self.bridge?.savedCall(withID: call.callbackId)
564
+ let targetCall = savedCall ?? call
565
+
566
+ targetCall.reject(message, code, error, data)
567
+
568
+ if savedCall != nil {
569
+ self.bridge?.releaseCall(withID: call.callbackId)
570
+ }
571
+ }
572
+
573
+ if Thread.isMainThread {
574
+ reject()
575
+ } else {
576
+ DispatchQueue.main.async {
577
+ reject()
578
+ }
579
+ }
580
+ }
581
+
582
+ private func saveCallForAsyncHandling(_ call: CAPPluginCall) {
583
+ bridge?.saveCall(call)
584
+ }
585
+
586
+ private func notifyListenersOnMain(_ eventName: String, data: JSObject) {
587
+ let notify = {
588
+ self.notifyListeners(eventName, data: data)
589
+ }
590
+
591
+ if Thread.isMainThread {
592
+ notify()
593
+ } else {
594
+ DispatchQueue.main.async {
595
+ notify()
596
+ }
597
+ }
598
+ }
599
+
600
+ private func bundlePayload(_ bundleInfo: BundleInfo) -> JSObject {
601
+ var payload: JSObject = [:]
602
+ for (key, value) in bundleInfo.toJSON() {
603
+ payload[key] = value
604
+ }
605
+ return payload
606
+ }
607
+
499
608
  @objc func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false, bundle: BundleInfo? = nil) {
500
609
  let bundleInfo = bundle ?? self.implementation.getBundleInfo(id: id)
501
- self.notifyListeners("download", data: ["percent": percent, "bundle": bundleInfo.toJSON()])
610
+ var downloadPayload: JSObject = [:]
611
+ downloadPayload["percent"] = percent
612
+ downloadPayload["bundle"] = bundlePayload(bundleInfo)
613
+ self.notifyListenersOnMain("download", data: downloadPayload)
502
614
  if percent == 100 {
503
- self.notifyListeners("downloadComplete", data: ["bundle": bundleInfo.toJSON()])
615
+ var downloadCompletePayload: JSObject = [:]
616
+ downloadCompletePayload["bundle"] = bundlePayload(bundleInfo)
617
+ self.notifyListenersOnMain("downloadComplete", data: downloadCompletePayload)
504
618
  self.implementation.sendStats(action: "download_complete", versionName: bundleInfo.getVersionName())
505
619
  } else if percent.isMultiple(of: 10) || ignoreMultipleOfTen {
506
620
  self.implementation.sendStats(action: "download_\(percent)", versionName: bundleInfo.getVersionName())
@@ -597,6 +711,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
597
711
  let manifestArray = call.getArray("manifest")
598
712
  let url = URL(string: urlString)
599
713
  logger.info("Downloading \(String(describing: url))")
714
+ self.saveCallForAsyncHandling(call)
600
715
  DispatchQueue.global(qos: .background).async {
601
716
  do {
602
717
  let next: BundleInfo
@@ -644,13 +759,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
644
759
  } else {
645
760
  self.logger.info("Good checksum \(next.getChecksum()) \(checksum)")
646
761
  }
647
- self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
648
- call.resolve(next.toJSON())
762
+ var updateAvailablePayload: JSObject = [:]
763
+ updateAvailablePayload["bundle"] = self.bundlePayload(next)
764
+ self.notifyListenersOnMain("updateAvailable", data: updateAvailablePayload)
765
+ self.resolveCall(call, data: next.toJSON())
649
766
  } catch {
650
767
  self.logger.error("Failed to download from: \(String(describing: url)) \(error.localizedDescription)")
651
- self.notifyListeners("downloadFailed", data: ["version": version])
768
+ var downloadFailedPayload: JSObject = [:]
769
+ downloadFailedPayload["version"] = version
770
+ self.notifyListenersOnMain("downloadFailed", data: downloadFailedPayload)
652
771
  self.implementation.sendStats(action: "download_fail")
653
- call.reject("Failed to download from: \(url!) - \(error.localizedDescription)")
772
+ self.rejectCall(call, message: "Failed to download from: \(url!) - \(error.localizedDescription)")
654
773
  }
655
774
  }
656
775
  }
@@ -876,25 +995,47 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
876
995
 
877
996
  @objc func getLatest(_ call: CAPPluginCall) {
878
997
  let channel = call.getString("channel")
879
- DispatchQueue.global(qos: .background).async {
998
+ self.saveCallForAsyncHandling(call)
999
+ runGetLatestWork {
880
1000
  let res = self.implementation.getLatest(url: URL(string: self.updateUrl)!, channel: channel)
881
- if res.error != nil {
882
- call.reject( res.error!)
883
- } else if res.message != nil {
884
- call.reject( res.message!)
1001
+ if let error = res.error, !error.isEmpty {
1002
+ let responseKind = self.updateResponseKind(kind: res.kind)
1003
+ res.kind = responseKind
1004
+ if responseKind == "failed" {
1005
+ self.rejectCall(call, message: error)
1006
+ } else {
1007
+ if res.version.isEmpty {
1008
+ res.version = self.implementation.getCurrentBundle().getVersionName()
1009
+ }
1010
+ self.resolveCall(call, data: res.toDict())
1011
+ }
1012
+ } else if let kind = res.kind, !kind.isEmpty {
1013
+ let responseKind = self.updateResponseKind(kind: kind)
1014
+ res.kind = responseKind
1015
+ if responseKind != "failed" {
1016
+ if res.version.isEmpty {
1017
+ res.version = self.implementation.getCurrentBundle().getVersionName()
1018
+ }
1019
+ self.resolveCall(call, data: res.toDict())
1020
+ } else {
1021
+ self.rejectCall(call, message: res.message ?? "server did not provide a message")
1022
+ }
1023
+ } else if let message = res.message, !message.isEmpty {
1024
+ self.rejectCall(call, message: message)
885
1025
  } else {
886
- call.resolve(res.toDict())
1026
+ self.resolveCall(call, data: res.toDict())
887
1027
  }
888
1028
  }
889
1029
  }
890
1030
 
891
1031
  @objc func unsetChannel(_ call: CAPPluginCall) {
892
1032
  let triggerAutoUpdate = call.getBool("triggerAutoUpdate", false)
893
- DispatchQueue.global(qos: .background).async {
1033
+ self.saveCallForAsyncHandling(call)
1034
+ DispatchQueue.global(qos: .utility).async {
894
1035
  let configDefaultChannel = self.getConfig().getString("defaultChannel", "")!
895
1036
  let res = self.implementation.unsetChannel(defaultChannelKey: self.defaultChannelDefaultsKey, configDefaultChannel: configDefaultChannel)
896
1037
  if res.error != "" {
897
- call.reject(res.error, "UNSETCHANNEL_FAILED", nil, [
1038
+ self.rejectCall(call, message: res.error, code: "UNSETCHANNEL_FAILED", data: [
898
1039
  "message": res.error,
899
1040
  "error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
900
1041
  ])
@@ -908,7 +1049,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
908
1049
  self.logger.info("Download already in progress, skipping duplicate download request")
909
1050
  }
910
1051
  }
911
- call.resolve(res.toDict())
1052
+ self.resolveCall(call, data: res.toDict())
912
1053
  }
913
1054
  }
914
1055
  }
@@ -923,17 +1064,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
923
1064
  return
924
1065
  }
925
1066
  let triggerAutoUpdate = call.getBool("triggerAutoUpdate") ?? false
926
- DispatchQueue.global(qos: .background).async {
1067
+ self.saveCallForAsyncHandling(call)
1068
+ DispatchQueue.global(qos: .utility).async {
927
1069
  let res = self.implementation.setChannel(channel: channel, defaultChannelKey: self.defaultChannelDefaultsKey, allowSetDefaultChannel: self.allowSetDefaultChannel)
928
1070
  if res.error != "" {
929
1071
  // Fire channelPrivate event if channel doesn't allow self-assignment
930
1072
  if res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed") {
931
- self.notifyListeners("channelPrivate", data: [
1073
+ self.notifyListenersOnMain("channelPrivate", data: [
932
1074
  "channel": channel,
933
1075
  "message": res.error
934
1076
  ])
935
1077
  }
936
- call.reject(res.error, "SETCHANNEL_FAILED", nil, [
1078
+ self.rejectCall(call, message: res.error, code: "SETCHANNEL_FAILED", data: [
937
1079
  "message": res.error,
938
1080
  "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
1081
  ])
@@ -947,35 +1089,39 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
947
1089
  self.logger.info("Download already in progress, skipping duplicate download request")
948
1090
  }
949
1091
  }
950
- call.resolve(res.toDict())
1092
+ self.resolveCall(call, data: res.toDict())
951
1093
  }
952
1094
  }
953
1095
  }
954
1096
 
955
1097
  @objc func getChannel(_ call: CAPPluginCall) {
956
- DispatchQueue.global(qos: .background).async {
1098
+ self.saveCallForAsyncHandling(call)
1099
+ DispatchQueue.global(qos: .utility).async {
957
1100
  let res = self.implementation.getChannel()
958
1101
  if res.error != "" {
959
- call.reject(res.error, "GETCHANNEL_FAILED", nil, [
1102
+ self.rejectCall(call, message: res.error, code: "GETCHANNEL_FAILED", data: [
960
1103
  "message": res.error,
961
1104
  "error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
962
1105
  ])
963
1106
  } else {
964
- call.resolve(res.toDict())
1107
+ self.resolveCall(call, data: res.toDict())
965
1108
  }
966
1109
  }
967
1110
  }
968
1111
 
969
1112
  @objc func listChannels(_ call: CAPPluginCall) {
970
- DispatchQueue.global(qos: .background).async {
1113
+ self.saveCallForAsyncHandling(call)
1114
+ DispatchQueue.global(qos: .utility).async {
971
1115
  let res = self.implementation.listChannels()
972
1116
  if res.error != "" {
973
- call.reject(res.error, "LISTCHANNELS_FAILED", nil, [
1117
+ self.rejectCall(call, message: res.error, code: "LISTCHANNELS_FAILED", data: [
974
1118
  "message": res.error,
975
1119
  "error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
976
1120
  ])
977
1121
  } else {
978
- call.resolve(res.toDict())
1122
+ var payload: JSObject = [:]
1123
+ payload["channels"] = res.channels
1124
+ self.resolveCall(call, data: payload)
979
1125
  }
980
1126
  }
981
1127
  }
@@ -1102,6 +1248,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1102
1248
  let bundle = self.implementation.getCurrentBundle()
1103
1249
  self.implementation.setSuccess(bundle: bundle, autoDeletePrevious: self.autoDeletePrevious)
1104
1250
  logger.info("Current bundle loaded successfully. [notifyAppReady was called] \(bundle.toString())")
1251
+
1105
1252
  call.resolve(["bundle": bundle.toJSON()])
1106
1253
  }
1107
1254
 
@@ -1191,7 +1338,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1191
1338
 
1192
1339
  logger.info("Current bundle is: \(current.toString())")
1193
1340
 
1194
- if BundleStatus.SUCCESS.localizedString != current.getStatus() {
1341
+ if BundleStatus.SUCCESS.storedValue != current.getStatus() {
1195
1342
  logger.error("notifyAppReady was not called, roll back current bundle: \(current.toString())")
1196
1343
  logger.error("Did you forget to call 'notifyAppReady()' in your Capacitor App code?")
1197
1344
  self.notifyListeners("updateFailed", data: [
@@ -1601,6 +1748,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1601
1748
  self.updateUrl = updateUrl
1602
1749
  }
1603
1750
 
1751
+ func setCurrentBuildVersionForTesting(_ currentBuildVersion: String) {
1752
+ self.currentBuildVersion = currentBuildVersion
1753
+ }
1754
+
1604
1755
  func shouldUseDirectUpdateForTesting() -> Bool {
1605
1756
  self.shouldUseDirectUpdate()
1606
1757
  }
@@ -1618,6 +1769,52 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1618
1769
  self.notifyListeners("majorAvailable", data: payload)
1619
1770
  }
1620
1771
 
1772
+ private func updateResponseKind(kind: String?) -> String {
1773
+ if let kind, ["up_to_date", "blocked", "failed"].contains(kind) {
1774
+ return kind
1775
+ }
1776
+ return "failed"
1777
+ }
1778
+
1779
+ private func endBackgroundDownloadAfterLatestError(
1780
+ backendError: String,
1781
+ res: AppVersion,
1782
+ current: BundleInfo,
1783
+ plannedDirectUpdate: Bool
1784
+ ) {
1785
+ let statusCode = res.statusCode
1786
+ let responseKind = self.updateResponseKind(kind: res.kind)
1787
+ let responseMessage = res.message?.isEmpty == false ? res.message : nil
1788
+ let message = responseMessage ?? (backendError.isEmpty ? "server did not provide a message" : backendError)
1789
+ let latestVersionName = res.version.isEmpty ? current.getVersionName() : res.version
1790
+ self.notifyListeners("updateCheckResult", data: [
1791
+ "kind": responseKind,
1792
+ "error": backendError,
1793
+ "message": message,
1794
+ "statusCode": statusCode,
1795
+ "version": latestVersionName,
1796
+ "bundle": current.toJSON()
1797
+ ])
1798
+
1799
+ if responseKind == "up_to_date" {
1800
+ self.logger.info("No new version available")
1801
+ } else if responseKind == "blocked" {
1802
+ self.logger.info("Update check blocked with error: \(backendError)")
1803
+ } else {
1804
+ self.logger.error("getLatest failed with error: \(backendError)")
1805
+ }
1806
+
1807
+ let isFailure = responseKind == "failed"
1808
+ self.endBackGroundTaskWithNotif(
1809
+ msg: message,
1810
+ latestVersionName: latestVersionName,
1811
+ current: current,
1812
+ error: isFailure,
1813
+ plannedDirectUpdate: plannedDirectUpdate,
1814
+ sendStats: isFailure
1815
+ )
1816
+ }
1817
+
1621
1818
  func endBackGroundTaskWithNotif(
1622
1819
  msg: String,
1623
1820
  latestVersionName: String,
@@ -1672,6 +1869,26 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1672
1869
  }
1673
1870
 
1674
1871
  func runBackgroundDownloadWork(_ work: @escaping () -> Void) {
1872
+ // Live update checks/downloads are user-visible work. Using `.background`
1873
+ // lets the scheduler starve them for minutes while the app is active.
1874
+ DispatchQueue.global(qos: .utility).async(execute: work)
1875
+ }
1876
+
1877
+ private func beginDownloadBackgroundTask() {
1878
+ let registerTask = {
1879
+ self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Download Tasks") {
1880
+ self.endBackGroundTask()
1881
+ }
1882
+ }
1883
+
1884
+ if Thread.isMainThread {
1885
+ registerTask()
1886
+ } else {
1887
+ DispatchQueue.main.sync(execute: registerTask)
1888
+ }
1889
+ }
1890
+
1891
+ func runGetLatestWork(_ work: @escaping () -> Void) {
1675
1892
  DispatchQueue.global(qos: .background).async(execute: work)
1676
1893
  }
1677
1894
 
@@ -1697,26 +1914,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1697
1914
  self.runBackgroundDownloadWork {
1698
1915
  // Wait for cleanup to complete before starting download
1699
1916
  self.waitForCleanupIfNeeded()
1700
- self.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish Download Tasks") {
1701
- // End the task if time expires.
1702
- self.endBackGroundTask()
1703
- }
1917
+ self.beginDownloadBackgroundTask()
1704
1918
  self.logger.info("Check for update via \(self.updateUrl)")
1705
1919
  let res = self.implementation.getLatest(url: url, channel: nil)
1706
1920
  let current = self.implementation.getCurrentBundle()
1707
1921
 
1708
1922
  // Handle network errors and other failures first
1709
- if let backendError = res.error, !backendError.isEmpty {
1710
- self.logger.error("getLatest failed with error: \(backendError)")
1711
- let statusCode = res.statusCode
1712
- let responseIsOk = statusCode >= 200 && statusCode < 300
1713
- self.endBackGroundTaskWithNotif(
1714
- msg: res.message ?? backendError,
1715
- latestVersionName: res.version,
1923
+ let backendError = res.error ?? ""
1924
+ let backendKind = res.kind ?? ""
1925
+ if !backendError.isEmpty || !backendKind.isEmpty {
1926
+ self.endBackgroundDownloadAfterLatestError(
1927
+ backendError: backendError,
1928
+ res: res,
1716
1929
  current: current,
1717
- error: true,
1718
- plannedDirectUpdate: plannedDirectUpdate,
1719
- sendStats: !responseIsOk
1930
+ plannedDirectUpdate: plannedDirectUpdate
1720
1931
  )
1721
1932
  return
1722
1933
  }
@@ -1987,7 +2198,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
1987
2198
  timer.invalidate()
1988
2199
  return
1989
2200
  }
1990
- DispatchQueue.global(qos: .background).async {
2201
+ DispatchQueue.global(qos: .utility).async {
1991
2202
  let res = self.implementation.getLatest(url: url, channel: nil)
1992
2203
  let current = self.implementation.getCurrentBundle()
1993
2204
 
@@ -2139,29 +2350,30 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2139
2350
 
2140
2351
  logger.info("Getting App Store update info for \(bundleId) in country \(country)")
2141
2352
 
2353
+ self.saveCallForAsyncHandling(call)
2142
2354
  DispatchQueue.global(qos: .background).async {
2143
2355
  let urlString = "https://itunes.apple.com/lookup?bundleId=\(bundleId)&country=\(country)"
2144
2356
  guard let url = URL(string: urlString) else {
2145
- call.reject("Invalid URL for App Store lookup")
2357
+ self.rejectCall(call, message: "Invalid URL for App Store lookup")
2146
2358
  return
2147
2359
  }
2148
2360
 
2149
2361
  let task = URLSession.shared.dataTask(with: url) { data, _, error in
2150
2362
  if let error = error {
2151
2363
  self.logger.error("App Store lookup failed: \(error.localizedDescription)")
2152
- call.reject("App Store lookup failed: \(error.localizedDescription)")
2364
+ self.rejectCall(call, message: "App Store lookup failed: \(error.localizedDescription)")
2153
2365
  return
2154
2366
  }
2155
2367
 
2156
2368
  guard let data = data else {
2157
- call.reject("No data received from App Store")
2369
+ self.rejectCall(call, message: "No data received from App Store")
2158
2370
  return
2159
2371
  }
2160
2372
 
2161
2373
  do {
2162
2374
  guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
2163
2375
  let resultCount = json["resultCount"] as? Int else {
2164
- call.reject("Invalid response from App Store")
2376
+ self.rejectCall(call, message: "Invalid response from App Store")
2165
2377
  return
2166
2378
  }
2167
2379
 
@@ -2218,10 +2430,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2218
2430
  self.logger.info("App not found in App Store for bundleId: \(bundleId)")
2219
2431
  }
2220
2432
 
2221
- call.resolve(result)
2433
+ self.resolveCall(call, data: result)
2222
2434
  } catch {
2223
2435
  self.logger.error("Failed to parse App Store response: \(error.localizedDescription)")
2224
- call.reject("Failed to parse App Store response: \(error.localizedDescription)")
2436
+ self.rejectCall(call, message: "Failed to parse App Store response: \(error.localizedDescription)")
2225
2437
  }
2226
2438
  }
2227
2439
  task.resume()
@@ -2230,37 +2442,48 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2230
2442
 
2231
2443
  @objc func openAppStore(_ call: CAPPluginCall) {
2232
2444
  let appId = call.getString("appId")
2445
+ let bundleId = implementation.appId
2446
+ self.saveCallForAsyncHandling(call)
2233
2447
 
2234
- if let appId = appId {
2235
- // Open App Store with provided app ID
2236
- let urlString = "https://apps.apple.com/app/id\(appId)"
2448
+ func openAppStorePage(urlString: String, invalidMessage: String = "Invalid App Store URL", failureMessage: String = "Failed to open App Store") {
2237
2449
  guard let url = URL(string: urlString) else {
2238
- call.reject("Invalid App Store URL")
2450
+ self.rejectCall(call, message: invalidMessage)
2239
2451
  return
2240
2452
  }
2241
2453
  DispatchQueue.main.async {
2242
2454
  UIApplication.shared.open(url) { success in
2243
2455
  if success {
2244
- call.resolve()
2456
+ self.resolveCall(call)
2245
2457
  } else {
2246
- call.reject("Failed to open App Store")
2458
+ self.rejectCall(call, message: failureMessage)
2247
2459
  }
2248
2460
  }
2249
2461
  }
2462
+ }
2463
+
2464
+ func openFallbackAppStorePage() {
2465
+ guard let encodedBundleId = bundleId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
2466
+ self.rejectCall(call, message: "Failed to build App Store fallback URL")
2467
+ return
2468
+ }
2469
+ openAppStorePage(urlString: "https://apps.apple.com/app/\(encodedBundleId)")
2470
+ }
2471
+
2472
+ if let appId = appId {
2473
+ openAppStorePage(urlString: "https://apps.apple.com/app/id\(appId)")
2250
2474
  } else {
2251
- // Look up app ID using bundle identifier
2252
- let bundleId = implementation.appId
2253
2475
  let lookupUrl = "https://itunes.apple.com/lookup?bundleId=\(bundleId)"
2254
2476
 
2255
2477
  DispatchQueue.global(qos: .background).async {
2256
2478
  guard let url = URL(string: lookupUrl) else {
2257
- call.reject("Invalid lookup URL")
2479
+ openFallbackAppStorePage()
2258
2480
  return
2259
2481
  }
2260
2482
 
2261
2483
  let task = URLSession.shared.dataTask(with: url) { data, _, error in
2262
2484
  if let error = error {
2263
- call.reject("Failed to lookup app: \(error.localizedDescription)")
2485
+ self.logger.error("App Store lookup failed: \(error.localizedDescription)")
2486
+ openFallbackAppStorePage()
2264
2487
  return
2265
2488
  }
2266
2489
 
@@ -2269,39 +2492,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
2269
2492
  let results = json["results"] as? [[String: Any]],
2270
2493
  let appInfo = results.first,
2271
2494
  let trackId = appInfo["trackId"] as? Int else {
2272
- // If lookup fails, try opening the generic App Store app page using bundle ID
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
- }
2495
+ openFallbackAppStorePage()
2287
2496
  return
2288
2497
  }
2289
2498
 
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")
2293
- return
2294
- }
2295
-
2296
- DispatchQueue.main.async {
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
- }
2499
+ openAppStorePage(urlString: "https://apps.apple.com/app/id\(trackId)")
2305
2500
  }
2306
2501
  task.resume()
2307
2502
  }