@capgo/capacitor-updater 6.14.26 → 6.14.33

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.
Files changed (56) hide show
  1. package/CapgoCapacitorUpdater.podspec +3 -2
  2. package/Package.swift +2 -2
  3. package/README.md +350 -74
  4. package/android/build.gradle +20 -8
  5. package/android/proguard-rules.pro +22 -5
  6. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +52 -16
  7. package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +2 -2
  8. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1202 -510
  9. package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java} +566 -154
  10. package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipher.java → CryptoCipherV1.java} +17 -9
  11. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +15 -26
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +0 -3
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
  14. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +221 -0
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +300 -119
  16. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +63 -25
  17. package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
  19. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
  20. package/dist/docs.json +652 -63
  21. package/dist/esm/definitions.d.ts +274 -15
  22. package/dist/esm/definitions.js.map +1 -1
  23. package/dist/esm/history.d.ts +1 -0
  24. package/dist/esm/history.js +283 -0
  25. package/dist/esm/history.js.map +1 -0
  26. package/dist/esm/index.d.ts +1 -0
  27. package/dist/esm/index.js +1 -0
  28. package/dist/esm/index.js.map +1 -1
  29. package/dist/esm/web.d.ts +12 -1
  30. package/dist/esm/web.js +29 -2
  31. package/dist/esm/web.js.map +1 -1
  32. package/dist/plugin.cjs.js +311 -2
  33. package/dist/plugin.cjs.js.map +1 -1
  34. package/dist/plugin.js +311 -2
  35. package/dist/plugin.js.map +1 -1
  36. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/AES.swift +6 -3
  37. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1578 -0
  38. package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift} +408 -139
  39. package/ios/{Plugin/CryptoCipher.swift → Sources/CapacitorUpdaterPlugin/CryptoCipherV1.swift} +13 -6
  40. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/CryptoCipherV2.swift +33 -27
  41. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
  42. package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
  43. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/InternalUtils.swift +47 -0
  44. package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
  45. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/RSA.swift +1 -0
  46. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
  47. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
  48. package/package.json +20 -16
  49. package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -1030
  50. /package/{LICENCE → LICENSE} +0 -0
  51. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BigInt.swift +0 -0
  52. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +0 -0
  53. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +0 -0
  54. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
  55. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
  56. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
@@ -161,7 +161,14 @@ fileprivate extension SecKey {
161
161
  }
162
162
  }
163
163
 
164
- public struct CryptoCipher {
164
+ // V1 Encryption - uses privateKey (deprecated but kept for backwards compatibility)
165
+ public struct CryptoCipherV1 {
166
+ private static var logger: Logger?
167
+
168
+ public static func setLogger(_ newLogger: Logger) {
169
+ logger = newLogger
170
+ }
171
+
165
172
  public static func calcChecksum(filePath: URL) -> String {
166
173
  let bufferSize = 1024 * 1024 * 5 // 5 MB
167
174
  var checksum = uLong(0)
@@ -186,16 +193,16 @@ public struct CryptoCipher {
186
193
 
187
194
  return String(format: "%08X", checksum).lowercased()
188
195
  } catch {
189
- print("\(CapacitorUpdater.TAG) Cannot get checksum: \(filePath.path)", error)
196
+ print("\("[Capacitor-updater]") Cannot get checksum: \(filePath.path)", error)
190
197
  return ""
191
198
  }
192
199
  }
193
200
  public static func decryptFile(filePath: URL, privateKey: String, sessionKey: String, version: String) throws {
194
201
  if privateKey.isEmpty {
195
- print("\(CapacitorUpdater.TAG) Cannot found privateKey")
202
+ print("\("[Capacitor-updater]") Cannot found privateKey")
196
203
  return
197
204
  } else if sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
198
- print("\(CapacitorUpdater.TAG) Cannot found sessionKey")
205
+ print("\("[Capacitor-updater]") Cannot found sessionKey")
199
206
  return
200
207
  }
201
208
  do {
@@ -218,7 +225,7 @@ public struct CryptoCipher {
218
225
  throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
219
226
  }
220
227
 
221
- let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted)
228
+ let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted, logger: logger ?? Logger(withTag: "[Capacitor-updater]"))
222
229
 
223
230
  guard let encryptedData = try? Data(contentsOf: filePath) else {
224
231
  throw NSError(domain: "Failed to read encrypted data", code: 3, userInfo: nil)
@@ -231,7 +238,7 @@ public struct CryptoCipher {
231
238
  try decryptedData.write(to: filePath)
232
239
 
233
240
  } catch {
234
- print("\(CapacitorUpdater.TAG) Cannot decode: \(filePath.path)", error)
241
+ print("\("[Capacitor-updater]") Cannot decode: \(filePath.path)", error)
235
242
  throw CustomError.cannotDecode
236
243
  }
237
244
  }
@@ -8,37 +8,43 @@ import Foundation
8
8
  import CryptoKit
9
9
  import BigInt
10
10
 
11
+ // V2 Encryption - uses publicKey (modern encryption from main branch)
11
12
  public struct CryptoCipherV2 {
13
+ private static var logger: Logger!
12
14
 
13
- public static func decryptChecksum(checksum: String, publicKey: String, version: String) throws -> String {
15
+ public static func setLogger(_ logger: Logger) {
16
+ self.logger = logger
17
+ }
18
+
19
+ public static func decryptChecksum(checksum: String, publicKey: String) throws -> String {
14
20
  if publicKey.isEmpty {
15
- print("\(CapacitorUpdater.TAG) The public key is empty")
21
+ logger.info("No encryption set (public key) ignored")
16
22
  return checksum
17
23
  }
18
24
  do {
19
25
  guard let checksumBytes = Data(base64Encoded: checksum) else {
20
- print("\(CapacitorUpdater.TAG) Cannot decode checksum as base64: \(checksum)")
26
+ logger.error("Cannot decode checksum as base64: \(checksum)")
21
27
  throw CustomError.cannotDecode
22
28
  }
23
29
 
24
30
  if checksumBytes.isEmpty {
25
- print("\(CapacitorUpdater.TAG) Decoded checksum is empty")
31
+ logger.error("Decoded checksum is empty")
26
32
  throw CustomError.cannotDecode
27
33
  }
28
34
 
29
35
  guard let rsaPublicKey = RSAPublicKey.load(rsaPublicKey: publicKey) else {
30
- print("\(CapacitorUpdater.TAG) The public key is not a valid RSA Public key")
36
+ logger.error("The public key is not a valid RSA Public key")
31
37
  throw CustomError.cannotDecode
32
38
  }
33
39
 
34
40
  guard let decryptedChecksum = rsaPublicKey.decrypt(data: checksumBytes) else {
35
- print("\(CapacitorUpdater.TAG) decryptChecksum fail")
41
+ logger.error("decryptChecksum fail")
36
42
  throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
37
43
  }
38
44
 
39
45
  return decryptedChecksum.base64EncodedString()
40
46
  } catch {
41
- print("\(CapacitorUpdater.TAG) decryptChecksum fail: \(error.localizedDescription)")
47
+ logger.error("decryptChecksum fail: \(error.localizedDescription)")
42
48
  throw CustomError.cannotDecode
43
49
  }
44
50
  }
@@ -51,7 +57,7 @@ public struct CryptoCipherV2 {
51
57
  do {
52
58
  fileHandle = try FileHandle(forReadingFrom: filePath)
53
59
  } catch {
54
- print("\(CapacitorUpdater.TAG) Cannot open file for checksum: \(filePath.path)", error)
60
+ logger.error("Cannot open file for checksum: \(filePath.path) \(error)")
55
61
  return ""
56
62
  }
57
63
 
@@ -59,7 +65,7 @@ public struct CryptoCipherV2 {
59
65
  do {
60
66
  try fileHandle.close()
61
67
  } catch {
62
- print("\(CapacitorUpdater.TAG) Error closing file: \(error)")
68
+ logger.error("Error closing file: \(error)")
63
69
  }
64
70
  }
65
71
 
@@ -72,7 +78,7 @@ public struct CryptoCipherV2 {
72
78
  fileData = fileHandle.readData(ofLength: bufferSize)
73
79
  }
74
80
  } catch {
75
- print("\(CapacitorUpdater.TAG) Error reading file: \(error)")
81
+ logger.error("Error reading file: \(error)")
76
82
  return false
77
83
  }
78
84
 
@@ -87,25 +93,25 @@ public struct CryptoCipherV2 {
87
93
  let digest = sha256.finalize()
88
94
  return digest.compactMap { String(format: "%02x", $0) }.joined()
89
95
  } catch {
90
- print("\(CapacitorUpdater.TAG) Cannot get checksum: \(filePath.path)", error)
96
+ logger.error("Cannot get checksum: \(filePath.path) \(error)")
91
97
  return ""
92
98
  }
93
99
  }
94
100
 
95
101
  public static func decryptFile(filePath: URL, publicKey: String, sessionKey: String, version: String) throws {
96
102
  if publicKey.isEmpty || sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
97
- print("\(CapacitorUpdater.TAG) Cannot found public key or sessionKey")
103
+ logger.info("Encryption not set, no public key or seesion, ignored")
98
104
  return
99
105
  }
100
106
 
101
107
  if !publicKey.hasPrefix("-----BEGIN RSA PUBLIC KEY-----") {
102
- print("\(CapacitorUpdater.TAG) The public key is not a valid RSA Public key")
108
+ logger.error("The public key is not a valid RSA Public key")
103
109
  return
104
110
  }
105
111
 
106
112
  do {
107
113
  guard let rsaPublicKey = RSAPublicKey.load(rsaPublicKey: publicKey) else {
108
- print("\(CapacitorUpdater.TAG) The public key is not a valid RSA Public key")
114
+ logger.error("The public key is not a valid RSA Public key")
109
115
  throw CustomError.cannotDecode
110
116
  }
111
117
 
@@ -114,68 +120,68 @@ public struct CryptoCipherV2 {
114
120
  let encryptedKeyBase64 = sessionKeyComponents[1]
115
121
 
116
122
  guard let ivData = Data(base64Encoded: ivBase64) else {
117
- print("\(CapacitorUpdater.TAG) Cannot decode sessionKey IV", ivBase64)
123
+ logger.error("Cannot decode sessionKey IV \(ivBase64)")
118
124
  throw CustomError.cannotDecode
119
125
  }
120
126
 
121
127
  if ivData.count != 16 {
122
- print("\(CapacitorUpdater.TAG) IV data has invalid length: \(ivData.count), expected 16")
128
+ logger.error("IV data has invalid length: \(ivData.count), expected 16")
123
129
  throw CustomError.cannotDecode
124
130
  }
125
131
 
126
132
  guard let sessionKeyDataEncrypted = Data(base64Encoded: encryptedKeyBase64) else {
127
- print("\(CapacitorUpdater.TAG) Cannot decode sessionKey data", encryptedKeyBase64)
133
+ logger.error("Cannot decode sessionKey data \(encryptedKeyBase64)")
128
134
  throw NSError(domain: "Invalid session key data", code: 1, userInfo: nil)
129
135
  }
130
136
 
131
137
  guard let sessionKeyDataDecrypted = rsaPublicKey.decrypt(data: sessionKeyDataEncrypted) else {
132
- print("\(CapacitorUpdater.TAG) Failed to decrypt session key data")
138
+ logger.error("Failed to decrypt session key data")
133
139
  throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
134
140
  }
135
141
 
136
142
  if sessionKeyDataDecrypted.count != 16 {
137
- print("\(CapacitorUpdater.TAG) Decrypted session key has invalid length: \(sessionKeyDataDecrypted.count), expected 16")
143
+ logger.error("Decrypted session key has invalid length: \(sessionKeyDataDecrypted.count), expected 16")
138
144
  throw NSError(domain: "Invalid decrypted session key", code: 5, userInfo: nil)
139
145
  }
140
146
 
141
- let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted)
147
+ let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted, logger: logger)
142
148
 
143
149
  let encryptedData: Data
144
150
  do {
145
151
  encryptedData = try Data(contentsOf: filePath)
146
152
  } catch {
147
- print("\(CapacitorUpdater.TAG) Failed to read encrypted data: \(error)")
153
+ logger.error("Failed to read encrypted data: \(error)")
148
154
  throw NSError(domain: "Failed to read encrypted data", code: 3, userInfo: nil)
149
155
  }
150
156
 
151
157
  if encryptedData.isEmpty {
152
- print("\(CapacitorUpdater.TAG) Encrypted file data is empty")
158
+ logger.error("Encrypted file data is empty")
153
159
  throw NSError(domain: "Empty encrypted data", code: 6, userInfo: nil)
154
160
  }
155
161
 
156
162
  guard let decryptedData = aesPrivateKey.decrypt(data: encryptedData) else {
157
- print("\(CapacitorUpdater.TAG) Failed to decrypt data")
163
+ logger.error("Failed to decrypt data")
158
164
  throw NSError(domain: "Failed to decrypt data", code: 4, userInfo: nil)
159
165
  }
160
166
 
161
167
  if decryptedData.isEmpty {
162
- print("\(CapacitorUpdater.TAG) Decrypted data is empty")
168
+ logger.error("Decrypted data is empty")
163
169
  throw NSError(domain: "Empty decrypted data", code: 7, userInfo: nil)
164
170
  }
165
171
 
166
172
  do {
167
173
  try decryptedData.write(to: filePath, options: .atomic)
168
174
  if !FileManager.default.fileExists(atPath: filePath.path) {
169
- print("\(CapacitorUpdater.TAG) File was not created after write")
175
+ logger.error("File was not created after write")
170
176
  throw NSError(domain: "File write failed", code: 8, userInfo: nil)
171
177
  }
172
178
  } catch {
173
- print("\(CapacitorUpdater.TAG) Error writing decrypted file: \(error)")
179
+ logger.error("Error writing decrypted file: \(error)")
174
180
  throw error
175
181
  }
176
182
 
177
183
  } catch {
178
- print("\(CapacitorUpdater.TAG) decryptFile fail")
184
+ logger.error("decryptFile fail")
179
185
  throw CustomError.cannotDecode
180
186
  }
181
187
  }
@@ -0,0 +1,220 @@
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
+ //
8
+ // DelayUpdateUtils.swift
9
+ // Plugin
10
+ //
11
+ // Created by Auto-generated based on Android implementation
12
+ // Copyright © 2024 Capgo. All rights reserved.
13
+ //
14
+
15
+ import Foundation
16
+ import Version
17
+
18
+ public class DelayUpdateUtils {
19
+
20
+ static let DELAY_CONDITION_PREFERENCES = "DELAY_CONDITION_PREFERENCES_CAPGO"
21
+ static let BACKGROUND_TIMESTAMP_KEY = "BACKGROUND_TIMESTAMP_KEY_CAPGO"
22
+ private let logger: Logger
23
+
24
+ private let currentVersionNative: Version
25
+
26
+ public enum CancelDelaySource {
27
+ case killed
28
+ case background
29
+ case foreground
30
+
31
+ var description: String {
32
+ switch self {
33
+ case .killed: return "KILLED"
34
+ case .background: return "BACKGROUND"
35
+ case .foreground: return "FOREGROUND"
36
+ }
37
+ }
38
+ }
39
+
40
+ public init(currentVersionNative: Version, logger: Logger) {
41
+ self.currentVersionNative = currentVersionNative
42
+ self.logger = logger
43
+ }
44
+
45
+ public func checkCancelDelay(source: CancelDelaySource) {
46
+ let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
47
+ let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
48
+ let kind: String = obj.value(forKey: "kind") as! String
49
+ let value: String? = obj.value(forKey: "value") as? String
50
+ return DelayCondition(kind: kind, value: value)
51
+ }
52
+
53
+ var delayConditionListToKeep: [DelayCondition] = []
54
+ var index = 0
55
+
56
+ for condition in delayConditionList {
57
+ let kind = condition.getKind()
58
+ let value = condition.getValue()
59
+
60
+ switch kind {
61
+ case "background":
62
+ if source == .foreground {
63
+ let backgroundedAt = getBackgroundTimestamp()
64
+ let now = Int64(Date().timeIntervalSince1970 * 1000) // Convert to milliseconds
65
+ let delta = max(0, now - backgroundedAt)
66
+
67
+ var longValue: Int64 = 0
68
+ if let value = value, !value.isEmpty {
69
+ longValue = Int64(value) ?? 0
70
+ }
71
+
72
+ if delta > longValue {
73
+ logger.info("Background condition (value: \(value ?? "")) deleted at index \(index). Delta: \(delta), longValue: \(longValue)")
74
+ } else {
75
+ delayConditionListToKeep.append(condition)
76
+ logger.info("Background delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
77
+ }
78
+ } else {
79
+ delayConditionListToKeep.append(condition)
80
+ logger.info("Background delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
81
+ }
82
+
83
+ case "kill":
84
+ if source == .killed {
85
+ logger.info("Kill delay (value: \(value ?? "")) condition removed at index \(index) after app kill")
86
+ } else {
87
+ delayConditionListToKeep.append(condition)
88
+ logger.info("Kill delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
89
+ }
90
+
91
+ case "date":
92
+ if let value = value, !value.isEmpty {
93
+ do {
94
+ let dateFormatter = ISO8601DateFormatter()
95
+ dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
96
+
97
+ if let date = dateFormatter.date(from: value) {
98
+ if Date() > date {
99
+ logger.info("Date delay (value: \(value)) condition removed due to expired date at index \(index)")
100
+ } else {
101
+ delayConditionListToKeep.append(condition)
102
+ logger.info("Date delay (value: \(value)) condition kept at index \(index)")
103
+ }
104
+ } else {
105
+ logger.error("Date delay (value: \(value)) condition removed due to parsing issue at index \(index)")
106
+ }
107
+ } catch {
108
+ logger.error("Date delay (value: \(value)) condition removed due to parsing issue at index \(index): \(error)")
109
+ }
110
+ } else {
111
+ logger.error("Date delay (value: \(value ?? "")) condition removed due to empty value at index \(index)")
112
+ }
113
+
114
+ case "nativeVersion":
115
+ if let value = value, !value.isEmpty {
116
+ do {
117
+ let versionLimit = try Version(value)
118
+ if currentVersionNative >= versionLimit {
119
+ logger.info("Native version delay (value: \(value)) condition removed due to above limit at index \(index)")
120
+ } else {
121
+ delayConditionListToKeep.append(condition)
122
+ logger.info("Native version delay (value: \(value)) condition kept at index \(index)")
123
+ }
124
+ } catch {
125
+ logger.error("Native version delay (value: \(value)) condition removed due to parsing issue at index \(index): \(error)")
126
+ }
127
+ } else {
128
+ logger.error("Native version delay (value: \(value ?? "")) condition removed due to empty value at index \(index)")
129
+ }
130
+
131
+ default:
132
+ logger.error("Unknown delay condition kind: \(kind) at index \(index)")
133
+ }
134
+
135
+ index += 1
136
+ }
137
+
138
+ if !delayConditionListToKeep.isEmpty {
139
+ let json = toJson(object: delayConditionListToKeep.map { $0.toJSON() })
140
+ _ = setMultiDelay(delayConditions: json)
141
+ } else {
142
+ // Clear all delay conditions if none are left to keep
143
+ _ = cancelDelay(source: "checkCancelDelay")
144
+ }
145
+ }
146
+
147
+ public func setMultiDelay(delayConditions: String) -> Bool {
148
+ do {
149
+ UserDefaults.standard.set(delayConditions, forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
150
+ UserDefaults.standard.synchronize()
151
+ logger.info("Delay update saved")
152
+ return true
153
+ } catch {
154
+ logger.error("Failed to delay update, [Error calling 'setMultiDelay()']: \(error)")
155
+ return false
156
+ }
157
+ }
158
+
159
+ public func setBackgroundTimestamp(_ backgroundTimestamp: Int64) {
160
+ do {
161
+ UserDefaults.standard.set(backgroundTimestamp, forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
162
+ UserDefaults.standard.synchronize()
163
+ logger.info("Background timestamp saved")
164
+ } catch {
165
+ logger.error("Failed to save background timestamp, [Error calling 'setBackgroundTimestamp()']: \(error)")
166
+ }
167
+ }
168
+
169
+ public func unsetBackgroundTimestamp() {
170
+ do {
171
+ UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
172
+ UserDefaults.standard.synchronize()
173
+ logger.info("Background timestamp removed")
174
+ } catch {
175
+ logger.error("Failed to remove background timestamp, [Error calling 'unsetBackgroundTimestamp()']: \(error)")
176
+ }
177
+ }
178
+
179
+ private func getBackgroundTimestamp() -> Int64 {
180
+ do {
181
+ let timestamp = UserDefaults.standard.object(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY) as? Int64 ?? 0
182
+ return timestamp
183
+ } catch {
184
+ logger.error("Failed to get background timestamp, [Error calling 'getBackgroundTimestamp()']: \(error)")
185
+ return 0
186
+ }
187
+ }
188
+
189
+ public func cancelDelay(source: String) -> Bool {
190
+ do {
191
+ UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
192
+ UserDefaults.standard.synchronize()
193
+ logger.info("All delays canceled from \(source)")
194
+ return true
195
+ } catch {
196
+ logger.error("Failed to cancel update delay: \(error)")
197
+ return false
198
+ }
199
+ }
200
+
201
+ // MARK: - Helper methods
202
+
203
+ private func toJson(object: Any) -> String {
204
+ guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else {
205
+ return ""
206
+ }
207
+ return String(data: data, encoding: String.Encoding.utf8) ?? ""
208
+ }
209
+
210
+ private func fromJsonArr(json: String) -> [NSObject] {
211
+ guard let jsonData = json.data(using: .utf8) else {
212
+ return []
213
+ }
214
+ let object = try? JSONSerialization.jsonObject(
215
+ with: jsonData,
216
+ options: .mutableContainers
217
+ ) as? [NSObject]
218
+ return object ?? []
219
+ }
220
+ }
@@ -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
+ }
@@ -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?
@@ -112,6 +157,7 @@ struct AppVersionDec: Decodable {
112
157
  let error: String?
113
158
  let session_key: String?
114
159
  let major: Bool?
160
+ let breaking: Bool?
115
161
  let data: [String: String]?
116
162
  let manifest: [ManifestEntry]?
117
163
  }
@@ -124,6 +170,7 @@ public class AppVersion: NSObject {
124
170
  var error: String?
125
171
  var sessionKey: String?
126
172
  var major: Bool?
173
+ var breaking: Bool?
127
174
  var data: [String: String]?
128
175
  var manifest: [ManifestEntry]?
129
176
  }