@capgo/capacitor-updater 7.4.0 → 7.6.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.
@@ -14,7 +14,7 @@ import Version
14
14
  */
15
15
  @objc(CapacitorUpdaterPlugin)
16
16
  public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
17
- private let logger = Logger(withTag: "✨ CapgoUpdater")
17
+ let logger = Logger(withTag: "✨ CapgoUpdater")
18
18
 
19
19
  public let identifier = "CapacitorUpdaterPlugin"
20
20
  public let jsName = "CapacitorUpdater"
@@ -37,6 +37,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
37
37
  CAPPluginMethod(name: "setChannel", returnType: CAPPluginReturnPromise),
38
38
  CAPPluginMethod(name: "unsetChannel", returnType: CAPPluginReturnPromise),
39
39
  CAPPluginMethod(name: "getChannel", returnType: CAPPluginReturnPromise),
40
+ CAPPluginMethod(name: "listChannels", returnType: CAPPluginReturnPromise),
40
41
  CAPPluginMethod(name: "setCustomId", returnType: CAPPluginReturnPromise),
41
42
  CAPPluginMethod(name: "getDeviceId", returnType: CAPPluginReturnPromise),
42
43
  CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise),
@@ -44,10 +45,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
44
45
  CAPPluginMethod(name: "isAutoUpdateEnabled", returnType: CAPPluginReturnPromise),
45
46
  CAPPluginMethod(name: "getBuiltinVersion", returnType: CAPPluginReturnPromise),
46
47
  CAPPluginMethod(name: "isAutoUpdateAvailable", returnType: CAPPluginReturnPromise),
47
- CAPPluginMethod(name: "getNextBundle", returnType: CAPPluginReturnPromise)
48
+ CAPPluginMethod(name: "getNextBundle", returnType: CAPPluginReturnPromise),
49
+ CAPPluginMethod(name: "setShakeMenu", returnType: CAPPluginReturnPromise),
50
+ CAPPluginMethod(name: "isShakeMenuEnabled", returnType: CAPPluginReturnPromise)
48
51
  ]
49
52
  public var implementation = CapgoUpdater()
50
- private let PLUGIN_VERSION: String = "7.4.0"
53
+ private let PLUGIN_VERSION: String = "7.6.0"
51
54
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
52
55
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
53
56
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -61,12 +64,16 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
61
64
  private var appReadyCheck: DispatchWorkItem?
62
65
  private var resetWhenUpdate = true
63
66
  private var directUpdate = false
67
+ private var directUpdateMode: String = "false"
68
+ private var wasRecentlyInstalledOrUpdated = false
69
+ private var autoSplashscreen = false
64
70
  private var autoDeleteFailed = false
65
71
  private var autoDeletePrevious = false
66
72
  private var keepUrlPathAfterReload = false
67
73
  private var backgroundWork: DispatchWorkItem?
68
74
  private var taskRunning = false
69
75
  private var periodCheckDelay = 0
76
+ public var shakeMenuEnabled = false
70
77
  let semaphoreReady = DispatchSemaphore(value: 0)
71
78
 
72
79
  private var delayUpdateUtils: DelayUpdateUtils!
@@ -105,12 +112,29 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
105
112
  autoDeleteFailed = getConfig().getBoolean("autoDeleteFailed", true)
106
113
  autoDeletePrevious = getConfig().getBoolean("autoDeletePrevious", true)
107
114
  keepUrlPathAfterReload = getConfig().getBoolean("keepUrlPathAfterReload", false)
108
- directUpdate = getConfig().getBoolean("directUpdate", false)
115
+
116
+ // Handle directUpdate configuration - support string values and backward compatibility
117
+ if let directUpdateString = getConfig().getString("directUpdate") {
118
+ directUpdateMode = directUpdateString
119
+ directUpdate = directUpdateString == "always" || directUpdateString == "atInstall"
120
+ } else {
121
+ let directUpdateBool = getConfig().getBoolean("directUpdate", false)
122
+ if directUpdateBool {
123
+ directUpdateMode = "always" // backward compatibility: true = always
124
+ directUpdate = true
125
+ } else {
126
+ directUpdateMode = "false"
127
+ directUpdate = false
128
+ }
129
+ }
130
+
131
+ autoSplashscreen = getConfig().getBoolean("autoSplashscreen", false)
109
132
  updateUrl = getConfig().getString("updateUrl", CapacitorUpdaterPlugin.updateUrlDefault)!
110
133
  autoUpdate = getConfig().getBoolean("autoUpdate", true)
111
134
  appReadyTimeout = getConfig().getInt("appReadyTimeout", 10000)
112
135
  implementation.timeout = Double(getConfig().getInt("responseTimeout", 20))
113
136
  resetWhenUpdate = getConfig().getBoolean("resetWhenUpdate", true)
137
+ shakeMenuEnabled = getConfig().getBoolean("shakeMenu", false)
114
138
  let periodCheckDelayValue = getConfig().getInt("periodCheckDelay", 0)
115
139
  if periodCheckDelayValue >= 0 && periodCheckDelayValue > 600 {
116
140
  periodCheckDelay = 600
@@ -143,6 +167,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
143
167
  implementation.defaultChannel = getConfig().getString("defaultChannel", "")!
144
168
  self.implementation.autoReset()
145
169
 
170
+ // Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
171
+ self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
172
+
146
173
  if resetWhenUpdate {
147
174
  self.cleanupObsoleteVersions()
148
175
  }
@@ -493,7 +520,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
493
520
  DispatchQueue.global(qos: .background).async {
494
521
  let res = self.implementation.unsetChannel()
495
522
  if res.error != "" {
496
- call.reject(res.error)
523
+ call.reject(res.error, "UNSETCHANNEL_FAILED", nil, [
524
+ "message": res.error,
525
+ "error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
526
+ ])
497
527
  } else {
498
528
  if self._isAutoUpdateEnabled() && triggerAutoUpdate {
499
529
  self.logger.info("Calling autoupdater after channel change!")
@@ -507,14 +537,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
507
537
  @objc func setChannel(_ call: CAPPluginCall) {
508
538
  guard let channel = call.getString("channel") else {
509
539
  logger.error("setChannel called without channel")
510
- call.reject("setChannel called without channel")
540
+ call.reject("setChannel called without channel", "SETCHANNEL_INVALID_PARAMS", nil, [
541
+ "message": "setChannel called without channel",
542
+ "error": "missing_parameter"
543
+ ])
511
544
  return
512
545
  }
513
546
  let triggerAutoUpdate = call.getBool("triggerAutoUpdate") ?? false
514
547
  DispatchQueue.global(qos: .background).async {
515
548
  let res = self.implementation.setChannel(channel: channel)
516
549
  if res.error != "" {
517
- call.reject(res.error)
550
+ call.reject(res.error, "SETCHANNEL_FAILED", nil, [
551
+ "message": res.error,
552
+ "error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
553
+ ])
518
554
  } else {
519
555
  if self._isAutoUpdateEnabled() && triggerAutoUpdate {
520
556
  self.logger.info("Calling autoupdater after channel change!")
@@ -529,12 +565,30 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
529
565
  DispatchQueue.global(qos: .background).async {
530
566
  let res = self.implementation.getChannel()
531
567
  if res.error != "" {
532
- call.reject(res.error)
568
+ call.reject(res.error, "GETCHANNEL_FAILED", nil, [
569
+ "message": res.error,
570
+ "error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
571
+ ])
572
+ } else {
573
+ call.resolve(res.toDict())
574
+ }
575
+ }
576
+ }
577
+
578
+ @objc func listChannels(_ call: CAPPluginCall) {
579
+ DispatchQueue.global(qos: .background).async {
580
+ let res = self.implementation.listChannels()
581
+ if res.error != "" {
582
+ call.reject(res.error, "LISTCHANNELS_FAILED", nil, [
583
+ "message": res.error,
584
+ "error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
585
+ ])
533
586
  } else {
534
587
  call.resolve(res.toDict())
535
588
  }
536
589
  }
537
590
  }
591
+
538
592
  @objc func setCustomId(_ call: CAPPluginCall) {
539
593
  guard let customId = call.getString("customId") else {
540
594
  logger.error("setCustomId called without customId")
@@ -717,6 +771,75 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
717
771
  DispatchQueue.global().async {
718
772
  self.semaphoreWait(waitTime: self.appReadyTimeout)
719
773
  self.notifyListeners("appReady", data: ["bundle": current.toJSON(), "status": msg])
774
+
775
+ // Auto hide splashscreen if enabled
776
+ if self.autoSplashscreen && self.shouldUseDirectUpdate() {
777
+ self.hideSplashscreen()
778
+ }
779
+ }
780
+ }
781
+
782
+ private func hideSplashscreen() {
783
+ DispatchQueue.main.async {
784
+ guard let bridge = self.bridge else {
785
+ self.logger.warn("Bridge not available for hiding splashscreen")
786
+ return
787
+ }
788
+
789
+ // Create a plugin call for the hide method
790
+ let call = CAPPluginCall(callbackId: "autoHideSplashscreen", options: [:], success: { (_, _) in
791
+ self.logger.info("Splashscreen hidden automatically")
792
+ }, error: { (_) in
793
+ self.logger.error("Failed to auto-hide splashscreen")
794
+ })
795
+
796
+ // Try to call the SplashScreen hide method directly through the bridge
797
+ if let splashScreenPlugin = bridge.plugin(withName: "SplashScreen") {
798
+ // Use runtime method invocation to call hide method
799
+ let selector = NSSelectorFromString("hide:")
800
+ if splashScreenPlugin.responds(to: selector) {
801
+ _ = splashScreenPlugin.perform(selector, with: call)
802
+ self.logger.info("Called SplashScreen hide method")
803
+ } else {
804
+ self.logger.warn("SplashScreen plugin does not respond to hide: method")
805
+ }
806
+ } else {
807
+ self.logger.warn("SplashScreen plugin not found")
808
+ }
809
+ }
810
+ }
811
+
812
+ private func checkIfRecentlyInstalledOrUpdated() -> Bool {
813
+ let userDefaults = UserDefaults.standard
814
+ let currentVersion = self.currentVersionNative.description
815
+ let lastKnownVersion = userDefaults.string(forKey: "LatestVersionNative") ?? "0.0.0"
816
+
817
+ if lastKnownVersion == "0.0.0" {
818
+ // First time running, consider it as recently installed
819
+ return true
820
+ } else if lastKnownVersion != currentVersion {
821
+ // Version changed, consider it as recently updated
822
+ return true
823
+ }
824
+
825
+ return false
826
+ }
827
+
828
+ private func shouldUseDirectUpdate() -> Bool {
829
+ switch directUpdateMode {
830
+ case "false":
831
+ return false
832
+ case "always":
833
+ return true
834
+ case "atInstall":
835
+ if self.wasRecentlyInstalledOrUpdated {
836
+ // Reset the flag after first use to prevent subsequent foreground events from using direct update
837
+ self.wasRecentlyInstalledOrUpdated = false
838
+ return true
839
+ }
840
+ return false
841
+ default:
842
+ return false
720
843
  }
721
844
  }
722
845
 
@@ -732,7 +855,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
732
855
  }
733
856
 
734
857
  func backgroundDownload() {
735
- let messageUpdate = self.directUpdate ? "Update will occur now." : "Update will occur next time app moves to background."
858
+ let shouldDirectUpdate = self.shouldUseDirectUpdate()
859
+ let messageUpdate = shouldDirectUpdate ? "Update will occur now." : "Update will occur next time app moves to background."
736
860
  guard let url = URL(string: self.updateUrl) else {
737
861
  logger.error("Error no url or wrong format")
738
862
  return
@@ -756,7 +880,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
756
880
  }
757
881
  if res.version == "builtin" {
758
882
  self.logger.info("Latest version is builtin")
759
- if self.directUpdate {
883
+ if shouldDirectUpdate {
760
884
  self.logger.info("Direct update to builtin version")
761
885
  _ = self._reset(toLastSuccessful: false)
762
886
  self.endBackGroundTaskWithNotif(msg: "Updated to builtin version", latestVersionName: res.version, current: self.implementation.getCurrentBundle(), error: false)
@@ -816,7 +940,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
816
940
  self.endBackGroundTaskWithNotif(msg: "Error checksum", latestVersionName: latestVersionName, current: current)
817
941
  return
818
942
  }
819
- if self.directUpdate {
943
+ if shouldDirectUpdate {
820
944
  let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
821
945
  let delayConditionList: [DelayCondition] = self.fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
822
946
  let kind: String = obj.value(forKey: "kind") as! String
@@ -960,4 +1084,22 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
960
1084
 
961
1085
  call.resolve(bundle!.toJSON())
962
1086
  }
1087
+
1088
+ @objc func setShakeMenu(_ call: CAPPluginCall) {
1089
+ guard let enabled = call.getBool("enabled") else {
1090
+ logger.error("setShakeMenu called without enabled parameter")
1091
+ call.reject("setShakeMenu called without enabled parameter")
1092
+ return
1093
+ }
1094
+
1095
+ self.shakeMenuEnabled = enabled
1096
+ logger.info("Shake menu \(enabled ? "enabled" : "disabled")")
1097
+ call.resolve()
1098
+ }
1099
+
1100
+ @objc func isShakeMenuEnabled(_ call: CAPPluginCall) {
1101
+ call.resolve([
1102
+ "enabled": self.shakeMenuEnabled
1103
+ ])
1104
+ }
963
1105
  }
@@ -955,19 +955,17 @@ import UIKit
955
955
  request.validate().responseDecodable(of: SetChannelDec.self) { response in
956
956
  switch response.result {
957
957
  case .success:
958
- if let status = response.value?.status {
959
- setChannel.status = status
960
- }
961
- if let error = response.value?.error {
962
- setChannel.error = error
963
- }
964
- if let message = response.value?.message {
965
- setChannel.message = message
958
+ if let responseValue = response.value {
959
+ if let error = responseValue.error {
960
+ setChannel.error = error
961
+ } else {
962
+ setChannel.status = responseValue.status ?? ""
963
+ setChannel.message = responseValue.message ?? ""
964
+ }
966
965
  }
967
966
  case let .failure(error):
968
- self.logger.error("Error unset Channel \(response.value.debugDescription) \(error)")
969
- setChannel.message = "Error unset Channel \(String(describing: response.value))"
970
- setChannel.error = "response_error"
967
+ self.logger.error("Error unset Channel \(error)")
968
+ setChannel.error = "Request failed: \(error.localizedDescription)"
971
969
  }
972
970
  semaphore.signal()
973
971
  }
@@ -992,19 +990,17 @@ import UIKit
992
990
  request.validate().responseDecodable(of: SetChannelDec.self) { response in
993
991
  switch response.result {
994
992
  case .success:
995
- if let status = response.value?.status {
996
- setChannel.status = status
997
- }
998
- if let error = response.value?.error {
999
- setChannel.error = error
1000
- }
1001
- if let message = response.value?.message {
1002
- setChannel.message = message
993
+ if let responseValue = response.value {
994
+ if let error = responseValue.error {
995
+ setChannel.error = error
996
+ } else {
997
+ setChannel.status = responseValue.status ?? ""
998
+ setChannel.message = responseValue.message ?? ""
999
+ }
1003
1000
  }
1004
1001
  case let .failure(error):
1005
- self.logger.error("Error set Channel \(response.value.debugDescription) \(error)")
1006
- setChannel.message = "Error set Channel \(String(describing: response.value))"
1007
- setChannel.error = "response_error"
1002
+ self.logger.error("Error set Channel \(error)")
1003
+ setChannel.error = "Request failed: \(error.localizedDescription)"
1008
1004
  }
1009
1005
  semaphore.signal()
1010
1006
  }
@@ -1030,20 +1026,15 @@ import UIKit
1030
1026
  }
1031
1027
  switch response.result {
1032
1028
  case .success:
1033
- if let status = response.value?.status {
1034
- getChannel.status = status
1035
- }
1036
- if let error = response.value?.error {
1037
- getChannel.error = error
1038
- }
1039
- if let message = response.value?.message {
1040
- getChannel.message = message
1041
- }
1042
- if let channel = response.value?.channel {
1043
- getChannel.channel = channel
1044
- }
1045
- if let allowSet = response.value?.allowSet {
1046
- getChannel.allowSet = allowSet
1029
+ if let responseValue = response.value {
1030
+ if let error = responseValue.error {
1031
+ getChannel.error = error
1032
+ } else {
1033
+ getChannel.status = responseValue.status ?? ""
1034
+ getChannel.message = responseValue.message ?? ""
1035
+ getChannel.channel = responseValue.channel ?? ""
1036
+ getChannel.allowSet = responseValue.allowSet ?? true
1037
+ }
1047
1038
  }
1048
1039
  case let .failure(error):
1049
1040
  if let data = response.data, let bodyString = String(data: data, encoding: .utf8) {
@@ -1054,15 +1045,81 @@ import UIKit
1054
1045
  }
1055
1046
  }
1056
1047
 
1057
- self.logger.error("Error get Channel \(response.value.debugDescription) \(error)")
1058
- getChannel.message = "Error get Channel \(String(describing: response.value)))"
1059
- getChannel.error = "response_error"
1048
+ self.logger.error("Error get Channel \(error)")
1049
+ getChannel.error = "Request failed: \(error.localizedDescription)"
1060
1050
  }
1061
1051
  }
1062
1052
  semaphore.wait()
1063
1053
  return getChannel
1064
1054
  }
1065
1055
 
1056
+ func listChannels() -> ListChannels {
1057
+ let listChannels: ListChannels = ListChannels()
1058
+ if (self.channelUrl).isEmpty {
1059
+ logger.error("Channel URL is not set")
1060
+ listChannels.error = "Channel URL is not set"
1061
+ return listChannels
1062
+ }
1063
+
1064
+ let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
1065
+
1066
+ // Auto-detect values
1067
+ let appId = self.appId
1068
+ let platform = "ios"
1069
+ let isEmulator = self.isEmulator()
1070
+ let isProd = self.isProd()
1071
+
1072
+ // Create query parameters
1073
+ var urlComponents = URLComponents(string: self.channelUrl)
1074
+ urlComponents?.queryItems = [
1075
+ URLQueryItem(name: "app_id", value: appId),
1076
+ URLQueryItem(name: "platform", value: platform),
1077
+ URLQueryItem(name: "is_emulator", value: String(isEmulator)),
1078
+ URLQueryItem(name: "is_prod", value: String(isProd))
1079
+ ]
1080
+
1081
+ guard let url = urlComponents?.url else {
1082
+ logger.error("Invalid channel URL")
1083
+ listChannels.error = "Invalid channel URL"
1084
+ return listChannels
1085
+ }
1086
+
1087
+ let request = AF.request(url, method: .get, requestModifier: { $0.timeoutInterval = self.timeout })
1088
+
1089
+ request.validate().responseDecodable(of: ListChannelsDec.self) { response in
1090
+ defer {
1091
+ semaphore.signal()
1092
+ }
1093
+ switch response.result {
1094
+ case .success:
1095
+ if let responseValue = response.value {
1096
+ // Check for server-side errors
1097
+ if let error = responseValue.error {
1098
+ listChannels.error = error
1099
+ return
1100
+ }
1101
+
1102
+ // Backend returns direct array, so channels should be populated by our custom decoder
1103
+ if let channels = responseValue.channels {
1104
+ listChannels.channels = channels.map { channel in
1105
+ var channelDict: [String: Any] = [:]
1106
+ channelDict["id"] = channel.id ?? ""
1107
+ channelDict["name"] = channel.name ?? ""
1108
+ channelDict["public"] = channel.public ?? false
1109
+ channelDict["allow_self_set"] = channel.allow_self_set ?? false
1110
+ return channelDict
1111
+ }
1112
+ }
1113
+ }
1114
+ case let .failure(error):
1115
+ self.logger.error("Error list channels \(error)")
1116
+ listChannels.error = "Request failed: \(error.localizedDescription)"
1117
+ }
1118
+ }
1119
+ semaphore.wait()
1120
+ return listChannels
1121
+ }
1122
+
1066
1123
  private let operationQueue = OperationQueue()
1067
1124
 
1068
1125
  func sendStats(action: String, versionName: String? = nil, oldVersionName: String? = "") {
@@ -67,6 +67,51 @@ extension GetChannel {
67
67
  return dict
68
68
  }
69
69
  }
70
+ struct ChannelInfo: Codable {
71
+ let id: String?
72
+ let name: String?
73
+ let `public`: Bool?
74
+ let allow_self_set: Bool?
75
+ }
76
+ struct ListChannelsDec: Decodable {
77
+ let channels: [ChannelInfo]?
78
+ let error: String?
79
+
80
+ init(from decoder: Decoder) throws {
81
+ let container = try decoder.singleValueContainer()
82
+
83
+ if let channelsArray = try? container.decode([ChannelInfo].self) {
84
+ // Backend returns direct array
85
+ self.channels = channelsArray
86
+ self.error = nil
87
+ } else {
88
+ // Handle error response
89
+ let errorContainer = try decoder.container(keyedBy: CodingKeys.self)
90
+ self.channels = nil
91
+ self.error = try? errorContainer.decode(String.self, forKey: .error)
92
+ }
93
+ }
94
+
95
+ private enum CodingKeys: String, CodingKey {
96
+ case error
97
+ }
98
+ }
99
+ public class ListChannels: NSObject {
100
+ var channels: [[String: Any]] = []
101
+ var error: String = ""
102
+ }
103
+ extension ListChannels {
104
+ func toDict() -> [String: Any] {
105
+ var dict: [String: Any] = [String: Any]()
106
+ let otherSelf: Mirror = Mirror(reflecting: self)
107
+ for child: Mirror.Child in otherSelf.children {
108
+ if let key: String = child.label {
109
+ dict[key] = child.value
110
+ }
111
+ }
112
+ return dict
113
+ }
114
+ }
70
115
  struct InfoObject: Codable {
71
116
  let platform: String?
72
117
  let device_id: String?
@@ -0,0 +1,112 @@
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 UIKit
8
+ import Capacitor
9
+
10
+ extension UIApplication {
11
+ public class func topViewController(_ base: UIViewController? = UIApplication.shared.windows.first?.rootViewController) -> UIViewController? {
12
+ if let nav = base as? UINavigationController {
13
+ return topViewController(nav.visibleViewController)
14
+ }
15
+ if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
16
+ return topViewController(selected)
17
+ }
18
+ if let presented = base?.presentedViewController {
19
+ return topViewController(presented)
20
+ }
21
+ return base
22
+ }
23
+ }
24
+
25
+ extension UIWindow {
26
+ override open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
27
+ if motion == .motionShake {
28
+ // Find the CapacitorUpdaterPlugin instance
29
+ guard let bridge = (rootViewController as? CAPBridgeProtocol),
30
+ let plugin = bridge.plugin(withName: "CapacitorUpdaterPlugin") as? CapacitorUpdaterPlugin else {
31
+ return
32
+ }
33
+
34
+ // Check if shake menu is enabled
35
+ if !plugin.shakeMenuEnabled {
36
+ return
37
+ }
38
+
39
+ showShakeMenu(plugin: plugin, bridge: bridge)
40
+ }
41
+ }
42
+
43
+ private func showShakeMenu(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) {
44
+ // Prevent multiple alerts from showing
45
+ if let topVC = UIApplication.topViewController(),
46
+ topVC.isKind(of: UIAlertController.self) {
47
+ plugin.logger.info("UIAlertController is already presented")
48
+ return
49
+ }
50
+
51
+ let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? "App"
52
+ let title = "Preview \(appName) Menu"
53
+ let message = "What would you like to do?"
54
+ let okButtonTitle = "Go Home"
55
+ let reloadButtonTitle = "Reload app"
56
+ let cancelButtonTitle = "Close menu"
57
+
58
+ let updater = plugin.implementation
59
+
60
+ func resetBuiltin() {
61
+ updater.reset()
62
+ bridge.setServerBasePath("")
63
+ DispatchQueue.main.async {
64
+ if let vc = (self.rootViewController as? CAPBridgeViewController) {
65
+ vc.loadView()
66
+ vc.viewDidLoad()
67
+ }
68
+ _ = updater.delete(id: updater.getCurrentBundleId())
69
+ plugin.logger.info("Reset to builtin version")
70
+ }
71
+ }
72
+
73
+ let bundleId = updater.getCurrentBundleId()
74
+ if let vc = (self.rootViewController as? CAPBridgeViewController) {
75
+ plugin.logger.info("getServerBasePath: \(vc.getServerBasePath())")
76
+ }
77
+ plugin.logger.info("bundleId: \(bundleId)")
78
+
79
+ let alertShake = UIAlertController(title: title, message: message, preferredStyle: .alert)
80
+
81
+ alertShake.addAction(UIAlertAction(title: okButtonTitle, style: .default) { _ in
82
+ guard let next = updater.getNextBundle() else {
83
+ resetBuiltin()
84
+ return
85
+ }
86
+ if !next.isBuiltin() {
87
+ plugin.logger.info("Resetting to: \(next.toString())")
88
+ _ = updater.set(bundle: next)
89
+ let destHot = updater.getBundleDirectory(id: next.getId())
90
+ plugin.logger.info("Reloading \(next.toString())")
91
+ bridge.setServerBasePath(destHot.path)
92
+ } else {
93
+ resetBuiltin()
94
+ }
95
+ plugin.logger.info("Reload app done")
96
+ })
97
+
98
+ alertShake.addAction(UIAlertAction(title: cancelButtonTitle, style: .default))
99
+
100
+ alertShake.addAction(UIAlertAction(title: reloadButtonTitle, style: .default) { _ in
101
+ DispatchQueue.main.async {
102
+ bridge.webView?.reload()
103
+ }
104
+ })
105
+
106
+ DispatchQueue.main.async {
107
+ if let topVC = UIApplication.topViewController() {
108
+ topVC.present(alertShake, animated: true)
109
+ }
110
+ }
111
+ }
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "7.4.0",
3
+ "version": "7.6.0",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",