@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.
- package/Package.swift +5 -2
- package/README.md +151 -41
- package/android/build.gradle +3 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +537 -172
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +173 -43
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +49 -13
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +38 -13
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +49 -9
- package/dist/docs.json +290 -10
- package/dist/esm/definitions.d.ts +134 -22
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +1 -2
- package/dist/esm/web.js +0 -4
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +0 -4
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +0 -4
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +558 -140
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +213 -55
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +30 -36
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +37 -16
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +2 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +20 -3
- package/package.json +12 -9
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
1472
|
-
self.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
160
|
+
try fileHandle.close()
|
|
153
161
|
} catch {
|
|
154
|
-
logger.error("
|
|
155
|
-
logger.debug("
|
|
156
|
-
return ""
|
|
162
|
+
logger.error("Error closing file during checksum")
|
|
163
|
+
logger.debug("Error: \(error)")
|
|
157
164
|
}
|
|
165
|
+
}
|
|
158
166
|
|
|
159
|
-
|
|
167
|
+
while autoreleasepool(invoking: {
|
|
168
|
+
let fileData: Data
|
|
169
|
+
if #available(iOS 13.4, *) {
|
|
160
170
|
do {
|
|
161
|
-
try fileHandle.
|
|
171
|
+
fileData = try fileHandle.read(upToCount: bufferSize) ?? Data()
|
|
162
172
|
} catch {
|
|
163
|
-
logger.error("Error
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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.
|
|
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
|
-
}
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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": "
|
|
45
|
-
"verify:ios": "
|
|
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": "
|
|
48
|
-
"test": "
|
|
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
|
-
"
|
|
52
|
-
"
|
|
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": "
|
|
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": "
|
|
63
|
+
"prepublishOnly": "bun run build",
|
|
61
64
|
"check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs"
|
|
62
65
|
},
|
|
63
66
|
"devDependencies": {
|