@capgo/capacitor-updater 7.43.3 → 7.50.1
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 +5 -2
- package/README.md +643 -115
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/AndroidAppExitReporter.java +92 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +3121 -353
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +924 -290
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +49 -13
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +45 -30
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +75 -32
- 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 +401 -27
- package/android/src/main/java/ee/forgr/capacitor_updater/ThreeFingerPinchDetector.java +323 -0
- package/dist/docs.json +1590 -196
- package/dist/esm/definitions.d.ts +755 -66
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +11 -2
- package/dist/esm/web.js +59 -5
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +59 -5
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +59 -5
- 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 +2732 -417
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1191 -363
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +0 -1
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +37 -16
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +80 -1
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +438 -39
- package/ios/Sources/CapacitorUpdaterPlugin/WebViewStatsReporter.swift +276 -0
- package/package.json +24 -9
|
@@ -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
|
|
46
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
|
|
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
|
|
55
244
|
}
|
|
56
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
|
+
})
|
|
291
|
+
}
|
|
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 {
|
|
@@ -308,19 +690,36 @@ extension UIWindow {
|
|
|
308
690
|
}
|
|
309
691
|
|
|
310
692
|
let latest = updater.getLatest(url: updateUrl, channel: name)
|
|
693
|
+
let latestKind = latest.kind
|
|
694
|
+
|
|
695
|
+
let detail = [latest.message, latest.error, latestKind]
|
|
696
|
+
.compactMap { value in
|
|
697
|
+
guard let value, !value.isEmpty else { return nil }
|
|
698
|
+
return value
|
|
699
|
+
}
|
|
700
|
+
.first ?? "server did not provide a message"
|
|
311
701
|
|
|
312
702
|
// Handle update errors first (before "no new version" check)
|
|
313
|
-
if
|
|
703
|
+
if latestKind == "failed" || (latest.error?.isEmpty == false && latestKind != "up_to_date" && latestKind != "blocked") {
|
|
704
|
+
DispatchQueue.main.async {
|
|
705
|
+
progressAlert.dismiss(animated: true) {
|
|
706
|
+
self.showError(message: "Channel set to \(name). Update check failed: \(detail)", plugin: plugin)
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if latestKind == "blocked" {
|
|
314
713
|
DispatchQueue.main.async {
|
|
315
714
|
progressAlert.dismiss(animated: true) {
|
|
316
|
-
self.showError(message: "Channel set to \(name). Update check
|
|
715
|
+
self.showError(message: "Channel set to \(name). Update check blocked: \(detail)", plugin: plugin)
|
|
317
716
|
}
|
|
318
717
|
}
|
|
319
718
|
return
|
|
320
719
|
}
|
|
321
720
|
|
|
322
721
|
// Check if there's an actual update available
|
|
323
|
-
if
|
|
722
|
+
if latestKind == "up_to_date" || latest.url.isEmpty {
|
|
324
723
|
DispatchQueue.main.async {
|
|
325
724
|
progressAlert.dismiss(animated: true) {
|
|
326
725
|
self.showSuccess(message: "Channel set to \(name). Already on latest version.", plugin: plugin)
|