@capgo/capacitor-updater 7.45.10 → 7.50.2
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 +510 -92
- package/android/src/main/java/ee/forgr/capacitor_updater/AndroidAppExitReporter.java +92 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +3192 -777
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +798 -299
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +45 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +46 -28
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +2 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +359 -25
- package/android/src/main/java/ee/forgr/capacitor_updater/ThreeFingerPinchDetector.java +323 -0
- package/dist/docs.json +1283 -169
- package/dist/esm/definitions.d.ts +621 -44
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +11 -1
- package/dist/esm/web.js +59 -1
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +59 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +59 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AppHealthTracker.swift +82 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +0 -16
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +2 -2
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +78 -2
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +2116 -223
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +997 -332
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +78 -1
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +418 -36
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
- package/package.json +15 -3
|
@@ -70,7 +70,7 @@ extension GetChannel {
|
|
|
70
70
|
}
|
|
71
71
|
// swiftlint:disable identifier_name
|
|
72
72
|
struct ChannelInfo: Codable {
|
|
73
|
-
let id:
|
|
73
|
+
let id: Int
|
|
74
74
|
let name: String?
|
|
75
75
|
let `public`: Bool?
|
|
76
76
|
let allow_self_set: Bool?
|
|
@@ -129,10 +129,61 @@ struct InfoObject: Codable {
|
|
|
129
129
|
let plugin_version: String?
|
|
130
130
|
let is_emulator: Bool?
|
|
131
131
|
let is_prod: Bool?
|
|
132
|
+
let installSource: String?
|
|
132
133
|
var action: String?
|
|
133
134
|
var channel: String?
|
|
134
135
|
var defaultChannel: String?
|
|
135
136
|
var key_id: String?
|
|
137
|
+
|
|
138
|
+
enum CodingKeys: String, CodingKey {
|
|
139
|
+
case platform
|
|
140
|
+
case device_id
|
|
141
|
+
case app_id
|
|
142
|
+
case custom_id
|
|
143
|
+
case version_build
|
|
144
|
+
case version_code
|
|
145
|
+
case version_os
|
|
146
|
+
case version_name
|
|
147
|
+
case old_version_name
|
|
148
|
+
case plugin_version
|
|
149
|
+
case is_emulator
|
|
150
|
+
case is_prod
|
|
151
|
+
case installSource = "install_source"
|
|
152
|
+
case action
|
|
153
|
+
case channel
|
|
154
|
+
case defaultChannel
|
|
155
|
+
case key_id
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
extension InfoObject {
|
|
160
|
+
func toParameters() -> [String: Any] {
|
|
161
|
+
var parameters: [String: Any] = [:]
|
|
162
|
+
func set(_ key: String, _ value: Any?) {
|
|
163
|
+
guard let value = value else {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
parameters[key] = value
|
|
167
|
+
}
|
|
168
|
+
set("platform", platform)
|
|
169
|
+
set("device_id", device_id)
|
|
170
|
+
set("app_id", app_id)
|
|
171
|
+
set("custom_id", custom_id)
|
|
172
|
+
set("version_build", version_build)
|
|
173
|
+
set("version_code", version_code)
|
|
174
|
+
set("version_os", version_os)
|
|
175
|
+
set("version_name", version_name)
|
|
176
|
+
set("old_version_name", old_version_name)
|
|
177
|
+
set("plugin_version", plugin_version)
|
|
178
|
+
set("is_emulator", is_emulator)
|
|
179
|
+
set("is_prod", is_prod)
|
|
180
|
+
set("install_source", installSource)
|
|
181
|
+
set("action", action)
|
|
182
|
+
set("channel", channel)
|
|
183
|
+
set("defaultChannel", defaultChannel)
|
|
184
|
+
set("key_id", key_id)
|
|
185
|
+
return parameters
|
|
186
|
+
}
|
|
136
187
|
}
|
|
137
188
|
// swiftlint:enable identifier_name
|
|
138
189
|
|
|
@@ -150,11 +201,35 @@ struct StatsEvent: Codable {
|
|
|
150
201
|
let plugin_version: String?
|
|
151
202
|
let is_emulator: Bool?
|
|
152
203
|
let is_prod: Bool?
|
|
204
|
+
let installSource: String?
|
|
153
205
|
let action: String?
|
|
154
206
|
let channel: String?
|
|
155
207
|
let defaultChannel: String?
|
|
156
208
|
let key_id: String?
|
|
209
|
+
let metadata: [String: String]?
|
|
157
210
|
let timestamp: Int64
|
|
211
|
+
|
|
212
|
+
enum CodingKeys: String, CodingKey {
|
|
213
|
+
case platform
|
|
214
|
+
case device_id
|
|
215
|
+
case app_id
|
|
216
|
+
case custom_id
|
|
217
|
+
case version_build
|
|
218
|
+
case version_code
|
|
219
|
+
case version_os
|
|
220
|
+
case version_name
|
|
221
|
+
case old_version_name
|
|
222
|
+
case plugin_version
|
|
223
|
+
case is_emulator
|
|
224
|
+
case is_prod
|
|
225
|
+
case installSource = "install_source"
|
|
226
|
+
case action
|
|
227
|
+
case channel
|
|
228
|
+
case defaultChannel
|
|
229
|
+
case key_id
|
|
230
|
+
case metadata
|
|
231
|
+
case timestamp
|
|
232
|
+
}
|
|
158
233
|
}
|
|
159
234
|
// swiftlint:enable identifier_name
|
|
160
235
|
|
|
@@ -210,6 +285,8 @@ public class AppVersion: NSObject {
|
|
|
210
285
|
var breaking: Bool?
|
|
211
286
|
var data: [String: String]?
|
|
212
287
|
var manifest: [ManifestEntry]?
|
|
288
|
+
var missing: [String: Any]?
|
|
289
|
+
var downloadSize: [String: Any]?
|
|
213
290
|
var link: String?
|
|
214
291
|
var comment: String?
|
|
215
292
|
var statusCode: Int = 0
|
|
@@ -7,6 +7,90 @@
|
|
|
7
7
|
import UIKit
|
|
8
8
|
import Capacitor
|
|
9
9
|
|
|
10
|
+
private var lastShakeMenuShownAt: TimeInterval = 0
|
|
11
|
+
private let shakeMenuCooldownSeconds: TimeInterval = 1.2
|
|
12
|
+
private let threeFingerPinchScaleDelta: CGFloat = 0.12
|
|
13
|
+
|
|
14
|
+
final class ThreeFingerPinchGestureRecognizer: UIGestureRecognizer {
|
|
15
|
+
private var initialSpan: CGFloat = 0
|
|
16
|
+
private(set) var scale: CGFloat = 1
|
|
17
|
+
var onTrackingStarted: (() -> Void)?
|
|
18
|
+
|
|
19
|
+
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
20
|
+
super.touchesBegan(touches, with: event)
|
|
21
|
+
guard let view = self.view, let activeTouches = activeTouches(with: event), activeTouches.count <= 3 else {
|
|
22
|
+
self.state = .failed
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if activeTouches.count == 3 {
|
|
27
|
+
self.initialSpan = span(for: activeTouches, in: view)
|
|
28
|
+
self.scale = 1
|
|
29
|
+
if self.initialSpan > 0 {
|
|
30
|
+
self.onTrackingStarted?()
|
|
31
|
+
} else {
|
|
32
|
+
self.state = .failed
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
38
|
+
super.touchesMoved(touches, with: event)
|
|
39
|
+
guard let view = self.view,
|
|
40
|
+
let activeTouches = activeTouches(with: event),
|
|
41
|
+
activeTouches.count == 3,
|
|
42
|
+
self.initialSpan > 0 else {
|
|
43
|
+
self.state = .failed
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
self.scale = span(for: activeTouches, in: view) / self.initialSpan
|
|
48
|
+
if abs(self.scale - 1) >= threeFingerPinchScaleDelta {
|
|
49
|
+
self.state = .recognized
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
54
|
+
super.touchesEnded(touches, with: event)
|
|
55
|
+
if self.state == .possible {
|
|
56
|
+
self.state = .failed
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
|
61
|
+
super.touchesCancelled(touches, with: event)
|
|
62
|
+
self.state = .cancelled
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override func reset() {
|
|
66
|
+
super.reset()
|
|
67
|
+
self.initialSpan = 0
|
|
68
|
+
self.scale = 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private func activeTouches(with event: UIEvent) -> [UITouch]? {
|
|
72
|
+
event.touches(for: self)?.filter { touch in
|
|
73
|
+
touch.phase != .ended && touch.phase != .cancelled
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private func span(for touches: [UITouch], in view: UIView) -> CGFloat {
|
|
78
|
+
guard !touches.isEmpty else {
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let points = touches.map { $0.location(in: view) }
|
|
83
|
+
let center = points.reduce(CGPoint.zero) { result, point in
|
|
84
|
+
CGPoint(x: result.x + point.x, y: result.y + point.y)
|
|
85
|
+
}
|
|
86
|
+
let centerPoint = CGPoint(x: center.x / CGFloat(points.count), y: center.y / CGFloat(points.count))
|
|
87
|
+
let totalDistance = points.reduce(CGFloat(0)) { result, point in
|
|
88
|
+
result + hypot(point.x - centerPoint.x, point.y - centerPoint.y)
|
|
89
|
+
}
|
|
90
|
+
return totalDistance / CGFloat(points.count)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
10
94
|
extension UIApplication {
|
|
11
95
|
// swiftlint:disable:next line_length
|
|
12
96
|
public class func topViewController(_ base: UIViewController? = UIApplication.shared.windows.first?.rootViewController) -> UIViewController? {
|
|
@@ -23,37 +107,208 @@ extension UIApplication {
|
|
|
23
107
|
}
|
|
24
108
|
}
|
|
25
109
|
|
|
110
|
+
extension CapacitorUpdaterPlugin: UIGestureRecognizerDelegate {
|
|
111
|
+
func syncShakeMenuGestureRecognizer() {
|
|
112
|
+
DispatchQueue.main.async { [weak self] in
|
|
113
|
+
guard let self else { return }
|
|
114
|
+
|
|
115
|
+
let shouldInstall = self.shakeMenuGesture == Self.shakeMenuGestureThreeFingerPinch &&
|
|
116
|
+
(self.shakeMenuEnabled || self.shakeChannelSelectorEnabled)
|
|
117
|
+
|
|
118
|
+
guard shouldInstall, let targetView = self.bridge?.viewController?.view ?? self.bridge?.webView else {
|
|
119
|
+
self.removeShakeMenuGestureRecognizer()
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if self.shakeMenuPinchGestureRecognizer?.view === targetView {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
self.removeShakeMenuGestureRecognizer()
|
|
128
|
+
|
|
129
|
+
let recognizer = ThreeFingerPinchGestureRecognizer(target: self, action: #selector(self.handleShakeMenuPinch(_:)))
|
|
130
|
+
recognizer.cancelsTouchesInView = false
|
|
131
|
+
recognizer.delaysTouchesBegan = false
|
|
132
|
+
recognizer.delaysTouchesEnded = false
|
|
133
|
+
recognizer.delegate = self
|
|
134
|
+
recognizer.onTrackingStarted = { [weak self] in
|
|
135
|
+
self?.logger.info("Three finger pinch tracking started")
|
|
136
|
+
}
|
|
137
|
+
targetView.addGestureRecognizer(recognizer)
|
|
138
|
+
self.shakeMenuPinchGestureRecognizer = recognizer
|
|
139
|
+
self.logger.info("Three finger pinch menu gesture initialized")
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func removeShakeMenuGestureRecognizer() {
|
|
144
|
+
if let recognizer = self.shakeMenuPinchGestureRecognizer {
|
|
145
|
+
recognizer.view?.removeGestureRecognizer(recognizer)
|
|
146
|
+
self.shakeMenuPinchGestureRecognizer = nil
|
|
147
|
+
self.shakeMenuPinchGestureTriggered = false
|
|
148
|
+
self.logger.info("Three finger pinch menu gesture stopped")
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@objc func handleShakeMenuPinch(_ recognizer: ThreeFingerPinchGestureRecognizer) {
|
|
153
|
+
guard recognizer.state == .recognized, !self.shakeMenuPinchGestureTriggered else {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
guard abs(recognizer.scale - 1) >= threeFingerPinchScaleDelta else {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
guard self.shakeMenuGesture == Self.shakeMenuGestureThreeFingerPinch, let bridge = self.bridge else {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
guard let window = recognizer.view?.window ?? bridge.viewController?.view.window else {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
self.shakeMenuPinchGestureTriggered = true
|
|
167
|
+
self.logger.info("Three finger pinch detected")
|
|
168
|
+
_ = window.showCapacitorUpdaterMenu(plugin: self, bridge: bridge, gestureName: "Three finger pinch")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
172
|
+
if gestureRecognizer === self.shakeMenuPinchGestureRecognizer {
|
|
173
|
+
self.shakeMenuPinchGestureTriggered = false
|
|
174
|
+
}
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public func gestureRecognizer(
|
|
179
|
+
_: UIGestureRecognizer,
|
|
180
|
+
shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer
|
|
181
|
+
) -> Bool {
|
|
182
|
+
true
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
26
186
|
extension UIWindow {
|
|
27
187
|
override open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
|
|
28
188
|
if motion == .motionShake {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let plugin = bridge.plugin(withName: "
|
|
189
|
+
guard let bridgeViewController = rootViewController as? CAPBridgeViewController,
|
|
190
|
+
let bridge = bridgeViewController.bridge,
|
|
191
|
+
let plugin = bridge.plugin(withName: "CapacitorUpdater") as? CapacitorUpdaterPlugin else {
|
|
32
192
|
return
|
|
33
193
|
}
|
|
34
|
-
|
|
35
|
-
// Check if shake menu is enabled
|
|
36
|
-
if !plugin.shakeMenuEnabled {
|
|
194
|
+
guard plugin.shakeMenuGesture == CapacitorUpdaterPlugin.shakeMenuGestureShake else {
|
|
37
195
|
return
|
|
38
196
|
}
|
|
39
197
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
198
|
+
_ = showCapacitorUpdaterMenu(plugin: plugin, bridge: bridge, gestureName: "Shake")
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@discardableResult
|
|
203
|
+
fileprivate func showCapacitorUpdaterMenu(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol, gestureName: String) -> Bool {
|
|
204
|
+
let canShowPreviewMenu = plugin.shakeMenuEnabled && plugin.hasActivePreviewSession()
|
|
205
|
+
let canShowChannelSelector = plugin.shakeChannelSelectorEnabled
|
|
206
|
+
|
|
207
|
+
if !canShowPreviewMenu && !canShowChannelSelector {
|
|
208
|
+
if plugin.shakeMenuEnabled {
|
|
209
|
+
plugin.logger.info("\(gestureName) preview menu ignored because no preview session is active")
|
|
45
210
|
}
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let now = Date().timeIntervalSince1970
|
|
215
|
+
guard now - lastShakeMenuShownAt >= shakeMenuCooldownSeconds else {
|
|
216
|
+
plugin.logger.info("\(gestureName) menu ignored because cooldown is active")
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let didShow = canShowPreviewMenu
|
|
221
|
+
? showDefaultMenu(plugin: plugin, bridge: bridge)
|
|
222
|
+
: showChannelSelector(plugin: plugin, bridge: bridge)
|
|
223
|
+
|
|
224
|
+
if didShow {
|
|
225
|
+
lastShakeMenuShownAt = now
|
|
46
226
|
}
|
|
227
|
+
return didShow
|
|
47
228
|
}
|
|
48
229
|
|
|
49
|
-
|
|
230
|
+
@discardableResult
|
|
231
|
+
private func showDefaultMenu(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) -> Bool {
|
|
50
232
|
// Prevent multiple alerts from showing
|
|
51
|
-
|
|
52
|
-
|
|
233
|
+
guard let topVC = UIApplication.topViewController() else {
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
if topVC.isKind(of: UIAlertController.self) {
|
|
53
237
|
plugin.logger.info("UIAlertController is already presented")
|
|
54
|
-
return
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
guard plugin.hasActivePreviewSession() else {
|
|
242
|
+
plugin.logger.info("Shake preview menu ignored because no preview session is active")
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? "App"
|
|
247
|
+
let title = "Preview \(appName) Menu"
|
|
248
|
+
let message = "Reload, switch, or leave the current preview."
|
|
249
|
+
let okButtonTitle = "Leave test app"
|
|
250
|
+
let reloadButtonTitle = "Reload preview"
|
|
251
|
+
let cancelButtonTitle = "Close menu"
|
|
252
|
+
|
|
253
|
+
let alertShake = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
|
254
|
+
|
|
255
|
+
alertShake.addAction(UIAlertAction(title: reloadButtonTitle, style: .default) { _ in
|
|
256
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
257
|
+
if !plugin.reloadPreviewSessionFromShakeMenu() {
|
|
258
|
+
DispatchQueue.main.async {
|
|
259
|
+
self.showError(message: "Could not reload the test app.", plugin: plugin)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
if !plugin.previewMenuPreviews().isEmpty {
|
|
266
|
+
alertShake.addAction(UIAlertAction(title: "Switch preview", style: .default) { _ in
|
|
267
|
+
let showSelector = {
|
|
268
|
+
self.showPreviewSelector(plugin: plugin)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if let presenter = alertShake.presentingViewController {
|
|
272
|
+
presenter.dismiss(animated: true, completion: showSelector)
|
|
273
|
+
} else {
|
|
274
|
+
DispatchQueue.main.async(execute: showSelector)
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if plugin.shakeChannelSelectorEnabled {
|
|
280
|
+
alertShake.addAction(UIAlertAction(title: "Switch channel", style: .default) { _ in
|
|
281
|
+
let showSelector = {
|
|
282
|
+
_ = self.showChannelSelector(plugin: plugin, bridge: bridge)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if let presenter = alertShake.presentingViewController {
|
|
286
|
+
presenter.dismiss(animated: true, completion: showSelector)
|
|
287
|
+
} else {
|
|
288
|
+
DispatchQueue.main.async(execute: showSelector)
|
|
289
|
+
}
|
|
290
|
+
})
|
|
55
291
|
}
|
|
56
292
|
|
|
293
|
+
alertShake.addAction(UIAlertAction(title: okButtonTitle, style: .default) { _ in
|
|
294
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
295
|
+
if !plugin.leavePreviewSessionFromShakeMenu() {
|
|
296
|
+
DispatchQueue.main.async {
|
|
297
|
+
self.showError(message: "Could not leave the test app.", plugin: plugin)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
alertShake.addAction(UIAlertAction(title: cancelButtonTitle, style: .default))
|
|
304
|
+
|
|
305
|
+
DispatchQueue.main.async {
|
|
306
|
+
topVC.present(alertShake, animated: true)
|
|
307
|
+
}
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private func showConfiguredDefaultMenu(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) {
|
|
57
312
|
let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? "App"
|
|
58
313
|
let title = "Preview \(appName) Menu"
|
|
59
314
|
let message = "What would you like to do?"
|
|
@@ -116,14 +371,141 @@ extension UIWindow {
|
|
|
116
371
|
}
|
|
117
372
|
}
|
|
118
373
|
|
|
119
|
-
private func
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
374
|
+
private func previewLabel(_ preview: [String: Any]) -> String {
|
|
375
|
+
let bundle = preview["bundle"] as? [String: Any]
|
|
376
|
+
let name = preview["name"] as? String
|
|
377
|
+
let version = bundle?["version"] as? String
|
|
378
|
+
var label = [name, version, preview["id"] as? String]
|
|
379
|
+
.compactMap { value in
|
|
380
|
+
guard let value, !value.isEmpty else { return nil }
|
|
381
|
+
return value
|
|
382
|
+
}
|
|
383
|
+
.first ?? "Preview"
|
|
384
|
+
if preview["isActive"] as? Bool == true {
|
|
385
|
+
label += " (current)"
|
|
386
|
+
}
|
|
387
|
+
return label
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private func showPreviewSelector(plugin: CapacitorUpdaterPlugin) {
|
|
391
|
+
guard let topVC = UIApplication.topViewController() else {
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
if topVC.isKind(of: UIAlertController.self) {
|
|
123
395
|
plugin.logger.info("UIAlertController is already presented")
|
|
124
396
|
return
|
|
125
397
|
}
|
|
126
398
|
|
|
399
|
+
let previews = plugin.previewMenuPreviews()
|
|
400
|
+
guard !previews.isEmpty else {
|
|
401
|
+
self.showError(message: "No saved previews available on this device.", plugin: plugin)
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let alert = UIAlertController(title: "Select Preview", message: "Choose a local preview to open", preferredStyle: .actionSheet)
|
|
406
|
+
let previewsToShow = Array(previews.prefix(5))
|
|
407
|
+
for preview in previewsToShow {
|
|
408
|
+
let title = self.previewLabel(preview)
|
|
409
|
+
let id = preview["id"] as? String ?? ""
|
|
410
|
+
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
|
|
411
|
+
self?.selectPreview(id: id, plugin: plugin)
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if previews.count > 5 {
|
|
416
|
+
alert.addAction(UIAlertAction(title: "More previews...", style: .default) { [weak self] _ in
|
|
417
|
+
self?.showSearchablePreviewPicker(previews: previews, plugin: plugin)
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
422
|
+
|
|
423
|
+
if let popoverController = alert.popoverPresentationController {
|
|
424
|
+
popoverController.sourceView = self
|
|
425
|
+
popoverController.sourceRect = CGRect(x: self.bounds.midX, y: self.bounds.midY, width: 0, height: 0)
|
|
426
|
+
popoverController.permittedArrowDirections = []
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
topVC.present(alert, animated: true)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private func showSearchablePreviewPicker(previews: [[String: Any]], plugin: CapacitorUpdaterPlugin) {
|
|
433
|
+
let alert = UIAlertController(title: "Search Previews", message: "Enter preview name or version", preferredStyle: .alert)
|
|
434
|
+
|
|
435
|
+
alert.addTextField { textField in
|
|
436
|
+
textField.placeholder = "Preview name..."
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
alert.addAction(UIAlertAction(title: "Search", style: .default) { [weak self, weak alert] _ in
|
|
440
|
+
guard let self else { return }
|
|
441
|
+
guard let searchText = alert?.textFields?.first?.text?.lowercased(), !searchText.isEmpty else {
|
|
442
|
+
self.showPreviewSelector(plugin: plugin)
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let filtered = previews.filter { self.previewLabel($0).lowercased().contains(searchText) }
|
|
447
|
+
if filtered.isEmpty {
|
|
448
|
+
self.showError(message: "No previews found matching '\(searchText)'", plugin: plugin)
|
|
449
|
+
} else if filtered.count == 1, let id = filtered[0]["id"] as? String {
|
|
450
|
+
self.selectPreview(id: id, plugin: plugin)
|
|
451
|
+
} else {
|
|
452
|
+
self.presentPreviewPicker(previews: filtered, plugin: plugin)
|
|
453
|
+
}
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
457
|
+
|
|
458
|
+
DispatchQueue.main.async {
|
|
459
|
+
if let topVC = UIApplication.topViewController() {
|
|
460
|
+
topVC.present(alert, animated: true)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private func presentPreviewPicker(previews: [[String: Any]], plugin: CapacitorUpdaterPlugin) {
|
|
466
|
+
let alert = UIAlertController(title: "Select Preview", message: "Choose a local preview to open", preferredStyle: .actionSheet)
|
|
467
|
+
for preview in previews.prefix(5) {
|
|
468
|
+
let id = preview["id"] as? String ?? ""
|
|
469
|
+
alert.addAction(UIAlertAction(title: self.previewLabel(preview), style: .default) { [weak self] _ in
|
|
470
|
+
self?.selectPreview(id: id, plugin: plugin)
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
|
474
|
+
|
|
475
|
+
if let popoverController = alert.popoverPresentationController {
|
|
476
|
+
popoverController.sourceView = self
|
|
477
|
+
popoverController.sourceRect = CGRect(x: self.bounds.midX, y: self.bounds.midY, width: 0, height: 0)
|
|
478
|
+
popoverController.permittedArrowDirections = []
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
DispatchQueue.main.async {
|
|
482
|
+
if let topVC = UIApplication.topViewController() {
|
|
483
|
+
topVC.present(alert, animated: true)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private func selectPreview(id: String, plugin: CapacitorUpdaterPlugin) {
|
|
489
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
490
|
+
if !plugin.setPreviewFromShakeMenu(id: id) {
|
|
491
|
+
DispatchQueue.main.async {
|
|
492
|
+
self.showError(message: "Could not switch preview.", plugin: plugin)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
@discardableResult
|
|
499
|
+
private func showChannelSelector(plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) -> Bool {
|
|
500
|
+
// Prevent multiple alerts from showing
|
|
501
|
+
guard let topVC = UIApplication.topViewController() else {
|
|
502
|
+
return false
|
|
503
|
+
}
|
|
504
|
+
if topVC.isKind(of: UIAlertController.self) {
|
|
505
|
+
plugin.logger.info("UIAlertController is already presented")
|
|
506
|
+
return false
|
|
507
|
+
}
|
|
508
|
+
|
|
127
509
|
let updater = plugin.implementation
|
|
128
510
|
|
|
129
511
|
// Show loading indicator
|
|
@@ -145,28 +527,27 @@ extension UIWindow {
|
|
|
145
527
|
})
|
|
146
528
|
|
|
147
529
|
DispatchQueue.main.async {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
self.presentChannelPicker(channels: result.channels, plugin: plugin, bridge: bridge)
|
|
163
|
-
}
|
|
530
|
+
topVC.present(loadingAlert, animated: true) {
|
|
531
|
+
// Fetch channels in background
|
|
532
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
533
|
+
let result = updater.listChannels()
|
|
534
|
+
|
|
535
|
+
DispatchQueue.main.async {
|
|
536
|
+
loadingAlert.dismiss(animated: true) {
|
|
537
|
+
guard !didCancel else { return }
|
|
538
|
+
if !result.error.isEmpty {
|
|
539
|
+
self.showError(message: "Failed to load channels: \(result.error)", plugin: plugin)
|
|
540
|
+
} else if result.channels.isEmpty {
|
|
541
|
+
self.showError(message: "No channels available for self-assignment", plugin: plugin)
|
|
542
|
+
} else {
|
|
543
|
+
self.presentChannelPicker(channels: result.channels, plugin: plugin, bridge: bridge)
|
|
164
544
|
}
|
|
165
545
|
}
|
|
166
546
|
}
|
|
167
547
|
}
|
|
168
548
|
}
|
|
169
549
|
}
|
|
550
|
+
return true
|
|
170
551
|
}
|
|
171
552
|
|
|
172
553
|
private func presentChannelPicker(channels: [[String: Any]], plugin: CapacitorUpdaterPlugin, bridge: CAPBridgeProtocol) {
|
|
@@ -275,7 +656,8 @@ extension UIWindow {
|
|
|
275
656
|
let setResult = updater.setChannel(
|
|
276
657
|
channel: name,
|
|
277
658
|
defaultChannelKey: "CapacitorUpdater.defaultChannel",
|
|
278
|
-
allowSetDefaultChannel: plugin.allowSetDefaultChannel
|
|
659
|
+
allowSetDefaultChannel: plugin.allowSetDefaultChannel,
|
|
660
|
+
configDefaultChannel: plugin.getConfig().getString("defaultChannel", "") ?? ""
|
|
279
661
|
)
|
|
280
662
|
|
|
281
663
|
if !setResult.error.isEmpty {
|