@capgo/capacitor-updater 8.0.1 → 8.2.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.
Files changed (55) hide show
  1. package/CapgoCapacitorUpdater.podspec +7 -5
  2. package/Package.swift +9 -7
  3. package/README.md +984 -215
  4. package/android/build.gradle +24 -12
  5. package/android/proguard-rules.pro +22 -5
  6. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +110 -22
  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 +1316 -489
  9. package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java} +662 -203
  10. package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipherV2.java → CryptoCipher.java} +138 -33
  11. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +0 -3
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +221 -0
  14. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +497 -133
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +80 -25
  16. package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
  17. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
  19. package/dist/docs.json +873 -154
  20. package/dist/esm/definitions.d.ts +881 -114
  21. package/dist/esm/definitions.js.map +1 -1
  22. package/dist/esm/history.d.ts +1 -0
  23. package/dist/esm/history.js +283 -0
  24. package/dist/esm/history.js.map +1 -0
  25. package/dist/esm/index.d.ts +1 -0
  26. package/dist/esm/index.js +1 -0
  27. package/dist/esm/index.js.map +1 -1
  28. package/dist/esm/web.d.ts +12 -1
  29. package/dist/esm/web.js +29 -2
  30. package/dist/esm/web.js.map +1 -1
  31. package/dist/plugin.cjs.js +311 -2
  32. package/dist/plugin.cjs.js.map +1 -1
  33. package/dist/plugin.js +311 -2
  34. package/dist/plugin.js.map +1 -1
  35. package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +69 -0
  36. package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +55 -0
  37. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +37 -10
  38. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +1 -1
  39. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1610 -0
  40. package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift} +541 -231
  41. package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +286 -0
  42. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
  43. package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
  44. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/InternalUtils.swift +54 -0
  45. package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
  46. package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +274 -0
  47. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
  48. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
  49. package/package.json +21 -19
  50. package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -975
  51. package/ios/Plugin/CryptoCipherV2.swift +0 -310
  52. /package/{LICENCE → LICENSE} +0 -0
  53. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
  54. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
  55. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
@@ -14,7 +14,8 @@ import Alamofire
14
14
  import Compression
15
15
  import UIKit
16
16
 
17
- @objc public class CapacitorUpdater: NSObject {
17
+ @objc public class CapgoUpdater: NSObject {
18
+ private var logger: Logger!
18
19
 
19
20
  private let versionCode: String = Bundle.main.versionCode ?? ""
20
21
  private let versionOs = UIDevice.current.systemVersion
@@ -29,11 +30,10 @@ import UIKit
29
30
  // Add this line to declare cacheFolder
30
31
  private let cacheFolder: URL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("capgo_downloads")
31
32
 
32
- public static let TAG: String = "✨ Capacitor-updater:"
33
33
  public let CAP_SERVER_PATH: String = "serverBasePath"
34
34
  public var versionBuild: String = ""
35
35
  public var customId: String = ""
36
- public var PLUGIN_VERSION: String = ""
36
+ public var pluginVersion: String = ""
37
37
  public var timeout: Double = 20
38
38
  public var statsUrl: String = ""
39
39
  public var channelUrl: String = ""
@@ -42,12 +42,37 @@ import UIKit
42
42
  public var deviceID = ""
43
43
  public var publicKey: String = ""
44
44
 
45
- public var notifyDownloadRaw: (String, Int, Bool) -> Void = { _, _, _ in }
46
- public func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false) {
47
- notifyDownloadRaw(id, percent, ignoreMultipleOfTen)
45
+ // Cached key ID calculated once from publicKey
46
+ private var cachedKeyId: String?
47
+
48
+ // Flag to track if we received a 429 response - stops requests until app restart
49
+ private static var rateLimitExceeded = false
50
+
51
+ // Flag to track if we've already sent the rate limit statistic - prevents infinite loop
52
+ private static var rateLimitStatisticSent = false
53
+
54
+ private var userAgent: String {
55
+ let safePluginVersion = pluginVersion.isEmpty ? "unknown" : pluginVersion
56
+ let safeAppId = appId.isEmpty ? "unknown" : appId
57
+ return "CapacitorUpdater/\(safePluginVersion) (\(safeAppId)) ios/\(versionOs)"
58
+ }
59
+
60
+ private lazy var alamofireSession: Session = {
61
+ let configuration = URLSessionConfiguration.default
62
+ configuration.httpAdditionalHeaders = ["User-Agent": self.userAgent]
63
+ return Session(configuration: configuration)
64
+ }()
65
+
66
+ public var notifyDownloadRaw: (String, Int, Bool, BundleInfo?) -> Void = { _, _, _, _ in }
67
+ public func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false, bundle: BundleInfo? = nil) {
68
+ notifyDownloadRaw(id, percent, ignoreMultipleOfTen, bundle)
48
69
  }
49
70
  public var notifyDownload: (String, Int) -> Void = { _, _ in }
50
71
 
72
+ public func setLogger(_ logger: Logger) {
73
+ self.logger = logger
74
+ }
75
+
51
76
  private func calcTotalPercent(percent: Int, min: Int, max: Int) -> Int {
52
77
  return (percent * (max - min)) / 100 + min
53
78
  }
@@ -57,6 +82,19 @@ import UIKit
57
82
  return String((0..<length).map { _ in letters.randomElement()! })
58
83
  }
59
84
 
85
+ public func setPublicKey(_ publicKey: String) {
86
+ self.publicKey = publicKey
87
+ if !publicKey.isEmpty {
88
+ self.cachedKeyId = CryptoCipher.calcKeyId(publicKey: publicKey)
89
+ } else {
90
+ self.cachedKeyId = nil
91
+ }
92
+ }
93
+
94
+ public func getKeyId() -> String? {
95
+ return self.cachedKeyId
96
+ }
97
+
60
98
  private var isDevEnvironment: Bool {
61
99
  #if DEBUG
62
100
  return true
@@ -69,6 +107,64 @@ import UIKit
69
107
  return !self.isDevEnvironment && !self.isAppStoreReceiptSandbox() && !self.hasEmbeddedMobileProvision()
70
108
  }
71
109
 
110
+ /**
111
+ * Check if a 429 (Too Many Requests) response was received and set the flag
112
+ */
113
+ private func checkAndHandleRateLimitResponse(statusCode: Int?) -> Bool {
114
+ if statusCode == 429 {
115
+ // Send a statistic about the rate limit BEFORE setting the flag
116
+ // Only send once to prevent infinite loop if the stat request itself gets rate limited
117
+ if !CapgoUpdater.rateLimitExceeded && !CapgoUpdater.rateLimitStatisticSent {
118
+ CapgoUpdater.rateLimitStatisticSent = true
119
+
120
+ // Dispatch to background queue to avoid blocking the main thread
121
+ DispatchQueue.global(qos: .utility).async {
122
+ self.sendRateLimitStatistic()
123
+ }
124
+ }
125
+ CapgoUpdater.rateLimitExceeded = true
126
+ logger.warn("Rate limit exceeded (429). Stopping all stats and channel requests until app restart.")
127
+ return true
128
+ }
129
+ return false
130
+ }
131
+
132
+ /**
133
+ * Send a synchronous statistic about rate limiting
134
+ * Note: This method uses a semaphore to block until the request completes.
135
+ * It MUST be called from a background queue to avoid blocking the main thread.
136
+ */
137
+ private func sendRateLimitStatistic() {
138
+ guard !statsUrl.isEmpty else {
139
+ return
140
+ }
141
+
142
+ let current = getCurrentBundle()
143
+ var parameters = createInfoObject()
144
+ parameters.action = "rate_limit_reached"
145
+ parameters.version_name = current.getVersionName()
146
+ parameters.old_version_name = ""
147
+
148
+ // Send synchronously using semaphore (safe because we're on a background queue)
149
+ let semaphore = DispatchSemaphore(value: 0)
150
+ self.alamofireSession.request(
151
+ self.statsUrl,
152
+ method: .post,
153
+ parameters: parameters,
154
+ encoder: JSONParameterEncoder.default,
155
+ requestModifier: { $0.timeoutInterval = self.timeout }
156
+ ).responseData { response in
157
+ switch response.result {
158
+ case .success:
159
+ self.logger.info("Rate limit statistic sent")
160
+ case let .failure(error):
161
+ self.logger.error("Error sending rate limit statistic: \(error.localizedDescription)")
162
+ }
163
+ semaphore.signal()
164
+ }
165
+ semaphore.wait()
166
+ }
167
+
72
168
  // MARK: Private
73
169
  private func hasEmbeddedMobileProvision() -> Bool {
74
170
  guard Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") == nil else {
@@ -108,7 +204,7 @@ import UIKit
108
204
  do {
109
205
  try FileManager.default.createDirectory(atPath: source.path, withIntermediateDirectories: true, attributes: nil)
110
206
  } catch {
111
- print("\(CapacitorUpdater.TAG) Cannot createDirectory \(source.path)")
207
+ logger.error("Cannot createDirectory \(source.path)")
112
208
  throw CustomError.cannotCreateDirectory
113
209
  }
114
210
  }
@@ -118,7 +214,7 @@ import UIKit
118
214
  do {
119
215
  try FileManager.default.removeItem(atPath: source.path)
120
216
  } catch {
121
- print("\(CapacitorUpdater.TAG) File not removed. \(source.path)")
217
+ logger.error("File not removed. \(source.path)")
122
218
  throw CustomError.cannotDeleteDirectory
123
219
  }
124
220
  }
@@ -135,58 +231,14 @@ import UIKit
135
231
  return false
136
232
  }
137
233
  } catch {
138
- print("\(CapacitorUpdater.TAG) File not moved. source: \(source.path) dest: \(dest.path)")
234
+ logger.error("File not moved. source: \(source.path) dest: \(dest.path)")
139
235
  throw CustomError.cannotUnflat
140
236
  }
141
237
  }
142
238
 
143
- private func decryptFileV2(filePath: URL, sessionKey: String, version: String) throws {
144
- if self.publicKey.isEmpty || sessionKey.isEmpty || sessionKey.components(separatedBy: ":").count != 2 {
145
- print("\(CapacitorUpdater.TAG) Cannot find public key or sessionKey")
146
- return
147
- }
148
- do {
149
- guard let rsaPublicKey: RSAPublicKey = .load(rsaPublicKey: self.publicKey) else {
150
- print("cannot decode publicKey", self.publicKey)
151
- throw CustomError.cannotDecode
152
- }
153
-
154
- let sessionKeyArray: [String] = sessionKey.components(separatedBy: ":")
155
- guard let ivData: Data = Data(base64Encoded: sessionKeyArray[0]) else {
156
- print("cannot decode sessionKey", sessionKey)
157
- throw CustomError.cannotDecode
158
- }
159
-
160
- guard let sessionKeyDataEncrypted = Data(base64Encoded: sessionKeyArray[1]) else {
161
- throw NSError(domain: "Invalid session key data", code: 1, userInfo: nil)
162
- }
163
-
164
- guard let sessionKeyDataDecrypted = rsaPublicKey.decrypt(data: sessionKeyDataEncrypted) else {
165
- throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
166
- }
167
-
168
- let aesPrivateKey = AES128Key(iv: ivData, aes128Key: sessionKeyDataDecrypted)
169
-
170
- guard let encryptedData = try? Data(contentsOf: filePath) else {
171
- throw NSError(domain: "Failed to read encrypted data", code: 3, userInfo: nil)
172
- }
173
-
174
- guard let decryptedData = aesPrivateKey.decrypt(data: encryptedData) else {
175
- throw NSError(domain: "Failed to decrypt data", code: 4, userInfo: nil)
176
- }
177
-
178
- try decryptedData.write(to: filePath)
179
-
180
- } catch {
181
- print("\(CapacitorUpdater.TAG) Cannot decode: \(filePath.path)", error)
182
- self.sendStats(action: "decrypt_fail", versionName: version)
183
- throw CustomError.cannotDecode
184
- }
185
- }
186
-
187
239
  private func unzipProgressHandler(entry: String, zipInfo: unz_file_info, entryNumber: Int, total: Int, destUnZip: URL, id: String, unzipError: inout NSError?) {
188
240
  if entry.contains("\\") {
189
- print("\(CapacitorUpdater.TAG) unzip: Windows path is not supported, please use unix path as required by zip RFC: \(entry)")
241
+ logger.error("unzip: Windows path is not supported, please use unix path as required by zip RFC: \(entry)")
190
242
  self.sendStats(action: "windows_path_fail")
191
243
  }
192
244
 
@@ -255,12 +307,22 @@ import UIKit
255
307
 
256
308
  if !success || unzipError != nil {
257
309
  self.sendStats(action: "unzip_fail")
310
+ try? FileManager.default.removeItem(at: destUnZip)
258
311
  throw unzipError ?? CustomError.cannotUnzip
259
312
  }
260
313
 
261
314
  if try unflatFolder(source: destUnZip, dest: destPersist) {
262
315
  try deleteFolder(source: destUnZip)
263
316
  }
317
+
318
+ // Cleanup: remove the downloaded/decrypted zip after successful extraction
319
+ do {
320
+ if FileManager.default.fileExists(atPath: sourceZip.path) {
321
+ try FileManager.default.removeItem(at: sourceZip)
322
+ }
323
+ } catch {
324
+ logger.error("Could not delete source zip at \(sourceZip.path): \(error)")
325
+ }
264
326
  }
265
327
 
266
328
  private func createInfoObject() -> InfoObject {
@@ -273,12 +335,13 @@ import UIKit
273
335
  version_code: self.versionCode,
274
336
  version_os: self.versionOs,
275
337
  version_name: self.getCurrentBundle().getVersionName(),
276
- plugin_version: self.PLUGIN_VERSION,
338
+ plugin_version: self.pluginVersion,
277
339
  is_emulator: self.isEmulator(),
278
340
  is_prod: self.isProd(),
279
341
  action: nil,
280
342
  channel: nil,
281
- defaultChannel: self.defaultChannel
343
+ defaultChannel: self.defaultChannel,
344
+ key_id: self.cachedKeyId
282
345
  )
283
346
  }
284
347
 
@@ -289,12 +352,13 @@ import UIKit
289
352
  if let channel = channel {
290
353
  parameters.defaultChannel = channel
291
354
  }
292
- print("\(CapacitorUpdater.TAG) Auto-update parameters: \(parameters)")
293
- let request = AF.request(url, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
355
+ logger.info("Auto-update parameters: \(parameters)")
356
+ let request = alamofireSession.request(url, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
294
357
 
295
358
  request.validate().responseDecodable(of: AppVersionDec.self) { response in
296
359
  switch response.result {
297
360
  case .success:
361
+ latest.statusCode = response.response?.statusCode ?? 0
298
362
  if let url = response.value?.url {
299
363
  latest.url = url
300
364
  }
@@ -307,6 +371,9 @@ import UIKit
307
371
  if let major = response.value?.major {
308
372
  latest.major = major
309
373
  }
374
+ if let breaking = response.value?.breaking {
375
+ latest.breaking = breaking
376
+ }
310
377
  if let error = response.value?.error {
311
378
  latest.error = error
312
379
  }
@@ -322,10 +389,17 @@ import UIKit
322
389
  if let manifest = response.value?.manifest {
323
390
  latest.manifest = manifest
324
391
  }
392
+ if let link = response.value?.link {
393
+ latest.link = link
394
+ }
395
+ if let comment = response.value?.comment {
396
+ latest.comment = comment
397
+ }
325
398
  case let .failure(error):
326
- print("\(CapacitorUpdater.TAG) Error getting Latest", response.value ?? "", error )
399
+ self.logger.error("Error getting Latest \(response.value.debugDescription) \(error)")
327
400
  latest.message = "Error getting Latest \(String(describing: response.value))"
328
401
  latest.error = "response_error"
402
+ latest.statusCode = response.response?.statusCode ?? 0
329
403
  }
330
404
  semaphore.signal()
331
405
  }
@@ -336,7 +410,7 @@ import UIKit
336
410
  private func setCurrentBundle(bundle: String) {
337
411
  UserDefaults.standard.set(bundle, forKey: self.CAP_SERVER_PATH)
338
412
  UserDefaults.standard.synchronize()
339
- print("\(CapacitorUpdater.TAG) Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
413
+ logger.info("Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
340
414
  }
341
415
 
342
416
  private var tempDataPath: URL {
@@ -347,36 +421,15 @@ import UIKit
347
421
  return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("update.dat")
348
422
  }
349
423
  private var tempData = Data()
350
-
424
+
351
425
  private func verifyChecksum(file: URL, expectedHash: String) -> Bool {
352
- let actualHash = CryptoCipherV2.calcChecksum(filePath: file)
426
+ let actualHash = CryptoCipher.calcChecksum(filePath: file)
353
427
  return actualHash == expectedHash
354
428
  }
355
429
 
356
- public func decryptChecksum(checksum: String, version: String) throws -> String {
357
- if self.publicKey.isEmpty {
358
- return checksum
359
- }
360
- do {
361
- let checksumBytes: Data = Data(base64Encoded: checksum)!
362
- guard let rsaPublicKey: RSAPublicKey = .load(rsaPublicKey: self.publicKey) else {
363
- print("cannot decode publicKey", self.publicKey)
364
- throw CustomError.cannotDecode
365
- }
366
- guard let decryptedChecksum = rsaPublicKey.decrypt(data: checksumBytes) else {
367
- throw NSError(domain: "Failed to decrypt session key data", code: 2, userInfo: nil)
368
- }
369
- return decryptedChecksum.base64EncodedString()
370
- } catch {
371
- print("\(CapacitorUpdater.TAG) Cannot decrypt checksum: \(checksum)", error)
372
- self.sendStats(action: "decrypt_fail", versionName: version)
373
- throw CustomError.cannotDecode
374
- }
375
- }
376
-
377
- public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String) throws -> BundleInfo {
430
+ public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
378
431
  let id = self.randomString(length: 10)
379
- print("\(CapacitorUpdater.TAG) downloadManifest start \(id)")
432
+ logger.info("downloadManifest start \(id)")
380
433
  let destFolder = self.getBundleDirectory(id: id)
381
434
  let builtinFolder = Bundle.main.bundleURL.appendingPathComponent("public")
382
435
 
@@ -384,9 +437,12 @@ import UIKit
384
437
  try FileManager.default.createDirectory(at: destFolder, withIntermediateDirectories: true, attributes: nil)
385
438
 
386
439
  // Create and save BundleInfo before starting the download process
387
- let bundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: "")
440
+ let bundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: "", link: link, comment: comment)
388
441
  self.saveBundleInfo(id: id, bundle: bundleInfo)
389
442
 
443
+ // Send stats for manifest download start
444
+ self.sendStats(action: "download_manifest_start", versionName: version)
445
+
390
446
  // Notify the start of the download process
391
447
  self.notifyDownload(id: id, percent: 0, ignoreMultipleOfTen: true)
392
448
 
@@ -405,17 +461,23 @@ import UIKit
405
461
 
406
462
  if !self.publicKey.isEmpty && !sessionKey.isEmpty {
407
463
  do {
408
- fileHash = try CryptoCipherV2.decryptChecksum(checksum: fileHash, publicKey: self.publicKey, version: version)
464
+ fileHash = try CryptoCipher.decryptChecksum(checksum: fileHash, publicKey: self.publicKey)
409
465
  } catch {
410
466
  downloadError = error
411
- print("\(CapacitorUpdater.TAG) CryptoCipherV2.decryptChecksum error \(id) \(fileName) error: \(error)")
467
+ logger.error("CryptoCipher.decryptChecksum error \(id) \(fileName) error: \(error)")
412
468
  }
413
469
  }
414
470
 
471
+ // Check if file has .br extension for Brotli decompression
415
472
  let fileNameWithoutPath = (fileName as NSString).lastPathComponent
416
473
  let cacheFileName = "\(fileHash)_\(fileNameWithoutPath)"
417
474
  let cacheFilePath = cacheFolder.appendingPathComponent(cacheFileName)
418
- let destFilePath = destFolder.appendingPathComponent(fileName)
475
+
476
+ // Check if file is Brotli compressed and remove .br extension from destination
477
+ let isBrotli = fileName.hasSuffix(".br")
478
+ let destFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
479
+
480
+ let destFilePath = destFolder.appendingPathComponent(destFileName)
419
481
  let builtinFilePath = builtinFolder.appendingPathComponent(fileName)
420
482
 
421
483
  // Create necessary subdirectories in the destination folder
@@ -424,20 +486,30 @@ import UIKit
424
486
  dispatchGroup.enter()
425
487
 
426
488
  if FileManager.default.fileExists(atPath: builtinFilePath.path) && verifyChecksum(file: builtinFilePath, expectedHash: fileHash) {
427
- try FileManager.default.copyItem(at: builtinFilePath, to: destFilePath)
428
- print("\(CapacitorUpdater.TAG) downloadManifest \(fileName) using builtin file \(id)")
429
- completedFiles += 1
430
- self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
489
+ do {
490
+ try FileManager.default.copyItem(at: builtinFilePath, to: destFilePath)
491
+ logger.info("downloadManifest \(fileName) using builtin file \(id)")
492
+ completedFiles += 1
493
+ self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
494
+ } catch {
495
+ downloadError = error
496
+ logger.error("Failed to copy builtin file \(fileName): \(error.localizedDescription)")
497
+ }
431
498
  dispatchGroup.leave()
432
499
  } else if FileManager.default.fileExists(atPath: cacheFilePath.path) && verifyChecksum(file: cacheFilePath, expectedHash: fileHash) {
433
- try FileManager.default.copyItem(at: cacheFilePath, to: destFilePath)
434
- print("\(CapacitorUpdater.TAG) downloadManifest \(fileName) copy from cache \(id)")
435
- completedFiles += 1
436
- self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
500
+ do {
501
+ try FileManager.default.copyItem(at: cacheFilePath, to: destFilePath)
502
+ logger.info("downloadManifest \(fileName) copy from cache \(id)")
503
+ completedFiles += 1
504
+ self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
505
+ } catch {
506
+ downloadError = error
507
+ logger.error("Failed to copy cached file \(fileName): \(error.localizedDescription)")
508
+ }
437
509
  dispatchGroup.leave()
438
510
  } else {
439
511
  // File not in cache, download, decompress, and save to both cache and destination
440
- AF.download(downloadUrl).responseData { response in
512
+ self.alamofireSession.download(downloadUrl).responseData { response in
441
513
  defer { dispatchGroup.leave() }
442
514
 
443
515
  switch response.result {
@@ -445,10 +517,11 @@ import UIKit
445
517
  do {
446
518
  let statusCode = response.response?.statusCode ?? 200
447
519
  if statusCode < 200 || statusCode >= 300 {
520
+ self.sendStats(action: "download_manifest_file_fail", versionName: "\(version):\(fileName)")
448
521
  if let stringData = String(data: data, encoding: .utf8) {
449
- throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid. Data: \(stringData)"])
522
+ throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid. Data: \(stringData) for file \(fileName) at url \(downloadUrl)"])
450
523
  } else {
451
- throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid"])
524
+ throw NSError(domain: "StatusCodeError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch. Status code (\(statusCode)) invalid for file \(fileName) at url \(downloadUrl)"])
452
525
  }
453
526
  }
454
527
 
@@ -458,7 +531,7 @@ import UIKit
458
531
  let tempFile = self.cacheFolder.appendingPathComponent("temp_\(UUID().uuidString)")
459
532
  try finalData.write(to: tempFile)
460
533
  do {
461
- try CryptoCipherV2.decryptFile(filePath: tempFile, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
534
+ try CryptoCipher.decryptFile(filePath: tempFile, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
462
535
  } catch {
463
536
  self.sendStats(action: "decrypt_fail", versionName: version)
464
537
  throw error
@@ -468,18 +541,27 @@ import UIKit
468
541
  try FileManager.default.removeItem(at: tempFile)
469
542
  }
470
543
 
471
- // Decompress the Brotli data
472
- guard let decompressedData = self.decompressBrotli(data: finalData, fileName: fileName) else {
473
- throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data"])
544
+ // Use the isBrotli and destFilePath already computed above
545
+ if isBrotli {
546
+ // Decompress the Brotli data
547
+ guard let decompressedData = self.decompressBrotli(data: finalData, fileName: fileName) else {
548
+ self.sendStats(action: "download_manifest_brotli_fail", versionName: "\(version):\(destFileName)")
549
+ throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data for file \(fileName) at url \(downloadUrl)"])
550
+ }
551
+ finalData = decompressedData
474
552
  }
475
- finalData = decompressedData
476
553
 
477
554
  try finalData.write(to: destFilePath)
478
555
  if !self.publicKey.isEmpty && !sessionKey.isEmpty {
479
556
  // assume that calcChecksum != null
480
- let calculatedChecksum = CryptoCipherV2.calcChecksum(filePath: destFilePath)
557
+ let calculatedChecksum = CryptoCipher.calcChecksum(filePath: destFilePath)
558
+ CryptoCipher.logChecksumInfo(label: "Calculated checksum", hexChecksum: calculatedChecksum)
559
+ CryptoCipher.logChecksumInfo(label: "Expected checksum", hexChecksum: fileHash)
481
560
  if calculatedChecksum != fileHash {
482
- throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash))"])
561
+ // Delete the corrupt file before throwing error
562
+ try? FileManager.default.removeItem(at: destFilePath)
563
+ self.sendStats(action: "download_manifest_checksum_fail", versionName: "\(version):\(destFileName)")
564
+ throw NSError(domain: "ChecksumError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Computed checksum is not equal to required checksum (\(calculatedChecksum) != \(fileHash)) for file \(fileName) at url \(downloadUrl)"])
483
565
  }
484
566
  }
485
567
 
@@ -488,13 +570,15 @@ import UIKit
488
570
 
489
571
  completedFiles += 1
490
572
  self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
491
- print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) downloaded, decompressed\(!self.publicKey.isEmpty && !sessionKey.isEmpty ? ", decrypted" : ""), and cached")
573
+ self.logger.info("downloadManifest \(id) \(fileName) downloaded\(isBrotli ? ", decompressed" : "")\(!self.publicKey.isEmpty && !sessionKey.isEmpty ? ", decrypted" : ""), and cached")
492
574
  } catch {
493
575
  downloadError = error
494
- print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) error: \(error)")
576
+ self.logger.error("downloadManifest \(id) \(fileName) error: \(error.localizedDescription)")
495
577
  }
496
578
  case .failure(let error):
497
- print("\(CapacitorUpdater.TAG) downloadManifest \(id) \(fileName) download error: \(error). Debug response: \(response.debugDescription).")
579
+ downloadError = error
580
+ self.sendStats(action: "download_manifest_file_fail", versionName: "\(version):\(fileName)")
581
+ self.logger.error("downloadManifest \(id) \(fileName) download error: \(error.localizedDescription). Debug response: \(response.debugDescription).")
498
582
  }
499
583
  }
500
584
  }
@@ -513,19 +597,52 @@ import UIKit
513
597
  let updatedBundle = bundleInfo.setStatus(status: BundleStatus.PENDING.localizedString)
514
598
  self.saveBundleInfo(id: id, bundle: updatedBundle)
515
599
 
516
- print("\(CapacitorUpdater.TAG) downloadManifest done \(id)")
600
+ // Send stats for manifest download complete
601
+ self.sendStats(action: "download_manifest_complete", versionName: version)
602
+
603
+ self.notifyDownload(id: id, percent: 100, bundle: updatedBundle)
604
+ logger.info("downloadManifest done \(id)")
517
605
  return updatedBundle
518
606
  }
519
607
 
520
608
  private func decompressBrotli(data: Data, fileName: String) -> Data? {
609
+ // Handle empty files
610
+ if data.count == 0 {
611
+ return data
612
+ }
613
+
614
+ // Handle the special EMPTY_BROTLI_STREAM case
615
+ if data.count == 3 && data[0] == 0x1B && data[1] == 0x00 && data[2] == 0x06 {
616
+ return Data()
617
+ }
618
+
619
+ // For small files, check if it's a minimal Brotli wrapper
620
+ if data.count > 3 {
621
+ let maxBytes = min(32, data.count)
622
+ let hexDump = data.prefix(maxBytes).map { String(format: "%02x", $0) }.joined(separator: " ")
623
+ // Handle our minimal wrapper pattern
624
+ if data[0] == 0x1B && data[1] == 0x00 && data[2] == 0x06 && data.last == 0x03 {
625
+ let range = data.index(data.startIndex, offsetBy: 3)..<data.index(data.endIndex, offsetBy: -1)
626
+ return data[range]
627
+ }
628
+
629
+ // Handle brotli.compress minimal wrapper (quality 0)
630
+ if data[0] == 0x0b && data[1] == 0x02 && data[2] == 0x80 && data.last == 0x03 {
631
+ let range = data.index(data.startIndex, offsetBy: 3)..<data.index(data.endIndex, offsetBy: -1)
632
+ return data[range]
633
+ }
634
+ }
635
+
636
+ // For all other cases, try standard decompression
521
637
  let outputBufferSize = 65536
522
638
  var outputBuffer = [UInt8](repeating: 0, count: outputBufferSize)
523
639
  var decompressedData = Data()
524
640
 
525
641
  let streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
526
642
  var status = compression_stream_init(streamPointer, COMPRESSION_STREAM_DECODE, COMPRESSION_BROTLI)
643
+
527
644
  guard status != COMPRESSION_STATUS_ERROR else {
528
- print("\(CapacitorUpdater.TAG) Unable to initialize the decompression stream. \(fileName)")
645
+ logger.error("Error: Failed to initialize Brotli stream for \(fileName). Status: \(status)")
529
646
  return nil
530
647
  }
531
648
 
@@ -547,7 +664,7 @@ import UIKit
547
664
  if let baseAddress = rawBufferPointer.baseAddress {
548
665
  streamPointer.pointee.src_ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
549
666
  } else {
550
- print("\(CapacitorUpdater.TAG) Error: Unable to get base address of input data. \(fileName)")
667
+ logger.error("Error: Failed to get base address for \(fileName)")
551
668
  status = COMPRESSION_STATUS_ERROR
552
669
  return
553
670
  }
@@ -555,6 +672,9 @@ import UIKit
555
672
  }
556
673
 
557
674
  if status == COMPRESSION_STATUS_ERROR {
675
+ let maxBytes = min(32, data.count)
676
+ let hexDump = data.prefix(maxBytes).map { String(format: "%02x", $0) }.joined(separator: " ")
677
+ logger.error("Error: Brotli decompression failed for \(fileName). First \(maxBytes) bytes: \(hexDump)")
558
678
  break
559
679
  }
560
680
 
@@ -568,15 +688,19 @@ import UIKit
568
688
  if status == COMPRESSION_STATUS_END {
569
689
  break
570
690
  } else if status == COMPRESSION_STATUS_ERROR {
571
- print("\(CapacitorUpdater.TAG) Error during Brotli decompression. \(fileName)")
572
- // Try to decode as text if mostly ASCII
691
+ logger.error("Error: Brotli process failed for \(fileName). Status: \(status)")
573
692
  if let text = String(data: data, encoding: .utf8) {
574
693
  let asciiCount = text.unicodeScalars.filter { $0.isASCII }.count
575
694
  let totalCount = text.unicodeScalars.count
576
695
  if totalCount > 0 && Double(asciiCount) / Double(totalCount) >= 0.8 {
577
- print("\(CapacitorUpdater.TAG) Compressed data as text: \(text)")
696
+ logger.error("Error: Input appears to be plain text: \(text)")
578
697
  }
579
698
  }
699
+
700
+ let maxBytes = min(32, data.count)
701
+ let hexDump = data.prefix(maxBytes).map { String(format: "%02x", $0) }.joined(separator: " ")
702
+ logger.error("Error: Raw data (\(fileName)): \(hexDump)")
703
+
580
704
  return nil
581
705
  }
582
706
 
@@ -586,6 +710,7 @@ import UIKit
586
710
  }
587
711
 
588
712
  if input.count == 0 {
713
+ logger.error("Error: Zero input size for \(fileName)")
589
714
  break
590
715
  }
591
716
  }
@@ -593,7 +718,7 @@ import UIKit
593
718
  return status == COMPRESSION_STATUS_END ? decompressedData : nil
594
719
  }
595
720
 
596
- public func download(url: URL, version: String, sessionKey: String) throws -> BundleInfo {
721
+ public func download(url: URL, version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
597
722
  let id: String = self.randomString(length: 10)
598
723
  let semaphore = DispatchSemaphore(value: 0)
599
724
  if version != getLocalUpdateVersion() {
@@ -606,6 +731,10 @@ import UIKit
606
731
  var lastSentProgress = 0
607
732
  var totalReceivedBytes: Int64 = loadDownloadProgress() // Retrieving the amount of already downloaded data if exist, defined at 0 otherwise
608
733
  let requestHeaders: HTTPHeaders = ["Range": "bytes=\(totalReceivedBytes)-"]
734
+
735
+ // Send stats for zip download start
736
+ self.sendStats(action: "download_zip_start", versionName: version)
737
+
609
738
  // Opening connection for streaming the bytes
610
739
  if totalReceivedBytes == 0 {
611
740
  self.notifyDownload(id: id, percent: 0, ignoreMultipleOfTen: true)
@@ -614,11 +743,13 @@ import UIKit
614
743
  let monitor = ClosureEventMonitor()
615
744
  monitor.requestDidCompleteTaskWithError = { (_, _, error) in
616
745
  if error != nil {
617
- print("\(CapacitorUpdater.TAG) Downloading failed - ClosureEventMonitor activated")
746
+ self.logger.error("Downloading failed - ClosureEventMonitor activated")
618
747
  mainError = error as NSError?
619
748
  }
620
749
  }
621
- let session = Session(eventMonitors: [monitor])
750
+ let configuration = URLSessionConfiguration.default
751
+ configuration.httpAdditionalHeaders = ["User-Agent": self.userAgent]
752
+ let session = Session(configuration: configuration, eventMonitors: [monitor])
622
753
 
623
754
  let request = session.streamRequest(url, headers: requestHeaders).validate().onHTTPResponse(perform: { response in
624
755
  if let contentLength = response.headers.value(for: "Content-Length") {
@@ -645,16 +776,16 @@ import UIKit
645
776
  }
646
777
 
647
778
  } else {
648
- print("\(CapacitorUpdater.TAG) Download failed")
779
+ self.logger.error("Download failed")
649
780
  }
650
781
 
651
782
  case .complete:
652
- print("\(CapacitorUpdater.TAG) Download complete, total received bytes: \(totalReceivedBytes)")
783
+ self.logger.info("Download complete, total received bytes: \(totalReceivedBytes)")
653
784
  self.notifyDownload(id: id, percent: 70, ignoreMultipleOfTen: true)
654
785
  semaphore.signal()
655
786
  }
656
787
  }
657
- self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum))
788
+ self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum, link: link, comment: comment))
658
789
  let reachabilityManager = NetworkReachabilityManager()
659
790
  reachabilityManager?.startListening { status in
660
791
  switch status {
@@ -671,55 +802,67 @@ import UIKit
671
802
  reachabilityManager?.stopListening()
672
803
 
673
804
  if mainError != nil {
674
- print("\(CapacitorUpdater.TAG) Failed to download: \(String(describing: mainError))")
675
- self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
805
+ logger.error("Failed to download: \(String(describing: mainError))")
806
+ self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum, link: link, comment: comment))
676
807
  throw mainError!
677
808
  }
678
809
 
679
810
  let finalPath = tempDataPath.deletingLastPathComponent().appendingPathComponent("\(id)")
680
811
  do {
681
- try self.decryptFileV2(filePath: tempDataPath, sessionKey: sessionKey, version: version)
812
+ try CryptoCipher.decryptFile(filePath: tempDataPath, publicKey: self.publicKey, sessionKey: sessionKey, version: version)
682
813
  try FileManager.default.moveItem(at: tempDataPath, to: finalPath)
683
814
  } catch {
684
- print("\(CapacitorUpdater.TAG) Failed decrypt file : \(error)")
685
- self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
815
+ logger.error("Failed decrypt file : \(error)")
816
+ self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum, link: link, comment: comment))
686
817
  cleanDownloadData()
687
818
  throw error
688
819
  }
689
820
 
690
821
  do {
691
- checksum = CryptoCipherV2.calcChecksum(filePath: finalPath)
692
- print("\(CapacitorUpdater.TAG) Downloading: 80% (unzipping)")
822
+ checksum = CryptoCipher.calcChecksum(filePath: finalPath)
823
+ CryptoCipher.logChecksumInfo(label: "Calculated bundle checksum", hexChecksum: checksum)
824
+ logger.info("Downloading: 80% (unzipping)")
693
825
  try self.saveDownloaded(sourceZip: finalPath, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory), notify: true)
694
826
 
695
827
  } catch {
696
- print("\(CapacitorUpdater.TAG) Failed to unzip file: \(error)")
697
- self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
828
+ logger.error("Failed to unzip file: \(error)")
829
+ self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum, link: link, comment: comment))
830
+ // Best-effort cleanup of the decrypted zip file when unzip fails
831
+ do {
832
+ if FileManager.default.fileExists(atPath: finalPath.path) {
833
+ try FileManager.default.removeItem(at: finalPath)
834
+ }
835
+ } catch {
836
+ logger.error("Could not delete failed zip at \(finalPath.path): \(error)")
837
+ }
698
838
  cleanDownloadData()
699
- // todo: cleanup zip attempts
700
839
  throw error
701
840
  }
702
841
 
703
842
  self.notifyDownload(id: id, percent: 90)
704
- print("\(CapacitorUpdater.TAG) Downloading: 90% (wrapping up)")
705
- let info = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum)
843
+ logger.info("Downloading: 90% (wrapping up)")
844
+ let info = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum, link: link, comment: comment)
706
845
  self.saveBundleInfo(id: id, bundle: info)
707
846
  self.cleanDownloadData()
708
- self.notifyDownload(id: id, percent: 100)
709
- print("\(CapacitorUpdater.TAG) Downloading: 100% (complete)")
847
+
848
+ // Send stats for zip download complete
849
+ self.sendStats(action: "download_zip_complete", versionName: version)
850
+
851
+ self.notifyDownload(id: id, percent: 100, bundle: info)
852
+ logger.info("Downloading: 100% (complete)")
710
853
  return info
711
854
  }
712
855
  private func ensureResumableFilesExist() {
713
856
  let fileManager = FileManager.default
714
857
  if !fileManager.fileExists(atPath: tempDataPath.path) {
715
858
  if !fileManager.createFile(atPath: tempDataPath.path, contents: Data()) {
716
- print("\(CapacitorUpdater.TAG) Cannot ensure that a file at \(tempDataPath.path) exists")
859
+ logger.error("Cannot ensure that a file at \(tempDataPath.path) exists")
717
860
  }
718
861
  }
719
862
 
720
863
  if !fileManager.fileExists(atPath: updateInfo.path) {
721
864
  if !fileManager.createFile(atPath: updateInfo.path, contents: Data()) {
722
- print("\(CapacitorUpdater.TAG) Cannot ensure that a file at \(updateInfo.path) exists")
865
+ logger.error("Cannot ensure that a file at \(updateInfo.path) exists")
723
866
  }
724
867
  }
725
868
  }
@@ -731,7 +874,7 @@ import UIKit
731
874
  do {
732
875
  try fileManager.removeItem(at: tempDataPath)
733
876
  } catch {
734
- print("\(CapacitorUpdater.TAG) Could not delete file at \(tempDataPath): \(error)")
877
+ logger.error("Could not delete file at \(tempDataPath): \(error)")
735
878
  }
736
879
  }
737
880
  // Deleting update.dat
@@ -739,7 +882,7 @@ import UIKit
739
882
  do {
740
883
  try fileManager.removeItem(at: updateInfo)
741
884
  } catch {
742
- print("\(CapacitorUpdater.TAG) Could not delete file at \(updateInfo): \(error)")
885
+ logger.error("Could not delete file at \(updateInfo): \(error)")
743
886
  }
744
887
  }
745
888
  }
@@ -758,7 +901,7 @@ import UIKit
758
901
  fileHandle.closeFile()
759
902
  }
760
903
  } catch {
761
- print("Failed to write data starting at byte \(byteOffset): \(error)")
904
+ logger.error("Failed to write data starting at byte \(byteOffset): \(error)")
762
905
  }
763
906
  self.tempData.removeAll() // Clearing tempData to avoid writing the same data multiple times
764
907
  }
@@ -767,7 +910,7 @@ import UIKit
767
910
  do {
768
911
  try "\(version)".write(to: updateInfo, atomically: true, encoding: .utf8)
769
912
  } catch {
770
- print("\(CapacitorUpdater.TAG) Failed to save progress: \(error)")
913
+ logger.error("Failed to save progress: \(error)")
771
914
  }
772
915
  }
773
916
  private func getLocalUpdateVersion() -> String { // Return the version that was tried to be downloaded on last download attempt
@@ -789,7 +932,7 @@ import UIKit
789
932
  return fileSize.int64Value
790
933
  }
791
934
  } catch {
792
- print("\(CapacitorUpdater.TAG) Could not retrieve already downloaded data size : \(error)")
935
+ logger.error("Could not retrieve already downloaded data size : \(error)")
793
936
  }
794
937
  return 0
795
938
  }
@@ -801,7 +944,7 @@ import UIKit
801
944
  do {
802
945
  let files: [String] = try FileManager.default.contentsOfDirectory(atPath: dest.path)
803
946
  var res: [BundleInfo] = []
804
- print("\(CapacitorUpdater.TAG) list File : \(dest.path)")
947
+ logger.info("list File : \(dest.path)")
805
948
  if dest.exist {
806
949
  for id: String in files {
807
950
  res.append(self.getBundleInfo(id: id))
@@ -809,12 +952,12 @@ import UIKit
809
952
  }
810
953
  return res
811
954
  } catch {
812
- print("\(CapacitorUpdater.TAG) No version available \(dest.path)")
955
+ logger.info("No version available \(dest.path)")
813
956
  return []
814
957
  }
815
958
  } else {
816
959
  guard let regex = try? NSRegularExpression(pattern: "^[0-9A-Za-z]{10}_info$") else {
817
- print("\(CapacitorUpdater.TAG) Invald regex ?????")
960
+ logger.error("Invalid regex ?????")
818
961
  return []
819
962
  }
820
963
  return UserDefaults.standard.dictionaryRepresentation().keys.filter {
@@ -833,7 +976,7 @@ import UIKit
833
976
  public func delete(id: String, removeInfo: Bool) -> Bool {
834
977
  let deleted: BundleInfo = self.getBundleInfo(id: id)
835
978
  if deleted.isBuiltin() || self.getCurrentBundleId() == id {
836
- print("\(CapacitorUpdater.TAG) Cannot delete \(id)")
979
+ logger.info("Cannot delete \(id)")
837
980
  return false
838
981
  }
839
982
 
@@ -842,7 +985,7 @@ import UIKit
842
985
  !next.isDeleted() &&
843
986
  !next.isErrorStatus() &&
844
987
  next.getId() == id {
845
- print("\(CapacitorUpdater.TAG) Cannot delete the next bundle \(id)")
988
+ logger.info("Cannot delete the next bundle \(id)")
846
989
  return false
847
990
  }
848
991
 
@@ -850,7 +993,7 @@ import UIKit
850
993
  do {
851
994
  try FileManager.default.removeItem(atPath: destPersist.path)
852
995
  } catch {
853
- print("\(CapacitorUpdater.TAG) Folder \(destPersist.path), not removed.")
996
+ logger.error("Folder \(destPersist.path), not removed.")
854
997
  // even if, we don;t care. Android doesn't care
855
998
  if removeInfo {
856
999
  self.removeBundleInfo(id: id)
@@ -863,7 +1006,7 @@ import UIKit
863
1006
  } else {
864
1007
  self.saveBundleInfo(id: id, bundle: deleted.setStatus(status: BundleStatus.DELETED.localizedString))
865
1008
  }
866
- print("\(CapacitorUpdater.TAG) bundle delete \(deleted.getVersionName())")
1009
+ logger.info("bundle delete \(deleted.getVersionName())")
867
1010
  self.sendStats(action: "delete", versionName: deleted.getVersionName())
868
1011
  return true
869
1012
  }
@@ -872,6 +1015,55 @@ import UIKit
872
1015
  return self.delete(id: id, removeInfo: true)
873
1016
  }
874
1017
 
1018
+ public func cleanupDeltaCache() {
1019
+ let fileManager = FileManager.default
1020
+ guard fileManager.fileExists(atPath: cacheFolder.path) else {
1021
+ return
1022
+ }
1023
+ do {
1024
+ try fileManager.removeItem(at: cacheFolder)
1025
+ logger.info("Cleaned up delta cache folder")
1026
+ } catch {
1027
+ logger.error("Failed to cleanup delta cache: \(error.localizedDescription)")
1028
+ }
1029
+ }
1030
+
1031
+ public func cleanupDownloadDirectories(allowedIds: Set<String>) {
1032
+ let bundleRoot = libraryDir.appendingPathComponent(bundleDirectory)
1033
+ let fileManager = FileManager.default
1034
+
1035
+ guard fileManager.fileExists(atPath: bundleRoot.path) else {
1036
+ return
1037
+ }
1038
+
1039
+ do {
1040
+ let contents = try fileManager.contentsOfDirectory(at: bundleRoot, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles])
1041
+
1042
+ for url in contents {
1043
+ let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey])
1044
+ if resourceValues.isDirectory != true {
1045
+ continue
1046
+ }
1047
+
1048
+ let id = url.lastPathComponent
1049
+
1050
+ if allowedIds.contains(id) {
1051
+ continue
1052
+ }
1053
+
1054
+ do {
1055
+ try fileManager.removeItem(at: url)
1056
+ self.removeBundleInfo(id: id)
1057
+ logger.info("Deleted orphan bundle directory: \(id)")
1058
+ } catch {
1059
+ logger.error("Failed to delete orphan bundle directory: \(id) \(error.localizedDescription)")
1060
+ }
1061
+ }
1062
+ } catch {
1063
+ logger.error("Failed to enumerate bundle directory for cleanup: \(error.localizedDescription)")
1064
+ }
1065
+ }
1066
+
875
1067
  public func getBundleDirectory(id: String) -> URL {
876
1068
  return libraryDir.appendingPathComponent(self.bundleDirectory).appendingPathComponent(id)
877
1069
  }
@@ -916,7 +1108,7 @@ import UIKit
916
1108
  public func autoReset() {
917
1109
  let currentBundle: BundleInfo = self.getCurrentBundle()
918
1110
  if !currentBundle.isBuiltin() && !self.bundleExists(id: currentBundle.getId()) {
919
- print("\(CapacitorUpdater.TAG) Folder at bundle path does not exist. Triggering reset.")
1111
+ logger.info("Folder at bundle path does not exist. Triggering reset.")
920
1112
  self.reset()
921
1113
  }
922
1114
  }
@@ -926,7 +1118,7 @@ import UIKit
926
1118
  }
927
1119
 
928
1120
  public func reset(isInternal: Bool) {
929
- print("\(CapacitorUpdater.TAG) reset: \(isInternal)")
1121
+ logger.info("reset: \(isInternal)")
930
1122
  let currentBundleName = self.getCurrentBundle().getVersionName()
931
1123
  self.setCurrentBundle(bundle: "")
932
1124
  self.setFallbackBundle(fallback: Optional<BundleInfo>.none)
@@ -939,14 +1131,14 @@ import UIKit
939
1131
  public func setSuccess(bundle: BundleInfo, autoDeletePrevious: Bool) {
940
1132
  self.setBundleStatus(id: bundle.getId(), status: BundleStatus.SUCCESS)
941
1133
  let fallback: BundleInfo = self.getFallbackBundle()
942
- print("\(CapacitorUpdater.TAG) Fallback bundle is: \(fallback.toString())")
943
- print("\(CapacitorUpdater.TAG) Version successfully loaded: \(bundle.toString())")
1134
+ logger.info("Fallback bundle is: \(fallback.toString())")
1135
+ logger.info("Version successfully loaded: \(bundle.toString())")
944
1136
  if autoDeletePrevious && !fallback.isBuiltin() && fallback.getId() != bundle.getId() {
945
1137
  let res = self.delete(id: fallback.getId())
946
1138
  if res {
947
- print("\(CapacitorUpdater.TAG) Deleted previous bundle: \(fallback.toString())")
1139
+ logger.info("Deleted previous bundle: \(fallback.toString())")
948
1140
  } else {
949
- print("\(CapacitorUpdater.TAG) Failed to delete previous bundle: \(fallback.toString())")
1141
+ logger.error("Failed to delete previous bundle: \(fallback.toString())")
950
1142
  }
951
1143
  }
952
1144
  self.setFallbackBundle(fallback: bundle)
@@ -956,46 +1148,41 @@ import UIKit
956
1148
  self.setBundleStatus(id: bundle.getId(), status: BundleStatus.ERROR)
957
1149
  }
958
1150
 
959
- func unsetChannel() -> SetChannel {
1151
+ func unsetChannel(defaultChannelKey: String, configDefaultChannel: String) -> SetChannel {
960
1152
  let setChannel: SetChannel = SetChannel()
961
- if (self.channelUrl ).isEmpty {
962
- print("\(CapacitorUpdater.TAG) Channel URL is not set")
963
- setChannel.message = "Channel URL is not set"
964
- setChannel.error = "missing_config"
965
- return setChannel
966
- }
967
- let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
968
- let parameters: InfoObject = self.createInfoObject()
969
1153
 
970
- let request = AF.request(self.channelUrl, method: .delete, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
1154
+ // Clear persisted defaultChannel and revert to config value
1155
+ UserDefaults.standard.removeObject(forKey: defaultChannelKey)
1156
+ UserDefaults.standard.synchronize()
1157
+ self.defaultChannel = configDefaultChannel
1158
+ self.logger.info("Persisted defaultChannel cleared, reverted to config value: \(configDefaultChannel)")
971
1159
 
972
- request.validate().responseDecodable(of: SetChannelDec.self) { response in
973
- switch response.result {
974
- case .success:
975
- if let status = response.value?.status {
976
- setChannel.status = status
977
- }
978
- if let error = response.value?.error {
979
- setChannel.error = error
980
- }
981
- if let message = response.value?.message {
982
- setChannel.message = message
983
- }
984
- case let .failure(error):
985
- print("\(CapacitorUpdater.TAG) Error unset Channel", response.value ?? "", error)
986
- setChannel.message = "Error unset Channel \(String(describing: response.value))"
987
- setChannel.error = "response_error"
988
- }
989
- semaphore.signal()
990
- }
991
- semaphore.wait()
1160
+ setChannel.status = "ok"
1161
+ setChannel.message = "Channel override removed"
992
1162
  return setChannel
993
1163
  }
994
1164
 
995
- func setChannel(channel: String) -> SetChannel {
1165
+ func setChannel(channel: String, defaultChannelKey: String, allowSetDefaultChannel: Bool) -> SetChannel {
996
1166
  let setChannel: SetChannel = SetChannel()
1167
+
1168
+ // Check if setting defaultChannel is allowed
1169
+ if !allowSetDefaultChannel {
1170
+ logger.error("setChannel is disabled by allowSetDefaultChannel config")
1171
+ setChannel.message = "setChannel is disabled by configuration"
1172
+ setChannel.error = "disabled_by_config"
1173
+ return setChannel
1174
+ }
1175
+
1176
+ // Check if rate limit was exceeded
1177
+ if CapgoUpdater.rateLimitExceeded {
1178
+ logger.debug("Skipping setChannel due to rate limit (429). Requests will resume after app restart.")
1179
+ setChannel.message = "Rate limit exceeded"
1180
+ setChannel.error = "rate_limit_exceeded"
1181
+ return setChannel
1182
+ }
1183
+
997
1184
  if (self.channelUrl ).isEmpty {
998
- print("\(CapacitorUpdater.TAG) Channel URL is not set")
1185
+ logger.error("Channel URL is not set")
999
1186
  setChannel.message = "Channel URL is not set"
1000
1187
  setChannel.error = "missing_config"
1001
1188
  return setChannel
@@ -1004,24 +1191,36 @@ import UIKit
1004
1191
  var parameters: InfoObject = self.createInfoObject()
1005
1192
  parameters.channel = channel
1006
1193
 
1007
- let request = AF.request(self.channelUrl, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
1194
+ let request = alamofireSession.request(self.channelUrl, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
1008
1195
 
1009
1196
  request.validate().responseDecodable(of: SetChannelDec.self) { response in
1197
+ // Check for 429 rate limit
1198
+ if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
1199
+ setChannel.message = "Rate limit exceeded"
1200
+ setChannel.error = "rate_limit_exceeded"
1201
+ semaphore.signal()
1202
+ return
1203
+ }
1204
+
1010
1205
  switch response.result {
1011
1206
  case .success:
1012
- if let status = response.value?.status {
1013
- setChannel.status = status
1014
- }
1015
- if let error = response.value?.error {
1016
- setChannel.error = error
1017
- }
1018
- if let message = response.value?.message {
1019
- setChannel.message = message
1207
+ if let responseValue = response.value {
1208
+ if let error = responseValue.error {
1209
+ setChannel.error = error
1210
+ } else {
1211
+ // Success - persist defaultChannel
1212
+ self.defaultChannel = channel
1213
+ UserDefaults.standard.set(channel, forKey: defaultChannelKey)
1214
+ UserDefaults.standard.synchronize()
1215
+ self.logger.info("defaultChannel persisted locally: \(channel)")
1216
+
1217
+ setChannel.status = responseValue.status ?? ""
1218
+ setChannel.message = responseValue.message ?? ""
1219
+ }
1020
1220
  }
1021
1221
  case let .failure(error):
1022
- print("\(CapacitorUpdater.TAG) Error set Channel", response.value ?? "", error)
1023
- setChannel.message = "Error set Channel \(String(describing: response.value))"
1024
- setChannel.error = "response_error"
1222
+ self.logger.error("Error set Channel \(error)")
1223
+ setChannel.error = "Request failed: \(error.localizedDescription)"
1025
1224
  }
1026
1225
  semaphore.signal()
1027
1226
  }
@@ -1031,36 +1230,48 @@ import UIKit
1031
1230
 
1032
1231
  func getChannel() -> GetChannel {
1033
1232
  let getChannel: GetChannel = GetChannel()
1233
+
1234
+ // Check if rate limit was exceeded
1235
+ if CapgoUpdater.rateLimitExceeded {
1236
+ logger.debug("Skipping getChannel due to rate limit (429). Requests will resume after app restart.")
1237
+ getChannel.message = "Rate limit exceeded"
1238
+ getChannel.error = "rate_limit_exceeded"
1239
+ return getChannel
1240
+ }
1241
+
1034
1242
  if (self.channelUrl ).isEmpty {
1035
- print("\(CapacitorUpdater.TAG) Channel URL is not set")
1243
+ logger.error("Channel URL is not set")
1036
1244
  getChannel.message = "Channel URL is not set"
1037
1245
  getChannel.error = "missing_config"
1038
1246
  return getChannel
1039
1247
  }
1040
1248
  let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
1041
1249
  let parameters: InfoObject = self.createInfoObject()
1042
- let request = AF.request(self.channelUrl, method: .put, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
1250
+ let request = alamofireSession.request(self.channelUrl, method: .put, parameters: parameters, encoder: JSONParameterEncoder.default, requestModifier: { $0.timeoutInterval = self.timeout })
1043
1251
 
1044
1252
  request.validate().responseDecodable(of: GetChannelDec.self) { response in
1045
1253
  defer {
1046
1254
  semaphore.signal()
1047
1255
  }
1256
+
1257
+ // Check for 429 rate limit
1258
+ if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
1259
+ getChannel.message = "Rate limit exceeded"
1260
+ getChannel.error = "rate_limit_exceeded"
1261
+ return
1262
+ }
1263
+
1048
1264
  switch response.result {
1049
1265
  case .success:
1050
- if let status = response.value?.status {
1051
- getChannel.status = status
1052
- }
1053
- if let error = response.value?.error {
1054
- getChannel.error = error
1055
- }
1056
- if let message = response.value?.message {
1057
- getChannel.message = message
1058
- }
1059
- if let channel = response.value?.channel {
1060
- getChannel.channel = channel
1061
- }
1062
- if let allowSet = response.value?.allowSet {
1063
- getChannel.allowSet = allowSet
1266
+ if let responseValue = response.value {
1267
+ if let error = responseValue.error {
1268
+ getChannel.error = error
1269
+ } else {
1270
+ getChannel.status = responseValue.status ?? ""
1271
+ getChannel.message = responseValue.message ?? ""
1272
+ getChannel.channel = responseValue.channel ?? ""
1273
+ getChannel.allowSet = responseValue.allowSet ?? true
1274
+ }
1064
1275
  }
1065
1276
  case let .failure(error):
1066
1277
  if let data = response.data, let bodyString = String(data: data, encoding: .utf8) {
@@ -1071,18 +1282,113 @@ import UIKit
1071
1282
  }
1072
1283
  }
1073
1284
 
1074
- print("\(CapacitorUpdater.TAG) Error get Channel", response.value ?? "", error)
1075
- getChannel.message = "Error get Channel \(String(describing: response.value)))"
1076
- getChannel.error = "response_error"
1285
+ self.logger.error("Error get Channel \(error)")
1286
+ getChannel.error = "Request failed: \(error.localizedDescription)"
1077
1287
  }
1078
1288
  }
1079
1289
  semaphore.wait()
1080
1290
  return getChannel
1081
1291
  }
1082
1292
 
1293
+ func listChannels() -> ListChannels {
1294
+ let listChannels: ListChannels = ListChannels()
1295
+
1296
+ // Check if rate limit was exceeded
1297
+ if CapgoUpdater.rateLimitExceeded {
1298
+ logger.debug("Skipping listChannels due to rate limit (429). Requests will resume after app restart.")
1299
+ listChannels.error = "rate_limit_exceeded"
1300
+ return listChannels
1301
+ }
1302
+
1303
+ if (self.channelUrl).isEmpty {
1304
+ logger.error("Channel URL is not set")
1305
+ listChannels.error = "Channel URL is not set"
1306
+ return listChannels
1307
+ }
1308
+
1309
+ let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
1310
+
1311
+ // Create info object and convert to query parameters
1312
+ let infoObject = self.createInfoObject()
1313
+
1314
+ // Create query parameters from InfoObject
1315
+ var urlComponents = URLComponents(string: self.channelUrl)
1316
+ var queryItems: [URLQueryItem] = []
1317
+
1318
+ // Convert InfoObject to dictionary using Mirror
1319
+ let mirror = Mirror(reflecting: infoObject)
1320
+ for child in mirror.children {
1321
+ if let key = child.label, let value = child.value as? CustomStringConvertible {
1322
+ queryItems.append(URLQueryItem(name: key, value: String(describing: value)))
1323
+ } else if let key = child.label {
1324
+ // Handle optional values
1325
+ let mirror = Mirror(reflecting: child.value)
1326
+ if let value = mirror.children.first?.value {
1327
+ queryItems.append(URLQueryItem(name: key, value: String(describing: value)))
1328
+ }
1329
+ }
1330
+ }
1331
+
1332
+ urlComponents?.queryItems = queryItems
1333
+
1334
+ guard let url = urlComponents?.url else {
1335
+ logger.error("Invalid channel URL")
1336
+ listChannels.error = "Invalid channel URL"
1337
+ return listChannels
1338
+ }
1339
+
1340
+ let request = alamofireSession.request(url, method: .get, requestModifier: { $0.timeoutInterval = self.timeout })
1341
+
1342
+ request.validate().responseDecodable(of: ListChannelsDec.self) { response in
1343
+ defer {
1344
+ semaphore.signal()
1345
+ }
1346
+
1347
+ // Check for 429 rate limit
1348
+ if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
1349
+ listChannels.error = "rate_limit_exceeded"
1350
+ return
1351
+ }
1352
+
1353
+ switch response.result {
1354
+ case .success:
1355
+ if let responseValue = response.value {
1356
+ // Check for server-side errors
1357
+ if let error = responseValue.error {
1358
+ listChannels.error = error
1359
+ return
1360
+ }
1361
+
1362
+ // Backend returns direct array, so channels should be populated by our custom decoder
1363
+ if let channels = responseValue.channels {
1364
+ listChannels.channels = channels.map { channel in
1365
+ var channelDict: [String: Any] = [:]
1366
+ channelDict["id"] = channel.id ?? ""
1367
+ channelDict["name"] = channel.name ?? ""
1368
+ channelDict["public"] = channel.public ?? false
1369
+ channelDict["allow_self_set"] = channel.allow_self_set ?? false
1370
+ return channelDict
1371
+ }
1372
+ }
1373
+ }
1374
+ case let .failure(error):
1375
+ self.logger.error("Error list channels \(error)")
1376
+ listChannels.error = "Request failed: \(error.localizedDescription)"
1377
+ }
1378
+ }
1379
+ semaphore.wait()
1380
+ return listChannels
1381
+ }
1382
+
1083
1383
  private let operationQueue = OperationQueue()
1084
1384
 
1085
1385
  func sendStats(action: String, versionName: String? = nil, oldVersionName: String? = "") {
1386
+ // Check if rate limit was exceeded
1387
+ if CapgoUpdater.rateLimitExceeded {
1388
+ logger.debug("Skipping sendStats due to rate limit (429). Stats will resume after app restart.")
1389
+ return
1390
+ }
1391
+
1086
1392
  guard !statsUrl.isEmpty else {
1087
1393
  return
1088
1394
  }
@@ -1097,22 +1403,28 @@ import UIKit
1097
1403
 
1098
1404
  let operation = BlockOperation {
1099
1405
  let semaphore = DispatchSemaphore(value: 0)
1100
- AF.request(
1406
+ self.alamofireSession.request(
1101
1407
  self.statsUrl,
1102
1408
  method: .post,
1103
1409
  parameters: parameters,
1104
1410
  encoder: JSONParameterEncoder.default,
1105
1411
  requestModifier: { $0.timeoutInterval = self.timeout }
1106
1412
  ).responseData { response in
1413
+ // Check for 429 rate limit
1414
+ if self.checkAndHandleRateLimitResponse(statusCode: response.response?.statusCode) {
1415
+ semaphore.signal()
1416
+ return
1417
+ }
1418
+
1107
1419
  switch response.result {
1108
1420
  case .success:
1109
- print("\(CapacitorUpdater.TAG) Stats sent for \(action), version \(versionName)")
1421
+ self.logger.info("Stats sent for \(action), version \(versionName)")
1110
1422
  case let .failure(error):
1111
- print("\(CapacitorUpdater.TAG) Error sending stats: ", response.value ?? "", error.localizedDescription)
1423
+ self.logger.error("Error sending stats: \(response.value?.debugDescription ?? "") \(error.localizedDescription)")
1112
1424
  }
1113
1425
  semaphore.signal()
1114
1426
  }
1115
- semaphore.signal()
1427
+ semaphore.wait()
1116
1428
  }
1117
1429
  operationQueue.addOperation(operation)
1118
1430
 
@@ -1123,7 +1435,6 @@ import UIKit
1123
1435
  if id != nil {
1124
1436
  trueId = id!
1125
1437
  }
1126
- // print("\(CapacitorUpdater.TAG) Getting info for bundle [\(trueId)]")
1127
1438
  let result: BundleInfo
1128
1439
  if BundleInfo.ID_BUILTIN == trueId {
1129
1440
  result = BundleInfo(id: trueId, version: "", status: BundleStatus.SUCCESS, checksum: "")
@@ -1133,11 +1444,10 @@ import UIKit
1133
1444
  do {
1134
1445
  result = try UserDefaults.standard.getObj(forKey: "\(trueId)\(self.INFO_SUFFIX)", castTo: BundleInfo.self)
1135
1446
  } catch {
1136
- print("\(CapacitorUpdater.TAG) Failed to parse info for bundle [\(trueId)]", error.localizedDescription)
1447
+ logger.error("Failed to parse info for bundle [\(trueId)] \(error.localizedDescription)")
1137
1448
  result = BundleInfo(id: trueId, version: "", status: BundleStatus.PENDING, checksum: "")
1138
1449
  }
1139
1450
  }
1140
- // print("\(CapacitorUpdater.TAG) Returning info bundle [\(result.toString())]")
1141
1451
  return result
1142
1452
  }
1143
1453
 
@@ -1157,26 +1467,26 @@ import UIKit
1157
1467
 
1158
1468
  public func saveBundleInfo(id: String, bundle: BundleInfo?) {
1159
1469
  if bundle != nil && (bundle!.isBuiltin() || bundle!.isUnknown()) {
1160
- print("\(CapacitorUpdater.TAG) Not saving info for bundle [\(id)]", bundle?.toString() ?? "")
1470
+ logger.info("Not saving info for bundle [\(id)] \(bundle?.toString() ?? "")")
1161
1471
  return
1162
1472
  }
1163
1473
  if bundle == nil {
1164
- print("\(CapacitorUpdater.TAG) Removing info for bundle [\(id)]")
1474
+ logger.info("Removing info for bundle [\(id)]")
1165
1475
  UserDefaults.standard.removeObject(forKey: "\(id)\(self.INFO_SUFFIX)")
1166
1476
  } else {
1167
1477
  let update = bundle!.setId(id: id)
1168
- print("\(CapacitorUpdater.TAG) Storing info for bundle [\(id)]", update.toString())
1478
+ logger.info("Storing info for bundle [\(id)] \(update.toString())")
1169
1479
  do {
1170
1480
  try UserDefaults.standard.setObj(update, forKey: "\(id)\(self.INFO_SUFFIX)")
1171
1481
  } catch {
1172
- print("\(CapacitorUpdater.TAG) Failed to save info for bundle [\(id)]", error.localizedDescription)
1482
+ logger.error("Failed to save info for bundle [\(id)] \(error.localizedDescription)")
1173
1483
  }
1174
1484
  }
1175
1485
  UserDefaults.standard.synchronize()
1176
1486
  }
1177
1487
 
1178
1488
  private func setBundleStatus(id: String, status: BundleStatus) {
1179
- print("\(CapacitorUpdater.TAG) Setting status for bundle [\(id)] to \(status)")
1489
+ logger.info("Setting status for bundle [\(id)] to \(status)")
1180
1490
  let info = self.getBundleInfo(id: id)
1181
1491
  self.saveBundleInfo(id: id, bundle: info.setStatus(status: status.localizedString))
1182
1492
  }