@capgo/capacitor-updater 6.43.4 → 6.45.10

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.
@@ -37,9 +37,7 @@ import UIKit
37
37
  public var defaultChannel: String = ""
38
38
  public var appId: String = ""
39
39
  public var deviceID = ""
40
- public var privateKey: String = ""
41
40
  public var publicKey: String = ""
42
- public var hasOldPrivateKeyPropertyInConfig: Bool = false
43
41
 
44
42
  // Cached key ID calculated once from publicKey
45
43
  private var cachedKeyId: String?
@@ -56,10 +54,31 @@ import UIKit
56
54
  private var statsFlushTimer: Timer?
57
55
  private static let statsFlushInterval: TimeInterval = 1.0
58
56
 
57
+ private static func sanitizeHeaderValue(_ value: String) -> String {
58
+ if value.isEmpty {
59
+ return "unknown"
60
+ }
61
+
62
+ let filteredScalars = value.unicodeScalars.filter { scalar in
63
+ let cp = scalar.value
64
+ let isVisibleAscii = (0x20...0x7E).contains(cp)
65
+ let isIso88591 = (0xA0...0xFF).contains(cp)
66
+ return isVisibleAscii || isIso88591
67
+ }
68
+
69
+ let sanitized = String(String.UnicodeScalarView(filteredScalars)).trimmingCharacters(in: .whitespacesAndNewlines)
70
+ return sanitized.isEmpty ? "unknown" : sanitized
71
+ }
72
+
73
+ static func buildUserAgent(appId: String, pluginVersion: String, versionOs: String) -> String {
74
+ let safePluginVersion = sanitizeHeaderValue(pluginVersion)
75
+ let safeAppId = sanitizeHeaderValue(appId)
76
+ let safeVersionOs = sanitizeHeaderValue(versionOs)
77
+ return "CapacitorUpdater/\(safePluginVersion) (\(safeAppId)) ios/\(safeVersionOs)"
78
+ }
79
+
59
80
  private var userAgent: String {
60
- let safePluginVersion = pluginVersion.isEmpty ? "unknown" : pluginVersion
61
- let safeAppId = appId.isEmpty ? "unknown" : appId
62
- return "CapacitorUpdater/\(safePluginVersion) (\(safeAppId)) ios/\(versionOs)"
81
+ CapgoUpdater.buildUserAgent(appId: appId, pluginVersion: pluginVersion, versionOs: versionOs)
63
82
  }
64
83
 
65
84
  private lazy var alamofireSession: Session = {
@@ -73,6 +92,7 @@ import UIKit
73
92
  notifyDownloadRaw(id, percent, ignoreMultipleOfTen, bundle)
74
93
  }
75
94
  public var notifyDownload: (String, Int) -> Void = { _, _ in }
95
+ public var notifyListeners: (String, [String: Any]) -> Void = { _, _ in }
76
96
 
77
97
  public func setLogger(_ logger: Logger) {
78
98
  self.logger = logger
@@ -458,6 +478,47 @@ import UIKit
458
478
  public func getLatest(url: URL, channel: String?) -> AppVersion {
459
479
  let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
460
480
  let latest: AppVersion = AppVersion()
481
+ func applyLatestResponse(_ value: AppVersionDec?) {
482
+ if let url = value?.url {
483
+ latest.url = url
484
+ }
485
+ if let checksum = value?.checksum {
486
+ latest.checksum = checksum
487
+ }
488
+ if let version = value?.version {
489
+ latest.version = version
490
+ }
491
+ if let major = value?.major {
492
+ latest.major = major
493
+ }
494
+ if let breaking = value?.breaking {
495
+ latest.breaking = breaking
496
+ }
497
+ if let error = value?.error {
498
+ latest.error = error
499
+ }
500
+ if let kind = value?.kind {
501
+ latest.kind = kind
502
+ }
503
+ if let message = value?.message {
504
+ latest.message = message
505
+ }
506
+ if let sessionKey = value?.session_key {
507
+ latest.sessionKey = sessionKey
508
+ }
509
+ if let data = value?.data {
510
+ latest.data = data
511
+ }
512
+ if let manifest = value?.manifest {
513
+ latest.manifest = manifest
514
+ }
515
+ if let link = value?.link {
516
+ latest.link = link
517
+ }
518
+ if let comment = value?.comment {
519
+ latest.comment = comment
520
+ }
521
+ }
461
522
  var parameters: InfoObject = self.createInfoObject()
462
523
  if let channel = channel {
463
524
  parameters.defaultChannel = channel
@@ -469,48 +530,32 @@ import UIKit
469
530
  switch response.result {
470
531
  case .success:
471
532
  latest.statusCode = response.response?.statusCode ?? 0
472
- if let url = response.value?.url {
473
- latest.url = url
474
- }
475
- if let checksum = response.value?.checksum {
476
- latest.checksum = checksum
477
- }
478
- if let version = response.value?.version {
479
- latest.version = version
480
- }
481
- if let major = response.value?.major {
482
- latest.major = major
483
- }
484
- if let breaking = response.value?.breaking {
485
- latest.breaking = breaking
486
- }
487
- if let error = response.value?.error {
488
- latest.error = error
489
- }
490
- if let message = response.value?.message {
491
- latest.message = message
492
- }
493
- if let sessionKey = response.value?.session_key {
494
- latest.sessionKey = sessionKey
495
- }
496
- if let data = response.value?.data {
497
- latest.data = data
498
- }
499
- if let manifest = response.value?.manifest {
500
- latest.manifest = manifest
501
- }
502
- if let link = response.value?.link {
503
- latest.link = link
504
- }
505
- if let comment = response.value?.comment {
506
- latest.comment = comment
507
- }
533
+ applyLatestResponse(response.value)
508
534
  case let .failure(error):
509
535
  self.logger.error("Error getting latest version")
510
536
  self.logger.debug("Response: \(response.value.debugDescription), Error: \(error)")
511
- latest.message = "Error getting Latest"
512
- latest.error = "response_error"
513
537
  latest.statusCode = response.response?.statusCode ?? 0
538
+ if let data = response.data,
539
+ let decoded = try? JSONDecoder().decode(AppVersionDec.self, from: data) {
540
+ applyLatestResponse(decoded)
541
+ let decodedError = decoded.error ?? ""
542
+ let decodedKind = decoded.kind ?? ""
543
+ if decodedError.isEmpty && decodedKind.isEmpty {
544
+ if latest.message == nil || latest.message?.isEmpty == true {
545
+ latest.message = "Error getting Latest"
546
+ }
547
+ if latest.error == nil || latest.error?.isEmpty == true {
548
+ latest.error = "response_error"
549
+ }
550
+ if latest.kind == nil || latest.kind?.isEmpty == true {
551
+ latest.kind = "failed"
552
+ }
553
+ }
554
+ } else {
555
+ latest.message = "Error getting Latest"
556
+ latest.error = "response_error"
557
+ latest.kind = "failed"
558
+ }
514
559
  }
515
560
  semaphore.signal()
516
561
  }
@@ -524,6 +569,22 @@ import UIKit
524
569
  logger.info("Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
525
570
  }
526
571
 
572
+ static func shouldResetForForeignBundle(bundlePath: String?, isBuiltin: Bool, hasStoredBundleInfo: Bool) -> Bool {
573
+ guard let bundlePath, !bundlePath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
574
+ return false
575
+ }
576
+ return !isBuiltin && !hasStoredBundleInfo
577
+ }
578
+
579
+ private func hasStoredBundleInfo(id: String) -> Bool {
580
+ guard !id.isEmpty,
581
+ id != BundleInfo.ID_BUILTIN,
582
+ id != BundleInfo.VERSION_UNKNOWN else {
583
+ return false
584
+ }
585
+ return UserDefaults.standard.object(forKey: "\(id)\(self.INFO_SUFFIX)") != nil
586
+ }
587
+
527
588
  // Per-download temp file paths to prevent collisions when multiple downloads run concurrently
528
589
  private func tempDataPath(for id: String) -> URL {
529
590
  return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("package_\(id).tmp")
@@ -580,10 +641,36 @@ import UIKit
580
641
  for entry in manifest {
581
642
  guard let fileName = entry.file_name,
582
643
  let downloadUrl = entry.download_url else {
644
+ let error = NSError(
645
+ domain: "ManifestEntryError",
646
+ code: 1,
647
+ userInfo: [
648
+ NSLocalizedDescriptionKey: "Manifest entry is missing file_name or download_url"
649
+ ]
650
+ )
651
+ errorLock.lock()
652
+ if downloadError == nil {
653
+ downloadError = error
654
+ }
655
+ errorLock.unlock()
656
+ hasError.value = true
657
+ logger.error("Manifest entry is missing file_name or download_url")
583
658
  continue
584
659
  }
585
660
  guard let entryFileHash = entry.file_hash, !entryFileHash.isEmpty else {
586
661
  logger.error("Missing file_hash for manifest entry: \(entry.file_name ?? "unknown")")
662
+ let error = NSError(
663
+ domain: "ManifestEntryError",
664
+ code: 2,
665
+ userInfo: [
666
+ NSLocalizedDescriptionKey: "Manifest entry is missing file_hash for \(entry.file_name ?? "unknown")"
667
+ ]
668
+ )
669
+ errorLock.lock()
670
+ if downloadError == nil {
671
+ downloadError = error
672
+ }
673
+ errorLock.unlock()
587
674
  hasError.value = true
588
675
  continue
589
676
  }
@@ -602,9 +689,6 @@ import UIKit
602
689
  logger.debug("Bundle: \(id), File: \(fileName), Error: \(error)")
603
690
  continue
604
691
  }
605
- } else if self.hasOldPrivateKeyPropertyInConfig {
606
- // V1 Encryption (privateKey) - deprecated but supported
607
- // V1 doesn't decrypt checksum, uses different method
608
692
  }
609
693
 
610
694
  let finalFileHash = fileHash
@@ -675,11 +759,16 @@ import UIKit
675
759
  // Execute all operations concurrently and wait for completion
676
760
  manifestDownloadQueue.addOperations(operations, waitUntilFinished: true)
677
761
 
678
- if hasError.value, let error = downloadError {
762
+ if hasError.value {
763
+ let resolvedError = downloadError ?? NSError(
764
+ domain: "ManifestDownloadError",
765
+ code: 1,
766
+ userInfo: [NSLocalizedDescriptionKey: "Manifest download failed due to invalid or missing entries"]
767
+ )
679
768
  // Update bundle status to ERROR if download failed
680
769
  let errorBundle = bundleInfo.setStatus(status: BundleStatus.ERROR.localizedString)
681
770
  self.saveBundleInfo(id: id, bundle: errorBundle)
682
- throw error
771
+ throw resolvedError
683
772
  }
684
773
 
685
774
  // Update bundle status to PENDING after successful download
@@ -1416,6 +1505,52 @@ import UIKit
1416
1505
  return libraryDir.appendingPathComponent(self.bundleDirectory).appendingPathComponent(id)
1417
1506
  }
1418
1507
 
1508
+ struct ResetState {
1509
+ let currentBundlePath: String
1510
+ let fallbackBundleId: String
1511
+ let nextBundleId: String?
1512
+ }
1513
+
1514
+ func captureResetState() -> ResetState {
1515
+ ResetState(
1516
+ currentBundlePath: UserDefaults.standard.string(forKey: self.CAP_SERVER_PATH) ?? self.DEFAULT_FOLDER,
1517
+ fallbackBundleId: UserDefaults.standard.string(forKey: self.FALLBACK_VERSION) ?? BundleInfo.ID_BUILTIN,
1518
+ nextBundleId: UserDefaults.standard.string(forKey: self.NEXT_VERSION)
1519
+ )
1520
+ }
1521
+
1522
+ func restoreResetState(_ state: ResetState) {
1523
+ let currentBundlePath = state.currentBundlePath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
1524
+ ? self.DEFAULT_FOLDER
1525
+ : state.currentBundlePath
1526
+ let fallbackBundleId = state.fallbackBundleId.isEmpty ? BundleInfo.ID_BUILTIN : state.fallbackBundleId
1527
+
1528
+ self.setCurrentBundle(bundle: currentBundlePath)
1529
+ UserDefaults.standard.set(fallbackBundleId, forKey: self.FALLBACK_VERSION)
1530
+ if let nextBundleId = state.nextBundleId, !nextBundleId.isEmpty {
1531
+ UserDefaults.standard.set(nextBundleId, forKey: self.NEXT_VERSION)
1532
+ } else {
1533
+ UserDefaults.standard.removeObject(forKey: self.NEXT_VERSION)
1534
+ }
1535
+ UserDefaults.standard.synchronize()
1536
+ }
1537
+
1538
+ func prepareResetStateForTransition() {
1539
+ self.setCurrentBundle(bundle: "")
1540
+ self.setFallbackBundle(fallback: Optional<BundleInfo>.none)
1541
+ _ = self.setNextBundle(next: Optional<String>.none)
1542
+ }
1543
+
1544
+ func finalizeResetTransition(previousBundleName: String, isInternal: Bool) {
1545
+ if !isInternal {
1546
+ self.sendStats(action: "reset", versionName: self.getCurrentBundle().getVersionName(), oldVersionName: previousBundleName)
1547
+ }
1548
+ }
1549
+
1550
+ func canSet(bundle: BundleInfo) -> Bool {
1551
+ bundle.isBuiltin() || self.bundleExists(id: bundle.getId())
1552
+ }
1553
+
1419
1554
  public func set(bundle: BundleInfo) -> Bool {
1420
1555
  return self.set(id: bundle.getId())
1421
1556
  }
@@ -1453,11 +1588,36 @@ import UIKit
1453
1588
  return false
1454
1589
  }
1455
1590
 
1591
+ func stagePendingReload(bundle: BundleInfo) -> Bool {
1592
+ guard !bundle.isBuiltin(), bundleExists(id: bundle.getId()) else {
1593
+ return false
1594
+ }
1595
+ self.setCurrentBundle(bundle: self.getBundleDirectory(id: bundle.getId()).path)
1596
+ return true
1597
+ }
1598
+
1599
+ func finalizePendingReload(bundle: BundleInfo, previousBundleName: String) {
1600
+ guard !bundle.isBuiltin() else {
1601
+ return
1602
+ }
1603
+ self.sendStats(action: "set", versionName: bundle.getVersionName(), oldVersionName: previousBundleName)
1604
+ }
1605
+
1456
1606
  public func autoReset() {
1457
1607
  let currentBundle: BundleInfo = self.getCurrentBundle()
1458
1608
  if !currentBundle.isBuiltin() && !self.bundleExists(id: currentBundle.getId()) {
1459
1609
  logger.info("Folder at bundle path does not exist. Triggering reset.")
1460
1610
  self.reset()
1611
+ return
1612
+ }
1613
+ let bundlePath = UserDefaults.standard.string(forKey: self.CAP_SERVER_PATH)
1614
+ if Self.shouldResetForForeignBundle(
1615
+ bundlePath: bundlePath,
1616
+ isBuiltin: currentBundle.isBuiltin(),
1617
+ hasStoredBundleInfo: self.hasStoredBundleInfo(id: currentBundle.getId())
1618
+ ) {
1619
+ logger.info("Current bundle id is not one of the bundle ids stored by this plugin. Triggering reset.")
1620
+ self.reset()
1461
1621
  }
1462
1622
  }
1463
1623
 
@@ -1468,12 +1628,8 @@ import UIKit
1468
1628
  public func reset(isInternal: Bool) {
1469
1629
  logger.info("reset: \(isInternal)")
1470
1630
  let currentBundleName = self.getCurrentBundle().getVersionName()
1471
- self.setCurrentBundle(bundle: "")
1472
- self.setFallbackBundle(fallback: Optional<BundleInfo>.none)
1473
- _ = self.setNextBundle(next: Optional<String>.none)
1474
- if !isInternal {
1475
- self.sendStats(action: "reset", versionName: self.getCurrentBundle().getVersionName(), oldVersionName: currentBundleName)
1476
- }
1631
+ self.prepareResetStateForTransition()
1632
+ self.finalizeResetTransition(previousBundleName: currentBundleName, isInternal: isInternal)
1477
1633
  }
1478
1634
 
1479
1635
  public func setSuccess(bundle: BundleInfo, autoDeletePrevious: Bool) {
@@ -1856,7 +2012,7 @@ import UIKit
1856
2012
  }
1857
2013
  let result: BundleInfo
1858
2014
  if BundleInfo.ID_BUILTIN == trueId {
1859
- result = BundleInfo(id: trueId, version: "", status: BundleStatus.SUCCESS, checksum: "")
2015
+ result = BundleInfo(id: trueId, version: self.versionBuild, status: BundleStatus.SUCCESS, checksum: "")
1860
2016
  } else if BundleInfo.VERSION_UNKNOWN == trueId {
1861
2017
  result = BundleInfo(id: trueId, version: "", status: BundleStatus.ERROR, checksum: "")
1862
2018
  } else {
@@ -1959,6 +2115,8 @@ import UIKit
1959
2115
  UserDefaults.standard.set(nextId, forKey: self.NEXT_VERSION)
1960
2116
  UserDefaults.standard.synchronize()
1961
2117
  self.setBundleStatus(id: nextId, status: BundleStatus.PENDING)
2118
+ self.sendStats(action: "set_next", versionName: newBundle.getVersionName(), oldVersionName: self.getCurrentBundle().getVersionName())
2119
+ self.notifyListeners("setNext", ["bundle": newBundle.toJSON()])
1962
2120
  return true
1963
2121
  }
1964
2122
  }
@@ -146,54 +146,48 @@ public struct CryptoCipher {
146
146
  let bufferSize = 1024 * 1024 * 5 // 5 MB
147
147
  var sha256 = SHA256()
148
148
 
149
+ let fileHandle: FileHandle
149
150
  do {
150
- let fileHandle: FileHandle
151
+ fileHandle = try FileHandle(forReadingFrom: filePath)
152
+ } catch {
153
+ logger.error("Cannot open file for checksum calculation")
154
+ logger.debug("Path: \(filePath.path), Error: \(error)")
155
+ return ""
156
+ }
157
+
158
+ defer {
151
159
  do {
152
- fileHandle = try FileHandle(forReadingFrom: filePath)
160
+ try fileHandle.close()
153
161
  } catch {
154
- logger.error("Cannot open file for checksum calculation")
155
- logger.debug("Path: \(filePath.path), Error: \(error)")
156
- return ""
162
+ logger.error("Error closing file during checksum")
163
+ logger.debug("Error: \(error)")
157
164
  }
165
+ }
158
166
 
159
- defer {
167
+ while autoreleasepool(invoking: {
168
+ let fileData: Data
169
+ if #available(iOS 13.4, *) {
160
170
  do {
161
- try fileHandle.close()
171
+ fileData = try fileHandle.read(upToCount: bufferSize) ?? Data()
162
172
  } catch {
163
- logger.error("Error closing file during checksum")
173
+ logger.error("Error reading file during checksum")
164
174
  logger.debug("Error: \(error)")
175
+ return false
165
176
  }
177
+ } else {
178
+ fileData = fileHandle.readData(ofLength: bufferSize)
166
179
  }
167
180
 
168
- while autoreleasepool(invoking: {
169
- let fileData: Data
170
- if #available(iOS 13.4, *) {
171
- do {
172
- fileData = try fileHandle.read(upToCount: bufferSize) ?? Data()
173
- } catch {
174
- logger.error("Error reading file during checksum")
175
- logger.debug("Error: \(error)")
176
- return false
177
- }
178
- } else {
179
- fileData = fileHandle.readData(ofLength: bufferSize)
180
- }
181
-
182
- if fileData.count > 0 {
183
- sha256.update(data: fileData)
184
- return true // Continue
185
- } else {
186
- return false // End of file
187
- }
188
- }) {}
181
+ if fileData.count > 0 {
182
+ sha256.update(data: fileData)
183
+ return true // Continue
184
+ } else {
185
+ return false // End of file
186
+ }
187
+ }) {}
189
188
 
190
- let digest = sha256.finalize()
191
- return digest.compactMap { String(format: "%02x", $0) }.joined()
192
- } catch {
193
- logger.error("Cannot calculate checksum")
194
- logger.debug("Path: \(filePath.path), Error: \(error)")
195
- return ""
196
- }
189
+ let digest = sha256.finalize()
190
+ return digest.compactMap { String(format: "%02x", $0) }.joined()
197
191
  }
198
192
 
199
193
  public static func decryptFile(filePath: URL, publicKey: String, sessionKey: String, version: String) throws {
@@ -97,25 +97,17 @@ public class DelayUpdateUtils {
97
97
 
98
98
  case "date":
99
99
  if let value = value, !value.isEmpty {
100
- do {
101
- let dateFormatter = ISO8601DateFormatter()
102
- dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
103
-
104
- if let date = dateFormatter.date(from: value) {
105
- if Date() > date {
106
- // swiftlint:disable:next line_length
107
- logger.info("Date delay (value: \(value)) condition removed due to expired date at index \(index)")
108
- } else {
109
- delayConditionListToKeep.append(condition)
110
- logger.info("Date delay (value: \(value)) kept at index \(index)")
111
- }
112
- } else {
100
+ if let date = parseDateCondition(value) {
101
+ if Date() > date {
113
102
  // swiftlint:disable:next line_length
114
- logger.error("Date delay (value: \(value)) condition removed due to parsing issue at index \(index)")
103
+ logger.info("Date delay (value: \(value)) condition removed due to expired date at index \(index)")
104
+ } else {
105
+ delayConditionListToKeep.append(condition)
106
+ logger.info("Date delay (value: \(value)) kept at index \(index)")
115
107
  }
116
- } catch {
108
+ } else {
117
109
  // swiftlint:disable:next line_length
118
- logger.error("Date delay (value: \(value)) condition removed due to parsing issue at index \(index): \(error)")
110
+ logger.error("Date delay (value: \(value)) condition removed due to parsing issue at index \(index)")
119
111
  }
120
112
  } else {
121
113
  // swiftlint:disable:next line_length
@@ -216,6 +208,35 @@ public class DelayUpdateUtils {
216
208
 
217
209
  // MARK: - Helper methods
218
210
 
211
+ private func parseDateCondition(_ value: String) -> Date? {
212
+ let withFractionalSeconds = ISO8601DateFormatter()
213
+ withFractionalSeconds.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
214
+ if let date = withFractionalSeconds.date(from: value) {
215
+ return date
216
+ }
217
+
218
+ let withoutFractionalSeconds = ISO8601DateFormatter()
219
+ withoutFractionalSeconds.formatOptions = [.withInternetDateTime]
220
+ if let date = withoutFractionalSeconds.date(from: value) {
221
+ return date
222
+ }
223
+
224
+ // Legacy fallback for strings without timezone.
225
+ for format in ["yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'HH:mm:ss"] {
226
+ let formatter = DateFormatter()
227
+ formatter.locale = Locale(identifier: "en_US_POSIX")
228
+ formatter.calendar = Calendar(identifier: .gregorian)
229
+ formatter.timeZone = .current
230
+ formatter.isLenient = false
231
+ formatter.dateFormat = format
232
+ if let date = formatter.date(from: value) {
233
+ return date
234
+ }
235
+ }
236
+
237
+ return nil
238
+ }
239
+
219
240
  private func toJson(object: Any) -> String {
220
241
  guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else {
221
242
  return ""
@@ -186,6 +186,7 @@ struct AppVersionDec: Decodable {
186
186
  let url: String?
187
187
  let message: String?
188
188
  let error: String?
189
+ let kind: String?
189
190
  let session_key: String?
190
191
  let major: Bool?
191
192
  let breaking: Bool?
@@ -203,6 +204,7 @@ public class AppVersion: NSObject {
203
204
  var url: String = ""
204
205
  var message: String?
205
206
  var error: String?
207
+ var kind: String?
206
208
  var sessionKey: String?
207
209
  var major: Bool?
208
210
  var breaking: Bool?
@@ -308,19 +308,36 @@ extension UIWindow {
308
308
  }
309
309
 
310
310
  let latest = updater.getLatest(url: updateUrl, channel: name)
311
+ let latestKind = latest.kind
312
+
313
+ let detail = [latest.message, latest.error, latestKind]
314
+ .compactMap { value in
315
+ guard let value, !value.isEmpty else { return nil }
316
+ return value
317
+ }
318
+ .first ?? "server did not provide a message"
311
319
 
312
320
  // Handle update errors first (before "no new version" check)
313
- if let error = latest.error, !error.isEmpty && error != "no_new_version_available" {
321
+ if latestKind == "failed" || (latest.error?.isEmpty == false && latestKind != "up_to_date" && latestKind != "blocked") {
322
+ DispatchQueue.main.async {
323
+ progressAlert.dismiss(animated: true) {
324
+ self.showError(message: "Channel set to \(name). Update check failed: \(detail)", plugin: plugin)
325
+ }
326
+ }
327
+ return
328
+ }
329
+
330
+ if latestKind == "blocked" {
314
331
  DispatchQueue.main.async {
315
332
  progressAlert.dismiss(animated: true) {
316
- self.showError(message: "Channel set to \(name). Update check failed: \(error)", plugin: plugin)
333
+ self.showError(message: "Channel set to \(name). Update check blocked: \(detail)", plugin: plugin)
317
334
  }
318
335
  }
319
336
  return
320
337
  }
321
338
 
322
339
  // Check if there's an actual update available
323
- if latest.error == "no_new_version_available" || latest.url.isEmpty {
340
+ if latestKind == "up_to_date" || latest.url.isEmpty {
324
341
  DispatchQueue.main.async {
325
342
  progressAlert.dismiss(animated: true) {
326
343
  self.showSuccess(message: "Channel set to \(name). Already on latest version.", plugin: plugin)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "6.43.4",
3
+ "version": "6.45.10",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",
@@ -41,23 +41,26 @@
41
41
  "native"
42
42
  ],
43
43
  "scripts": {
44
- "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
45
- "verify:ios": "TMP_DERIVED_DATA=$(mktemp -d \"${TMPDIR:-/tmp}/capgo-updater-ios-verify.XXXXXX\") && xcodebuild -scheme CapgoCapacitorUpdater -destination generic/platform=iOS -derivedDataPath \"$TMP_DERIVED_DATA\" SWIFT_ENABLE_EXPLICIT_MODULES=NO; EXIT_CODE=$?; rm -rf \"$TMP_DERIVED_DATA\"; exit $EXIT_CODE",
44
+ "verify": "bun run verify:ios && bun run verify:android && bun run verify:web",
45
+ "verify:ios": "xcodebuild -scheme CapgoCapacitorUpdater -destination generic/platform=iOS",
46
46
  "verify:android": "cd android && ./gradlew clean build test && cd ..",
47
- "verify:web": "npm run build",
48
- "test": "npm run test:ios && npm run test:android",
47
+ "verify:web": "bun run build",
48
+ "test": "bun run test:ios && bun run test:android",
49
49
  "test:ios": "./scripts/test-ios.sh",
50
50
  "test:android": "cd android && ./gradlew test && cd ..",
51
- "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
52
- "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
51
+ "test:maestro": "./scripts/maestro/run-android-live-update.sh",
52
+ "test:maestro:android": "./scripts/test-maestro-android.sh",
53
+ "test:maestro:ios": "./scripts/test-maestro-ios.sh",
54
+ "lint": "bun run eslint && bun run prettier -- --check && bun run swiftlint -- lint",
55
+ "fmt": "bun run eslint -- --fix && bun run prettier -- --write && bun run swiftlint -- --fix --format",
53
56
  "eslint": "eslint . --ext .ts",
54
57
  "prettier": "prettier-pretty-check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
55
58
  "swiftlint": "node-swiftlint",
56
59
  "docgen": "node scripts/generate-docs.js",
57
- "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
60
+ "build": "bun run clean && bun run docgen && tsc && rollup -c rollup.config.mjs",
58
61
  "clean": "rimraf ./dist",
59
62
  "watch": "tsc --watch",
60
- "prepublishOnly": "npm run build",
63
+ "prepublishOnly": "bun run build",
61
64
  "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs"
62
65
  },
63
66
  "devDependencies": {