@capgo/cordova-updater 8.1.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/LICENSE +373 -0
- package/README.md +82 -0
- package/dist/esm/definitions.d.ts +2684 -0
- package/dist/esm/definitions.js +109 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/exec.d.ts +4 -0
- package/dist/esm/exec.js +25 -0
- package/dist/esm/exec.js.map +1 -0
- package/dist/esm/history.d.ts +1 -0
- package/dist/esm/history.js +283 -0
- package/dist/esm/history.js.map +1 -0
- package/dist/esm/index.d.ts +57 -0
- package/dist/esm/index.js +68 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/plugin.d.ts +57 -0
- package/dist/esm/plugin.js +15 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/plugin.cjs.js +217 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +222 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +96 -0
- package/plugin.xml +207 -0
- package/src/android/CordovaUpdaterPlugin.java +5662 -0
- package/src/android/UpdaterPathHandler.java +99 -0
- package/src/android/app/capgo/cordova/updater/AndroidAppExitReporter.java +92 -0
- package/src/android/app/capgo/cordova/updater/AppLifecycleObserver.java +115 -0
- package/src/android/app/capgo/cordova/updater/BundleInfo.java +246 -0
- package/src/android/app/capgo/cordova/updater/BundleStatus.java +45 -0
- package/src/android/app/capgo/cordova/updater/Callback.java +13 -0
- package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +3404 -0
- package/src/android/app/capgo/cordova/updater/CordovaUpdaterConfig.java +56 -0
- package/src/android/app/capgo/cordova/updater/CryptoCipher.java +462 -0
- package/src/android/app/capgo/cordova/updater/DataManager.java +44 -0
- package/src/android/app/capgo/cordova/updater/DelayCondition.java +56 -0
- package/src/android/app/capgo/cordova/updater/DelayUntilNext.java +14 -0
- package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +307 -0
- package/src/android/app/capgo/cordova/updater/DeviceIdHelper.java +230 -0
- package/src/android/app/capgo/cordova/updater/DownloadService.java +1219 -0
- package/src/android/app/capgo/cordova/updater/DownloadWorkerManager.java +260 -0
- package/src/android/app/capgo/cordova/updater/InternalUtils.java +54 -0
- package/src/android/app/capgo/cordova/updater/Logger.java +345 -0
- package/src/android/app/capgo/cordova/updater/ShakeDetector.java +72 -0
- package/src/android/app/capgo/cordova/updater/ShakeMenu.java +39 -0
- package/src/android/app/capgo/cordova/updater/ThreeFingerPinchDetector.java +323 -0
- package/src/android/app/capgo/cordova/updater/compat/JSArray.java +9 -0
- package/src/android/app/capgo/cordova/updater/compat/JSObject.java +83 -0
- package/src/android/app/capgo/cordova/updater/compat/PluginCall.java +165 -0
- package/src/android/capgo-cordova-updater.gradle +19 -0
- package/src/android/capgo-updater.xml +3 -0
- package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +3039 -0
- package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +177 -0
- package/src/android/test/app/capgo/cordova/updater/NativeContractTest.java +105 -0
- package/src/ios/AES.swift +216 -0
- package/src/ios/AppHealthTracker.swift +82 -0
- package/src/ios/BundleInfo.swift +185 -0
- package/src/ios/BundleStatus.swift +127 -0
- package/src/ios/CapgoRawRsa.swift +36 -0
- package/src/ios/CapgoUpdater.swift +3544 -0
- package/src/ios/CordovaCompat.swift +11 -0
- package/src/ios/CordovaPluginCall.swift +190 -0
- package/src/ios/CordovaUpdaterCommands.swift +270 -0
- package/src/ios/CordovaUpdaterPlugin.swift +4706 -0
- package/src/ios/CryptoCipher.swift +326 -0
- package/src/ios/DelayCondition.swift +74 -0
- package/src/ios/DelayUntilNext.swift +30 -0
- package/src/ios/DelayUpdateUtils.swift +257 -0
- package/src/ios/DeviceIdHelper.swift +120 -0
- package/src/ios/Info.plist +28 -0
- package/src/ios/InternalUtils.swift +471 -0
- package/src/ios/Logger.swift +283 -0
- package/src/ios/RSA.swift +96 -0
- package/src/ios/ShakeMenu.swift +52 -0
- package/src/ios/UpdaterSchemeHandler.swift +81 -0
- package/src/ios/UserDefaultsExtension.swift +46 -0
- package/src/ios/WebViewStatsReporter.swift +303 -0
|
@@ -0,0 +1,120 @@
|
|
|
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 Foundation
|
|
8
|
+
import Security
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper class to manage device ID persistence across app installations.
|
|
12
|
+
* Uses iOS Keychain to persist the device ID.
|
|
13
|
+
*/
|
|
14
|
+
class DeviceIdHelper {
|
|
15
|
+
private static let keychainService = "app.capgo.updater"
|
|
16
|
+
private static let keychainAccount = "deviceId"
|
|
17
|
+
private static let legacyDefaultsKey = "appUUID"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Gets or creates a device ID that persists across reinstalls.
|
|
21
|
+
*
|
|
22
|
+
* This method:
|
|
23
|
+
* 1. First checks for an existing ID in Keychain (persists across reinstalls)
|
|
24
|
+
* 2. Falls back to UserDefaults (for migration from older versions)
|
|
25
|
+
* 3. Generates a new UUID if neither exists
|
|
26
|
+
* 4. Stores the ID in Keychain for future use
|
|
27
|
+
*
|
|
28
|
+
* @return Device ID as a lowercase UUID string
|
|
29
|
+
*/
|
|
30
|
+
static func getOrCreateDeviceId() -> String {
|
|
31
|
+
// Try to get device ID from Keychain first
|
|
32
|
+
if let keychainDeviceId = getDeviceIdFromKeychain() {
|
|
33
|
+
return keychainDeviceId.lowercased()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Migration: Check UserDefaults for existing device ID
|
|
37
|
+
var deviceId = UserDefaults.standard.string(forKey: legacyDefaultsKey)
|
|
38
|
+
|
|
39
|
+
if deviceId == nil || deviceId!.isEmpty {
|
|
40
|
+
// Generate new device ID if none exists
|
|
41
|
+
deviceId = UUID().uuidString
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Ensure lowercase for consistency
|
|
45
|
+
deviceId = deviceId!.lowercased()
|
|
46
|
+
|
|
47
|
+
// Save to Keychain for persistence across reinstalls
|
|
48
|
+
saveDeviceIdToKeychain(deviceId: deviceId!)
|
|
49
|
+
|
|
50
|
+
return deviceId!
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Retrieves the device ID from iOS Keychain.
|
|
55
|
+
*
|
|
56
|
+
* @return Device ID string or nil if not found
|
|
57
|
+
*/
|
|
58
|
+
private static func getDeviceIdFromKeychain() -> String? {
|
|
59
|
+
let query: [String: Any] = [
|
|
60
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
61
|
+
kSecAttrService as String: keychainService,
|
|
62
|
+
kSecAttrAccount as String: keychainAccount,
|
|
63
|
+
kSecReturnData as String: true,
|
|
64
|
+
kSecMatchLimit as String: kSecMatchLimitOne
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
var result: AnyObject?
|
|
68
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
69
|
+
|
|
70
|
+
guard status == errSecSuccess,
|
|
71
|
+
let data = result as? Data,
|
|
72
|
+
let deviceId = String(data: data, encoding: .utf8) else {
|
|
73
|
+
return nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return deviceId
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Saves the device ID to iOS Keychain with appropriate accessibility settings.
|
|
81
|
+
*
|
|
82
|
+
* Uses kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:
|
|
83
|
+
* - Data persists across reinstalls
|
|
84
|
+
* - Data is NOT synced to iCloud
|
|
85
|
+
* - Data is accessible after first device unlock
|
|
86
|
+
* - Data stays on this device only (privacy-friendly)
|
|
87
|
+
*
|
|
88
|
+
* @param deviceId The device ID to save
|
|
89
|
+
*/
|
|
90
|
+
private static func saveDeviceIdToKeychain(deviceId: String) {
|
|
91
|
+
guard let data = deviceId.data(using: .utf8) else {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Delete any existing entry first
|
|
96
|
+
let deleteQuery: [String: Any] = [
|
|
97
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
98
|
+
kSecAttrService as String: keychainService,
|
|
99
|
+
kSecAttrAccount as String: keychainAccount
|
|
100
|
+
]
|
|
101
|
+
SecItemDelete(deleteQuery as CFDictionary)
|
|
102
|
+
|
|
103
|
+
// Add new entry with appropriate accessibility
|
|
104
|
+
let addQuery: [String: Any] = [
|
|
105
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
106
|
+
kSecAttrService as String: keychainService,
|
|
107
|
+
kSecAttrAccount as String: keychainAccount,
|
|
108
|
+
kSecValueData as String: data,
|
|
109
|
+
// This ensures data persists across reinstalls but stays on device (not synced)
|
|
110
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
let status = SecItemAdd(addQuery as CFDictionary, nil)
|
|
114
|
+
|
|
115
|
+
if status != errSecSuccess {
|
|
116
|
+
// Log error but don't crash - we'll fall back to UserDefaults on next launch
|
|
117
|
+
print("Failed to save device ID to Keychain: \(status)")
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
+
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
7
|
+
<key>CFBundleExecutable</key>
|
|
8
|
+
<string>$(EXECUTABLE_NAME)</string>
|
|
9
|
+
<key>CFBundleIdentifier</key>
|
|
10
|
+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
11
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
12
|
+
<string>6.0</string>
|
|
13
|
+
<key>CFBundleName</key>
|
|
14
|
+
<string>$(PRODUCT_NAME)</string>
|
|
15
|
+
<key>CFBundlePackageType</key>
|
|
16
|
+
<string>FMWK</string>
|
|
17
|
+
<key>CFBundleShortVersionString</key>
|
|
18
|
+
<string>1.0</string>
|
|
19
|
+
<key>CFBundleVersion</key>
|
|
20
|
+
<string>$(CURRENT_PROJECT_VERSION)</string>
|
|
21
|
+
<key>NSPrincipalClass</key>
|
|
22
|
+
<string></string>
|
|
23
|
+
<key>UIFileSharingEnabled</key>
|
|
24
|
+
<true/>
|
|
25
|
+
<key>LSSupportsOpeningDocumentsInPlace</key>
|
|
26
|
+
<true/>
|
|
27
|
+
</dict>
|
|
28
|
+
</plist>
|
|
@@ -0,0 +1,471 @@
|
|
|
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 Foundation
|
|
8
|
+
|
|
9
|
+
extension Collection {
|
|
10
|
+
subscript(safe index: Index) -> Element? {
|
|
11
|
+
return indices.contains(index) ? self[index] : nil
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
extension URL {
|
|
15
|
+
var isDirectory: Bool {
|
|
16
|
+
(try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
|
|
17
|
+
}
|
|
18
|
+
var exist: Bool {
|
|
19
|
+
return FileManager().fileExists(atPath: self.path)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
struct SetChannelDec: Decodable {
|
|
23
|
+
let status: String?
|
|
24
|
+
let error: String?
|
|
25
|
+
let message: String?
|
|
26
|
+
let unset: Bool?
|
|
27
|
+
}
|
|
28
|
+
public class SetChannel: NSObject {
|
|
29
|
+
var status: String = ""
|
|
30
|
+
var error: String = ""
|
|
31
|
+
var message: String = ""
|
|
32
|
+
}
|
|
33
|
+
extension SetChannel {
|
|
34
|
+
func toDict() -> [String: Any] {
|
|
35
|
+
var dict: [String: Any] = [String: Any]()
|
|
36
|
+
let otherSelf: Mirror = Mirror(reflecting: self)
|
|
37
|
+
for child: Mirror.Child in otherSelf.children {
|
|
38
|
+
if let key: String = child.label {
|
|
39
|
+
dict[key] = child.value
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return dict
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
struct GetChannelDec: Decodable {
|
|
46
|
+
let channel: String?
|
|
47
|
+
let status: String?
|
|
48
|
+
let error: String?
|
|
49
|
+
let message: String?
|
|
50
|
+
let allowSet: Bool?
|
|
51
|
+
}
|
|
52
|
+
public class GetChannel: NSObject {
|
|
53
|
+
var channel: String = ""
|
|
54
|
+
var status: String = ""
|
|
55
|
+
var error: String = ""
|
|
56
|
+
var message: String = ""
|
|
57
|
+
var allowSet: Bool = true
|
|
58
|
+
}
|
|
59
|
+
extension GetChannel {
|
|
60
|
+
func toDict() -> [String: Any] {
|
|
61
|
+
var dict: [String: Any] = [String: Any]()
|
|
62
|
+
let otherSelf: Mirror = Mirror(reflecting: self)
|
|
63
|
+
for child: Mirror.Child in otherSelf.children {
|
|
64
|
+
if let key: String = child.label {
|
|
65
|
+
dict[key] = child.value
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return dict
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// swiftlint:disable identifier_name
|
|
72
|
+
struct ChannelInfo: Codable {
|
|
73
|
+
let id: Int
|
|
74
|
+
let name: String?
|
|
75
|
+
let `public`: Bool?
|
|
76
|
+
let allow_self_set: Bool?
|
|
77
|
+
}
|
|
78
|
+
// swiftlint:enable identifier_name
|
|
79
|
+
struct ListChannelsDec: Decodable {
|
|
80
|
+
let channels: [ChannelInfo]?
|
|
81
|
+
let error: String?
|
|
82
|
+
|
|
83
|
+
init(from decoder: Decoder) throws {
|
|
84
|
+
let container = try decoder.singleValueContainer()
|
|
85
|
+
|
|
86
|
+
if let channelsArray = try? container.decode([ChannelInfo].self) {
|
|
87
|
+
// Backend returns direct array
|
|
88
|
+
self.channels = channelsArray
|
|
89
|
+
self.error = nil
|
|
90
|
+
} else {
|
|
91
|
+
// Handle error response
|
|
92
|
+
let errorContainer = try decoder.container(keyedBy: CodingKeys.self)
|
|
93
|
+
self.channels = nil
|
|
94
|
+
self.error = try? errorContainer.decode(String.self, forKey: .error)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private enum CodingKeys: String, CodingKey {
|
|
99
|
+
case error
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
public class ListChannels: NSObject {
|
|
103
|
+
var channels: [[String: Any]] = []
|
|
104
|
+
var error: String = ""
|
|
105
|
+
}
|
|
106
|
+
extension ListChannels {
|
|
107
|
+
func toDict() -> [String: Any] {
|
|
108
|
+
var dict: [String: Any] = [String: Any]()
|
|
109
|
+
let otherSelf: Mirror = Mirror(reflecting: self)
|
|
110
|
+
for child: Mirror.Child in otherSelf.children {
|
|
111
|
+
if let key: String = child.label {
|
|
112
|
+
dict[key] = child.value
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return dict
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// swiftlint:disable identifier_name
|
|
119
|
+
struct InfoObject: Codable {
|
|
120
|
+
let platform: String?
|
|
121
|
+
let device_id: String?
|
|
122
|
+
let app_id: String?
|
|
123
|
+
let custom_id: String?
|
|
124
|
+
let version_build: String?
|
|
125
|
+
let version_code: String?
|
|
126
|
+
let version_os: String?
|
|
127
|
+
var version_name: String?
|
|
128
|
+
var old_version_name: String?
|
|
129
|
+
let plugin_version: String?
|
|
130
|
+
let is_emulator: Bool?
|
|
131
|
+
let is_prod: Bool?
|
|
132
|
+
let installSource: String?
|
|
133
|
+
var action: String?
|
|
134
|
+
var channel: String?
|
|
135
|
+
var defaultChannel: String?
|
|
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
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// swiftlint:enable identifier_name
|
|
189
|
+
|
|
190
|
+
// swiftlint:disable identifier_name
|
|
191
|
+
struct StatsEvent: Codable {
|
|
192
|
+
let platform: String?
|
|
193
|
+
let device_id: String?
|
|
194
|
+
let app_id: String?
|
|
195
|
+
let custom_id: String?
|
|
196
|
+
let version_build: String?
|
|
197
|
+
let version_code: String?
|
|
198
|
+
let version_os: String?
|
|
199
|
+
let version_name: String?
|
|
200
|
+
let old_version_name: String?
|
|
201
|
+
let plugin_version: String?
|
|
202
|
+
let is_emulator: Bool?
|
|
203
|
+
let is_prod: Bool?
|
|
204
|
+
let installSource: String?
|
|
205
|
+
let action: String?
|
|
206
|
+
let channel: String?
|
|
207
|
+
let defaultChannel: String?
|
|
208
|
+
let key_id: String?
|
|
209
|
+
let metadata: [String: String]?
|
|
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
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// swiftlint:enable identifier_name
|
|
235
|
+
|
|
236
|
+
// swiftlint:disable identifier_name
|
|
237
|
+
public struct ManifestEntry: Codable {
|
|
238
|
+
let file_name: String?
|
|
239
|
+
let file_hash: String?
|
|
240
|
+
let download_url: String?
|
|
241
|
+
}
|
|
242
|
+
// swiftlint:enable identifier_name
|
|
243
|
+
|
|
244
|
+
extension ManifestEntry {
|
|
245
|
+
func toDict() -> [String: Any] {
|
|
246
|
+
var dict: [String: Any] = [String: Any]()
|
|
247
|
+
let mirror = Mirror(reflecting: self)
|
|
248
|
+
for child in mirror.children {
|
|
249
|
+
if let key = child.label {
|
|
250
|
+
dict[key] = child.value
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return dict
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// swiftlint:disable identifier_name
|
|
258
|
+
struct AppVersionDec: Decodable {
|
|
259
|
+
let version: String?
|
|
260
|
+
let checksum: String?
|
|
261
|
+
let url: String?
|
|
262
|
+
let message: String?
|
|
263
|
+
let error: String?
|
|
264
|
+
let kind: String?
|
|
265
|
+
let session_key: String?
|
|
266
|
+
let major: Bool?
|
|
267
|
+
let breaking: Bool?
|
|
268
|
+
let data: [String: String]?
|
|
269
|
+
let manifest: [ManifestEntry]?
|
|
270
|
+
let link: String?
|
|
271
|
+
let comment: String?
|
|
272
|
+
// The HTTP status code is captured separately in CapgoUpdater; this struct only mirrors JSON.
|
|
273
|
+
}
|
|
274
|
+
// swiftlint:enable identifier_name
|
|
275
|
+
|
|
276
|
+
public class AppVersion: NSObject {
|
|
277
|
+
var version: String = ""
|
|
278
|
+
var checksum: String = ""
|
|
279
|
+
var url: String = ""
|
|
280
|
+
var message: String?
|
|
281
|
+
var error: String?
|
|
282
|
+
var kind: String?
|
|
283
|
+
var sessionKey: String?
|
|
284
|
+
var major: Bool?
|
|
285
|
+
var breaking: Bool?
|
|
286
|
+
var data: [String: String]?
|
|
287
|
+
var manifest: [ManifestEntry]?
|
|
288
|
+
var missing: [String: Any]?
|
|
289
|
+
var downloadSize: [String: Any]?
|
|
290
|
+
var link: String?
|
|
291
|
+
var comment: String?
|
|
292
|
+
var statusCode: Int = 0
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
extension AppVersion {
|
|
296
|
+
func toDict() -> [String: Any] {
|
|
297
|
+
var dict: [String: Any] = [String: Any]()
|
|
298
|
+
let otherSelf: Mirror = Mirror(reflecting: self)
|
|
299
|
+
for child: Mirror.Child in otherSelf.children {
|
|
300
|
+
if let key: String = child.label {
|
|
301
|
+
if key == "manifest", let manifestEntries = child.value as? [ManifestEntry] {
|
|
302
|
+
dict[key] = manifestEntries.map { $0.toDict() }
|
|
303
|
+
} else {
|
|
304
|
+
dict[key] = child.value
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return dict
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
extension OperatingSystemVersion {
|
|
313
|
+
func getFullVersion(separator: String = ".") -> String {
|
|
314
|
+
return "\(majorVersion)\(separator)\(minorVersion)\(separator)\(patchVersion)"
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
extension Bundle {
|
|
318
|
+
var versionName: String? {
|
|
319
|
+
return infoDictionary?["CFBundleShortVersionString"] as? String
|
|
320
|
+
}
|
|
321
|
+
var versionCode: String? {
|
|
322
|
+
return infoDictionary?["CFBundleVersion"] as? String
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
extension ISO8601DateFormatter {
|
|
327
|
+
convenience init(_ formatOptions: Options) {
|
|
328
|
+
self.init()
|
|
329
|
+
self.formatOptions = formatOptions
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
extension Formatter {
|
|
333
|
+
static let iso8601withFractionalSeconds: ISO8601DateFormatter = ISO8601DateFormatter(
|
|
334
|
+
[.withInternetDateTime, .withFractionalSeconds])
|
|
335
|
+
}
|
|
336
|
+
extension Date {
|
|
337
|
+
var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }
|
|
338
|
+
}
|
|
339
|
+
extension String {
|
|
340
|
+
|
|
341
|
+
var fileURL: URL {
|
|
342
|
+
return URL(fileURLWithPath: self)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
var lastPathComponent: String {
|
|
346
|
+
get {
|
|
347
|
+
return fileURL.lastPathComponent
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
var iso8601withFractionalSeconds: Date? {
|
|
351
|
+
return Formatter.iso8601withFractionalSeconds.date(from: self)
|
|
352
|
+
}
|
|
353
|
+
func trim(using characterSet: CharacterSet = .whitespacesAndNewlines) -> String {
|
|
354
|
+
return trimmingCharacters(in: characterSet)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
enum CustomError: Error {
|
|
359
|
+
// Throw when an unzip fail
|
|
360
|
+
case cannotUnzip
|
|
361
|
+
case cannotWrite
|
|
362
|
+
case cannotDecode
|
|
363
|
+
case cannotUnflat
|
|
364
|
+
case cannotCreateDirectory
|
|
365
|
+
case cannotDeleteDirectory
|
|
366
|
+
case cannotDecryptSessionKey
|
|
367
|
+
case invalidBase64
|
|
368
|
+
case insufficientDiskSpace
|
|
369
|
+
|
|
370
|
+
// Throw in all other cases
|
|
371
|
+
case unexpected(code: Int)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
extension CustomError: LocalizedError {
|
|
375
|
+
public var errorDescription: String? {
|
|
376
|
+
switch self {
|
|
377
|
+
case .cannotUnzip:
|
|
378
|
+
return NSLocalizedString(
|
|
379
|
+
"The file cannot be unzip",
|
|
380
|
+
comment: "Invalid zip"
|
|
381
|
+
)
|
|
382
|
+
case .cannotCreateDirectory:
|
|
383
|
+
return NSLocalizedString(
|
|
384
|
+
"The folder cannot be created",
|
|
385
|
+
comment: "Invalid folder"
|
|
386
|
+
)
|
|
387
|
+
case .cannotDeleteDirectory:
|
|
388
|
+
return NSLocalizedString(
|
|
389
|
+
"The folder cannot be deleted",
|
|
390
|
+
comment: "Invalid folder"
|
|
391
|
+
)
|
|
392
|
+
case .cannotUnflat:
|
|
393
|
+
return NSLocalizedString(
|
|
394
|
+
"The file cannot be unflat",
|
|
395
|
+
comment: "Invalid folder"
|
|
396
|
+
)
|
|
397
|
+
case .unexpected:
|
|
398
|
+
return NSLocalizedString(
|
|
399
|
+
"An unexpected error occurred.",
|
|
400
|
+
comment: "Unexpected Error"
|
|
401
|
+
)
|
|
402
|
+
case .cannotDecode:
|
|
403
|
+
return NSLocalizedString(
|
|
404
|
+
"Decoding the zip failed with this key",
|
|
405
|
+
comment: "Invalid public key"
|
|
406
|
+
)
|
|
407
|
+
case .cannotWrite:
|
|
408
|
+
return NSLocalizedString(
|
|
409
|
+
"Cannot write to the destination",
|
|
410
|
+
comment: "Invalid destination"
|
|
411
|
+
)
|
|
412
|
+
case .cannotDecryptSessionKey:
|
|
413
|
+
return NSLocalizedString(
|
|
414
|
+
"Decrypting the session key failed",
|
|
415
|
+
comment: "Invalid session key"
|
|
416
|
+
)
|
|
417
|
+
case .invalidBase64:
|
|
418
|
+
return NSLocalizedString(
|
|
419
|
+
"Decrypting the base64 failed",
|
|
420
|
+
comment: "Invalid checksum key"
|
|
421
|
+
)
|
|
422
|
+
case .insufficientDiskSpace:
|
|
423
|
+
return NSLocalizedString(
|
|
424
|
+
"Insufficient disk space for download",
|
|
425
|
+
comment: "Not enough storage"
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/// Thread-safe atomic counter for concurrent operations
|
|
432
|
+
final class AtomicCounter {
|
|
433
|
+
private var value: Int = 0
|
|
434
|
+
private let lock = NSLock()
|
|
435
|
+
|
|
436
|
+
func increment() -> Int {
|
|
437
|
+
lock.lock()
|
|
438
|
+
defer { lock.unlock() }
|
|
439
|
+
value += 1
|
|
440
|
+
return value
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
var current: Int {
|
|
444
|
+
lock.lock()
|
|
445
|
+
defer { lock.unlock() }
|
|
446
|
+
return value
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/// Thread-safe atomic boolean for concurrent operations
|
|
451
|
+
final class AtomicBool {
|
|
452
|
+
private var _value: Bool
|
|
453
|
+
private let lock = NSLock()
|
|
454
|
+
|
|
455
|
+
init(initialValue: Bool = false) {
|
|
456
|
+
_value = initialValue
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
var value: Bool {
|
|
460
|
+
get {
|
|
461
|
+
lock.lock()
|
|
462
|
+
defer { lock.unlock() }
|
|
463
|
+
return _value
|
|
464
|
+
}
|
|
465
|
+
set {
|
|
466
|
+
lock.lock()
|
|
467
|
+
defer { lock.unlock() }
|
|
468
|
+
_value = newValue
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|