@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.
- package/README.md +115 -28
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +209 -11
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +134 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
- package/dist/docs.json +230 -2
- package/dist/esm/definitions.d.ts +91 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +4 -1
- package/dist/esm/web.js +14 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +14 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +14 -2
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +153 -11
- package/ios/Plugin/CapgoUpdater.swift +96 -39
- package/ios/Plugin/InternalUtils.swift +45 -0
- package/ios/Plugin/ShakeMenu.swift +112 -0
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ import Version
|
|
|
14
14
|
*/
|
|
15
15
|
@objc(CapacitorUpdaterPlugin)
|
|
16
16
|
public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
17
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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 \(
|
|
969
|
-
setChannel.
|
|
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
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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 \(
|
|
1006
|
-
setChannel.
|
|
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
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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 \(
|
|
1058
|
-
getChannel.
|
|
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
|
+
}
|