@capgo/capacitor-updater 8.51.1 → 8.51.2
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/README.md +49 -46
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +111 -7
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +0 -1
- package/dist/docs.json +18 -2
- package/dist/esm/definitions.d.ts +22 -6
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +506 -22
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +2 -3
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +8 -0
- package/package.json +1 -1
|
@@ -16,6 +16,13 @@ import Version
|
|
|
16
16
|
*/
|
|
17
17
|
@objc(CapacitorUpdaterPlugin)
|
|
18
18
|
public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
19
|
+
enum DefaultChannelPreviewSnapshot: Equatable {
|
|
20
|
+
case missing
|
|
21
|
+
case invalidated
|
|
22
|
+
case snapshot(String?)
|
|
23
|
+
case unreadable
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
lazy var logger: Logger = {
|
|
20
27
|
// Default to true for OS logging. In test environments without a bridge,
|
|
21
28
|
// this will default to true. In production, it reads from config.
|
|
@@ -85,7 +92,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
85
92
|
CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
|
|
86
93
|
]
|
|
87
94
|
public var implementation = CapgoUpdater()
|
|
88
|
-
private let pluginVersion: String = "8.51.
|
|
95
|
+
private let pluginVersion: String = "8.51.2"
|
|
89
96
|
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
90
97
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
91
98
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
@@ -124,6 +131,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
124
131
|
private let previewSourceDefaultsKey = "CapacitorUpdater.previewSource"
|
|
125
132
|
private let previewSessionsDefaultsKey = "CapacitorUpdater.previewSessions"
|
|
126
133
|
private let previewSessionAlertPendingDefaultsKey = "CapacitorUpdater.previewSessionAlertPending"
|
|
134
|
+
private let defaultChannelInstallMarkerDefaultsKey = "CapacitorUpdater.defaultChannelInstallMarkerCreated"
|
|
135
|
+
private let defaultChannelInstallMarkerFilename = "CapacitorUpdater.defaultChannelInstallMarker"
|
|
136
|
+
private let defaultChannelStateFilename = "CapacitorUpdater.defaultChannelState"
|
|
137
|
+
private let defaultChannelPreviewSnapshotFilename = "CapacitorUpdater.defaultChannelPreviewSnapshot"
|
|
127
138
|
private let previewDeepLinkScheme = "capgo"
|
|
128
139
|
private let previewDeepLinkRootComponent = "preview"
|
|
129
140
|
private let previewDeepLinkChannelComponent = "channel"
|
|
@@ -163,6 +174,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
163
174
|
private var autoDeleteFailed = false
|
|
164
175
|
private var autoDeletePrevious = false
|
|
165
176
|
var allowSetDefaultChannel = true
|
|
177
|
+
var persistDefaultChannelOnReinstall = true
|
|
166
178
|
private var keepUrlPathAfterReload = false
|
|
167
179
|
private var backgroundWork: DispatchWorkItem?
|
|
168
180
|
private var taskRunning = false
|
|
@@ -175,9 +187,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
175
187
|
|
|
176
188
|
// Lock to ensure cleanup completes before downloads start
|
|
177
189
|
private let cleanupLock = NSLock()
|
|
190
|
+
private let defaultChannelStateLock = NSLock()
|
|
178
191
|
private let cleanupGroup = DispatchGroup()
|
|
179
192
|
private var cleanupComplete = false
|
|
180
193
|
private var cleanupThread: Thread?
|
|
194
|
+
private var defaultChannelCleanupMustRetry = false
|
|
181
195
|
private var persistCustomId = false
|
|
182
196
|
private var persistModifyUrl = false
|
|
183
197
|
private var allowManualBundleError = false
|
|
@@ -223,6 +237,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
223
237
|
self.implementation.deviceID = DeviceIdHelper.getOrCreateDeviceId()
|
|
224
238
|
persistCustomId = getConfig().getBoolean("persistCustomId", false)
|
|
225
239
|
allowSetDefaultChannel = getConfig().getBoolean("allowSetDefaultChannel", true)
|
|
240
|
+
persistDefaultChannelOnReinstall = getConfig().getBoolean("persistDefaultChannelOnReinstall", true)
|
|
226
241
|
if persistCustomId {
|
|
227
242
|
let storedCustomId = UserDefaults.standard.string(forKey: customIdDefaultsKey) ?? ""
|
|
228
243
|
if !storedCustomId.isEmpty {
|
|
@@ -339,12 +354,57 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
339
354
|
}
|
|
340
355
|
}
|
|
341
356
|
|
|
342
|
-
|
|
343
|
-
|
|
357
|
+
let nativeBuildVersionChanged = self.hasNativeBuildVersionChanged()
|
|
358
|
+
let defaultChannelPersistenceDisabled = !persistDefaultChannelOnReinstall
|
|
359
|
+
let restoredReinstall = defaultChannelPersistenceDisabled && self.isRestoredReinstall()
|
|
360
|
+
var installMarkerCanBePrepared = true
|
|
361
|
+
if shouldClearPersistedDefaultChannel(
|
|
362
|
+
nativeBuildVersionChanged: nativeBuildVersionChanged,
|
|
363
|
+
resetWhenUpdate: resetWhenUpdate,
|
|
364
|
+
restoredReinstall: restoredReinstall
|
|
365
|
+
) {
|
|
366
|
+
installMarkerCanBePrepared = clearPersistedDefaultChannel()
|
|
367
|
+
if installMarkerCanBePrepared {
|
|
368
|
+
logger.info("Cleared persisted defaultChannel because reinstall persistence is disabled")
|
|
369
|
+
} else {
|
|
370
|
+
logger.warn("Cannot durably clear persisted defaultChannel")
|
|
371
|
+
self.defaultChannelCleanupMustRetry = true
|
|
372
|
+
self.invalidateDefaultChannelInstallMarker()
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if defaultChannelPersistenceDisabled && installMarkerCanBePrepared {
|
|
376
|
+
self.prepareDefaultChannelInstallMarker()
|
|
377
|
+
}
|
|
378
|
+
if !previewSessionEnabled,
|
|
379
|
+
!defaultChannelCleanupMustRetry,
|
|
380
|
+
self.hasPendingDefaultChannelPreviewSnapshot(),
|
|
381
|
+
!self.restorePreviewPreviousDefaultChannel() {
|
|
382
|
+
logger.warn("Default channel preview restore remains pending")
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let configDefaultChannel = getConfig().getString("defaultChannel", "")!
|
|
386
|
+
let storedDefaultChannel = UserDefaults.standard.string(forKey: defaultChannelDefaultsKey)
|
|
387
|
+
let stateFile = self.defaultChannelStateFile()
|
|
388
|
+
let state = stateFile.map { self.defaultChannelState(file: $0) }
|
|
389
|
+
if defaultChannelCleanupMustRetry {
|
|
390
|
+
implementation.defaultChannel = configDefaultChannel
|
|
391
|
+
logger.warn("Using configured defaultChannel until persisted cleanup can retry")
|
|
392
|
+
} else if let state = state, state.exists, state.isReadable {
|
|
393
|
+
self.reconcileDefaultChannelDefaults(with: state.channel)
|
|
394
|
+
implementation.defaultChannel = state.channel ?? configDefaultChannel
|
|
395
|
+
logger.info("Loaded persisted defaultChannel from local state")
|
|
396
|
+
} else if defaultChannelPersistenceDisabled, state?.exists == true {
|
|
397
|
+
implementation.defaultChannel = configDefaultChannel
|
|
398
|
+
logger.warn("Ignoring unreadable persisted defaultChannel while reinstall persistence is disabled")
|
|
399
|
+
} else if let storedDefaultChannel = storedDefaultChannel {
|
|
344
400
|
implementation.defaultChannel = storedDefaultChannel
|
|
345
401
|
logger.info("Loaded persisted defaultChannel from setChannel()")
|
|
346
402
|
} else {
|
|
347
|
-
implementation.defaultChannel =
|
|
403
|
+
implementation.defaultChannel = configDefaultChannel
|
|
404
|
+
}
|
|
405
|
+
if !defaultChannelCleanupMustRetry,
|
|
406
|
+
state?.exists != true || (!defaultChannelPersistenceDisabled && state?.isReadable != true) {
|
|
407
|
+
_ = self.persistDefaultChannelStateFromDefaults()
|
|
348
408
|
}
|
|
349
409
|
self.reportAppLaunchStart()
|
|
350
410
|
self.implementation.autoReset()
|
|
@@ -355,7 +415,6 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
355
415
|
|
|
356
416
|
// Check if app was recently installed/updated BEFORE cleanup updates the stored native build version.
|
|
357
417
|
self.wasRecentlyInstalledOrUpdated = self.checkIfRecentlyInstalledOrUpdated()
|
|
358
|
-
let nativeBuildVersionChanged = self.hasNativeBuildVersionChanged()
|
|
359
418
|
if nativeBuildVersionChanged {
|
|
360
419
|
self.clearPreviewSessionForNativeBuildChange()
|
|
361
420
|
}
|
|
@@ -551,6 +610,327 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
551
610
|
UserDefaults.standard.string(forKey: "LatestNativeBuildVersion") ?? UserDefaults.standard.string(forKey: "LatestVersionNative") ?? "0"
|
|
552
611
|
}
|
|
553
612
|
|
|
613
|
+
func shouldClearPersistedDefaultChannel(
|
|
614
|
+
nativeBuildVersionChanged: Bool,
|
|
615
|
+
resetWhenUpdate: Bool,
|
|
616
|
+
restoredReinstall: Bool
|
|
617
|
+
) -> Bool {
|
|
618
|
+
!persistDefaultChannelOnReinstall && (restoredReinstall || (resetWhenUpdate && nativeBuildVersionChanged))
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
func clearPersistedDefaultChannel() -> Bool {
|
|
622
|
+
guard let stateFile = self.defaultChannelStateFile(),
|
|
623
|
+
let previewSnapshotFile = self.defaultChannelPreviewSnapshotFile() else {
|
|
624
|
+
return false
|
|
625
|
+
}
|
|
626
|
+
return self.clearPersistedDefaultChannel(
|
|
627
|
+
stateFile: stateFile,
|
|
628
|
+
previewSnapshotFile: previewSnapshotFile
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
func clearPersistedDefaultChannel(stateFile: URL, previewSnapshotFile: URL) -> Bool {
|
|
633
|
+
defaultChannelStateLock.lock()
|
|
634
|
+
defer { defaultChannelStateLock.unlock() }
|
|
635
|
+
do {
|
|
636
|
+
try self.persistDefaultChannelStateWithoutLock(channel: nil, file: stateFile)
|
|
637
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
638
|
+
channel: nil,
|
|
639
|
+
isValid: false,
|
|
640
|
+
file: previewSnapshotFile
|
|
641
|
+
)
|
|
642
|
+
UserDefaults.standard.removeObject(forKey: defaultChannelDefaultsKey)
|
|
643
|
+
UserDefaults.standard.removeObject(forKey: previewPreviousDefaultChannelDefaultsKey)
|
|
644
|
+
UserDefaults.standard.removeObject(forKey: previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
645
|
+
return true
|
|
646
|
+
} catch {
|
|
647
|
+
logger.warn("Cannot persist cleared default channel state: \(error.localizedDescription)")
|
|
648
|
+
return false
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private func defaultChannelStateFile() -> URL? {
|
|
653
|
+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
654
|
+
.appendingPathComponent(defaultChannelStateFilename)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private func defaultChannelPreviewSnapshotFile() -> URL? {
|
|
658
|
+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
659
|
+
.appendingPathComponent(defaultChannelPreviewSnapshotFilename)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
func defaultChannelState(file: URL) -> (exists: Bool, channel: String?, isReadable: Bool) {
|
|
663
|
+
defaultChannelStateLock.lock()
|
|
664
|
+
defer { defaultChannelStateLock.unlock() }
|
|
665
|
+
return self.defaultChannelStateWithoutLock(file: file)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private func defaultChannelStateWithoutLock(file: URL) -> (exists: Bool, channel: String?, isReadable: Bool) {
|
|
669
|
+
let fileManager = FileManager.default
|
|
670
|
+
guard fileManager.fileExists(atPath: file.path) else {
|
|
671
|
+
return (false, nil, true)
|
|
672
|
+
}
|
|
673
|
+
do {
|
|
674
|
+
let data = try Data(contentsOf: file)
|
|
675
|
+
guard !data.isEmpty else {
|
|
676
|
+
return (true, nil, true)
|
|
677
|
+
}
|
|
678
|
+
guard let channel = String(data: data, encoding: .utf8) else {
|
|
679
|
+
logger.warn("Cannot decode persisted default channel state")
|
|
680
|
+
return (true, nil, false)
|
|
681
|
+
}
|
|
682
|
+
return (true, channel, true)
|
|
683
|
+
} catch {
|
|
684
|
+
logger.warn("Cannot read persisted default channel state: \(error.localizedDescription)")
|
|
685
|
+
return (true, nil, false)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
func persistDefaultChannelState(channel: String?, file: URL) throws {
|
|
690
|
+
defaultChannelStateLock.lock()
|
|
691
|
+
defer { defaultChannelStateLock.unlock() }
|
|
692
|
+
try self.persistDefaultChannelStateWithoutLock(channel: channel, file: file)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private func persistDefaultChannelStateWithoutLock(channel: String?, file: URL) throws {
|
|
696
|
+
let data = channel.map { Data($0.utf8) } ?? Data()
|
|
697
|
+
try self.persistBackupExcludedFile(file: file, data: data, overwrite: true)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
func defaultChannelPreviewSnapshot(file: URL) -> DefaultChannelPreviewSnapshot {
|
|
701
|
+
defaultChannelStateLock.lock()
|
|
702
|
+
defer { defaultChannelStateLock.unlock() }
|
|
703
|
+
return self.defaultChannelPreviewSnapshotWithoutLock(file: file)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private func defaultChannelPreviewSnapshotWithoutLock(file: URL) -> DefaultChannelPreviewSnapshot {
|
|
707
|
+
guard FileManager.default.fileExists(atPath: file.path) else {
|
|
708
|
+
return .missing
|
|
709
|
+
}
|
|
710
|
+
do {
|
|
711
|
+
let data = try Data(contentsOf: file)
|
|
712
|
+
guard let kind = data.first else {
|
|
713
|
+
return .unreadable
|
|
714
|
+
}
|
|
715
|
+
switch kind {
|
|
716
|
+
case 0 where data.count == 1:
|
|
717
|
+
return .invalidated
|
|
718
|
+
case 1 where data.count == 1:
|
|
719
|
+
return .snapshot(nil)
|
|
720
|
+
case 2:
|
|
721
|
+
guard let channel = String(data: Data(data.dropFirst()), encoding: .utf8) else {
|
|
722
|
+
return .unreadable
|
|
723
|
+
}
|
|
724
|
+
return .snapshot(channel)
|
|
725
|
+
default:
|
|
726
|
+
return .unreadable
|
|
727
|
+
}
|
|
728
|
+
} catch {
|
|
729
|
+
logger.warn("Cannot read default channel preview snapshot: \(error.localizedDescription)")
|
|
730
|
+
return .unreadable
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
func persistDefaultChannelPreviewSnapshot(channel: String?, file: URL) throws {
|
|
735
|
+
defaultChannelStateLock.lock()
|
|
736
|
+
defer { defaultChannelStateLock.unlock() }
|
|
737
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
738
|
+
channel: channel,
|
|
739
|
+
isValid: true,
|
|
740
|
+
file: file
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
func invalidateDefaultChannelPreviewSnapshot(file: URL) throws {
|
|
745
|
+
defaultChannelStateLock.lock()
|
|
746
|
+
defer { defaultChannelStateLock.unlock() }
|
|
747
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
748
|
+
channel: nil,
|
|
749
|
+
isValid: false,
|
|
750
|
+
file: file
|
|
751
|
+
)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private func persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
755
|
+
channel: String?,
|
|
756
|
+
isValid: Bool,
|
|
757
|
+
file: URL
|
|
758
|
+
) throws {
|
|
759
|
+
var data = Data()
|
|
760
|
+
if !isValid {
|
|
761
|
+
data.append(0)
|
|
762
|
+
} else if let channel = channel {
|
|
763
|
+
data.append(2)
|
|
764
|
+
data.append(contentsOf: channel.utf8)
|
|
765
|
+
} else {
|
|
766
|
+
data.append(1)
|
|
767
|
+
}
|
|
768
|
+
try self.persistBackupExcludedFile(file: file, data: data, overwrite: true)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private func persistDefaultChannelPreviewSnapshot(channel: String?) -> Bool {
|
|
772
|
+
guard let file = self.defaultChannelPreviewSnapshotFile() else {
|
|
773
|
+
return false
|
|
774
|
+
}
|
|
775
|
+
do {
|
|
776
|
+
try self.persistDefaultChannelPreviewSnapshot(channel: channel, file: file)
|
|
777
|
+
return true
|
|
778
|
+
} catch {
|
|
779
|
+
logger.warn("Cannot persist default channel preview snapshot: \(error.localizedDescription)")
|
|
780
|
+
return false
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private func hasPendingDefaultChannelPreviewSnapshot() -> Bool {
|
|
785
|
+
guard let file = self.defaultChannelPreviewSnapshotFile() else {
|
|
786
|
+
return false
|
|
787
|
+
}
|
|
788
|
+
switch self.defaultChannelPreviewSnapshot(file: file) {
|
|
789
|
+
case .snapshot, .unreadable:
|
|
790
|
+
return true
|
|
791
|
+
case .missing, .invalidated:
|
|
792
|
+
return false
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
@discardableResult
|
|
797
|
+
func persistDefaultChannelStateFromDefaults() -> Bool {
|
|
798
|
+
guard let file = self.defaultChannelStateFile() else {
|
|
799
|
+
logger.warn("Cannot locate default channel state file")
|
|
800
|
+
return false
|
|
801
|
+
}
|
|
802
|
+
return self.persistDefaultChannelStateFromDefaults(stateFile: file)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
@discardableResult
|
|
806
|
+
func persistDefaultChannelStateFromDefaults(stateFile: URL) -> Bool {
|
|
807
|
+
defaultChannelStateLock.lock()
|
|
808
|
+
defer { defaultChannelStateLock.unlock() }
|
|
809
|
+
do {
|
|
810
|
+
try self.persistDefaultChannelStateWithoutLock(
|
|
811
|
+
channel: UserDefaults.standard.string(forKey: defaultChannelDefaultsKey),
|
|
812
|
+
file: stateFile
|
|
813
|
+
)
|
|
814
|
+
return true
|
|
815
|
+
} catch {
|
|
816
|
+
do {
|
|
817
|
+
if FileManager.default.fileExists(atPath: stateFile.path) {
|
|
818
|
+
try FileManager.default.removeItem(at: stateFile)
|
|
819
|
+
}
|
|
820
|
+
logger.warn("Cannot persist default channel state; falling back to UserDefaults: \(error.localizedDescription)")
|
|
821
|
+
return true
|
|
822
|
+
} catch let invalidationError {
|
|
823
|
+
logger.warn("Cannot persist or invalidate default channel state: \(invalidationError.localizedDescription)")
|
|
824
|
+
return false
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private func persistedDefaultChannel() -> String? {
|
|
830
|
+
guard let file = self.defaultChannelStateFile() else {
|
|
831
|
+
return UserDefaults.standard.string(forKey: defaultChannelDefaultsKey)
|
|
832
|
+
}
|
|
833
|
+
return self.persistedDefaultChannel(stateFile: file)
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
func persistedDefaultChannel(stateFile: URL) -> String? {
|
|
837
|
+
let state = self.defaultChannelState(file: stateFile)
|
|
838
|
+
if state.exists {
|
|
839
|
+
if state.isReadable {
|
|
840
|
+
return state.channel
|
|
841
|
+
}
|
|
842
|
+
if !persistDefaultChannelOnReinstall {
|
|
843
|
+
return nil
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return UserDefaults.standard.string(forKey: defaultChannelDefaultsKey)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private func reconcileDefaultChannelDefaults(with channel: String?) {
|
|
850
|
+
if let channel = channel {
|
|
851
|
+
UserDefaults.standard.set(channel, forKey: defaultChannelDefaultsKey)
|
|
852
|
+
} else {
|
|
853
|
+
UserDefaults.standard.removeObject(forKey: defaultChannelDefaultsKey)
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private func defaultChannelInstallMarker() -> URL? {
|
|
858
|
+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
859
|
+
.appendingPathComponent(defaultChannelInstallMarkerFilename)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private func invalidateDefaultChannelInstallMarker() {
|
|
863
|
+
guard let marker = self.defaultChannelInstallMarker() else {
|
|
864
|
+
return
|
|
865
|
+
}
|
|
866
|
+
do {
|
|
867
|
+
try self.invalidateDefaultChannelInstallMarker(marker: marker)
|
|
868
|
+
} catch {
|
|
869
|
+
logger.warn("Cannot invalidate default channel install marker for cleanup retry: \(error.localizedDescription)")
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
func invalidateDefaultChannelInstallMarker(marker: URL) throws {
|
|
874
|
+
let fileManager = FileManager.default
|
|
875
|
+
if fileManager.fileExists(atPath: marker.path) {
|
|
876
|
+
try fileManager.removeItem(at: marker)
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
func isRestoredReinstall() -> Bool {
|
|
881
|
+
guard let marker = self.defaultChannelInstallMarker() else {
|
|
882
|
+
return false
|
|
883
|
+
}
|
|
884
|
+
return self.isRestoredReinstall(
|
|
885
|
+
marker: marker,
|
|
886
|
+
markerWasCreated: UserDefaults.standard.bool(forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
887
|
+
)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
func isRestoredReinstall(marker: URL, markerWasCreated: Bool) -> Bool {
|
|
891
|
+
markerWasCreated && !FileManager.default.fileExists(atPath: marker.path)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
private func prepareDefaultChannelInstallMarker() {
|
|
895
|
+
guard let marker = self.defaultChannelInstallMarker() else {
|
|
896
|
+
return
|
|
897
|
+
}
|
|
898
|
+
self.prepareDefaultChannelInstallMarker(
|
|
899
|
+
marker: marker,
|
|
900
|
+
markerWasCreated: UserDefaults.standard.bool(forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
901
|
+
)
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
func prepareDefaultChannelInstallMarker(marker: URL, markerWasCreated: Bool) {
|
|
905
|
+
do {
|
|
906
|
+
try self.persistBackupExcludedFile(file: marker, data: Data())
|
|
907
|
+
if !markerWasCreated {
|
|
908
|
+
UserDefaults.standard.set(true, forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
909
|
+
UserDefaults.standard.synchronize()
|
|
910
|
+
}
|
|
911
|
+
} catch {
|
|
912
|
+
try? FileManager.default.removeItem(at: marker)
|
|
913
|
+
UserDefaults.standard.set(false, forKey: defaultChannelInstallMarkerDefaultsKey)
|
|
914
|
+
UserDefaults.standard.synchronize()
|
|
915
|
+
logger.warn("Cannot prepare default channel install marker: \(error.localizedDescription)")
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
private func persistBackupExcludedFile(file: URL, data: Data, overwrite: Bool = false) throws {
|
|
920
|
+
let fileManager = FileManager.default
|
|
921
|
+
if overwrite || !fileManager.fileExists(atPath: file.path) {
|
|
922
|
+
try fileManager.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
923
|
+
try data.write(to: file, options: .atomic)
|
|
924
|
+
}
|
|
925
|
+
let currentResourceValues = try file.resourceValues(forKeys: [.isExcludedFromBackupKey])
|
|
926
|
+
if currentResourceValues.isExcludedFromBackup != true {
|
|
927
|
+
var resourceValues = URLResourceValues()
|
|
928
|
+
resourceValues.isExcludedFromBackup = true
|
|
929
|
+
var mutableFile = file
|
|
930
|
+
try mutableFile.setResourceValues(resourceValues)
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
554
934
|
func hasNativeBuildVersionChanged() -> Bool {
|
|
555
935
|
let previous = self.storedNativeBuildVersion()
|
|
556
936
|
return previous != "0" && self.currentBuildVersion != previous
|
|
@@ -730,8 +1110,12 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
730
1110
|
self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds)
|
|
731
1111
|
self.implementation.cleanupOrphanedTempFolders(threadToCheck: nil)
|
|
732
1112
|
|
|
733
|
-
|
|
734
|
-
|
|
1113
|
+
if self.defaultChannelCleanupMustRetry {
|
|
1114
|
+
self.logger.warn("Keeping the previous native build version so default channel cleanup retries")
|
|
1115
|
+
} else {
|
|
1116
|
+
UserDefaults.standard.set(self.currentBuildVersion, forKey: "LatestNativeBuildVersion")
|
|
1117
|
+
UserDefaults.standard.synchronize()
|
|
1118
|
+
}
|
|
735
1119
|
}
|
|
736
1120
|
cleanupThread?.start()
|
|
737
1121
|
}
|
|
@@ -1517,8 +1901,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1517
1901
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
|
|
1518
1902
|
}
|
|
1519
1903
|
|
|
1904
|
+
let previousDefaultChannel = self.persistedDefaultChannel()
|
|
1905
|
+
guard self.persistDefaultChannelPreviewSnapshot(channel: previousDefaultChannel) else {
|
|
1906
|
+
logger.error("Could not durably save the default channel preview snapshot")
|
|
1907
|
+
return false
|
|
1908
|
+
}
|
|
1520
1909
|
UserDefaults.standard.set(self.implementation.appId, forKey: self.previewPreviousAppIdDefaultsKey)
|
|
1521
|
-
if let previousDefaultChannel =
|
|
1910
|
+
if let previousDefaultChannel = previousDefaultChannel {
|
|
1522
1911
|
UserDefaults.standard.set(previousDefaultChannel, forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
1523
1912
|
UserDefaults.standard.set(true, forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
1524
1913
|
} else {
|
|
@@ -1966,7 +2355,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1966
2355
|
?? self.getBooleanConfig("allowShakeChannelSelector", defaultValue: false)
|
|
1967
2356
|
self.restorePreviewPreviousNextBundle()
|
|
1968
2357
|
self.restorePreviewPreviousAppId()
|
|
1969
|
-
self.restorePreviewPreviousDefaultChannel()
|
|
2358
|
+
if !self.restorePreviewPreviousDefaultChannel() {
|
|
2359
|
+
logger.warn("Default channel preview restore will retry on next launch")
|
|
2360
|
+
}
|
|
1970
2361
|
|
|
1971
2362
|
self.previewSessionEnabled = false
|
|
1972
2363
|
self.previewSessionAlertPending = false
|
|
@@ -1993,7 +2384,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1993
2384
|
|
|
1994
2385
|
self.restorePreviewPreviousNextBundle()
|
|
1995
2386
|
self.restorePreviewPreviousAppId()
|
|
1996
|
-
self.restorePreviewPreviousDefaultChannel()
|
|
2387
|
+
if !self.restorePreviewPreviousDefaultChannel() {
|
|
2388
|
+
logger.warn("Default channel preview restore will retry on next launch")
|
|
2389
|
+
}
|
|
1997
2390
|
self.previewSessionEnabled = false
|
|
1998
2391
|
self.previewSessionAlertPending = false
|
|
1999
2392
|
self.isLeavingPreviewForIncomingLink = false
|
|
@@ -2021,14 +2414,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2021
2414
|
}
|
|
2022
2415
|
|
|
2023
2416
|
private func clearPreviewSessionPreferences() {
|
|
2417
|
+
let keepDefaultChannelSnapshotFallback = self.hasPendingDefaultChannelPreviewSnapshot()
|
|
2024
2418
|
_ = self.implementation.setPreviewFallbackBundle(fallback: nil)
|
|
2025
2419
|
UserDefaults.standard.removeObject(forKey: self.previewSessionDefaultsKey)
|
|
2026
2420
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeMenuDefaultsKey)
|
|
2027
2421
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousShakeChannelSelectorDefaultsKey)
|
|
2028
2422
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousNextBundleDefaultsKey)
|
|
2029
2423
|
UserDefaults.standard.removeObject(forKey: self.previewPreviousAppIdDefaultsKey)
|
|
2030
|
-
|
|
2031
|
-
|
|
2424
|
+
if !keepDefaultChannelSnapshotFallback {
|
|
2425
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
2426
|
+
UserDefaults.standard.removeObject(forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey)
|
|
2427
|
+
}
|
|
2032
2428
|
UserDefaults.standard.removeObject(forKey: self.previewAppIdDefaultsKey)
|
|
2033
2429
|
UserDefaults.standard.removeObject(forKey: self.previewPayloadUrlDefaultsKey)
|
|
2034
2430
|
UserDefaults.standard.removeObject(forKey: self.previewNameDefaultsKey)
|
|
@@ -2046,21 +2442,95 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2046
2442
|
logger.info("Restored appId after preview: \(previousAppId)")
|
|
2047
2443
|
}
|
|
2048
2444
|
|
|
2049
|
-
private func restorePreviewPreviousDefaultChannel() {
|
|
2050
|
-
|
|
2051
|
-
|
|
2445
|
+
private func restorePreviewPreviousDefaultChannel() -> Bool {
|
|
2446
|
+
guard !defaultChannelCleanupMustRetry else {
|
|
2447
|
+
logger.warn("Skipping default channel preview restore until cleanup retries")
|
|
2448
|
+
return false
|
|
2449
|
+
}
|
|
2450
|
+
guard let stateFile = self.defaultChannelStateFile(),
|
|
2451
|
+
let previewSnapshotFile = self.defaultChannelPreviewSnapshotFile() else {
|
|
2452
|
+
return false
|
|
2453
|
+
}
|
|
2052
2454
|
|
|
2053
|
-
|
|
2054
|
-
|
|
2455
|
+
let previousDefaultChannel: String?
|
|
2456
|
+
switch self.defaultChannelPreviewSnapshot(file: previewSnapshotFile) {
|
|
2457
|
+
case .invalidated:
|
|
2458
|
+
return true
|
|
2459
|
+
case let .snapshot(channel):
|
|
2460
|
+
previousDefaultChannel = channel
|
|
2461
|
+
case .missing:
|
|
2462
|
+
let hadPreviousDefaultChannel = UserDefaults.standard.object(
|
|
2463
|
+
forKey: self.previewPreviousDefaultChannelWasSetDefaultsKey
|
|
2464
|
+
) as? Bool ?? false
|
|
2465
|
+
previousDefaultChannel = hadPreviousDefaultChannel
|
|
2466
|
+
? UserDefaults.standard.string(forKey: self.previewPreviousDefaultChannelDefaultsKey)
|
|
2467
|
+
: nil
|
|
2468
|
+
do {
|
|
2469
|
+
try self.persistDefaultChannelPreviewSnapshot(
|
|
2470
|
+
channel: previousDefaultChannel,
|
|
2471
|
+
file: previewSnapshotFile
|
|
2472
|
+
)
|
|
2473
|
+
} catch {
|
|
2474
|
+
logger.warn("Cannot migrate default channel preview snapshot: \(error.localizedDescription)")
|
|
2475
|
+
return false
|
|
2476
|
+
}
|
|
2477
|
+
case .unreadable:
|
|
2478
|
+
logger.warn("Cannot restore default channel from an unreadable preview snapshot")
|
|
2479
|
+
return false
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
return self.persistRestoredDefaultChannelFromPreview(
|
|
2483
|
+
channel: previousDefaultChannel,
|
|
2484
|
+
stateFile: stateFile,
|
|
2485
|
+
previewSnapshotFile: previewSnapshotFile
|
|
2486
|
+
)
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
private func applyRestoredDefaultChannelFromPreview(_ channel: String?) {
|
|
2490
|
+
let configDefaultChannel = self.getStringConfig("defaultChannel", defaultValue: "")
|
|
2491
|
+
if let channel = channel {
|
|
2492
|
+
UserDefaults.standard.set(channel, forKey: self.defaultChannelDefaultsKey)
|
|
2493
|
+
self.implementation.defaultChannel = channel
|
|
2494
|
+
logger.info("Restored defaultChannel after preview")
|
|
2495
|
+
} else {
|
|
2055
2496
|
UserDefaults.standard.removeObject(forKey: self.defaultChannelDefaultsKey)
|
|
2056
2497
|
self.implementation.defaultChannel = configDefaultChannel
|
|
2057
2498
|
logger.info("Restored defaultChannel after preview to config value")
|
|
2058
|
-
return
|
|
2059
2499
|
}
|
|
2500
|
+
}
|
|
2060
2501
|
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2502
|
+
private func persistRestoredDefaultChannelFromPreview(
|
|
2503
|
+
channel: String?,
|
|
2504
|
+
stateFile: URL,
|
|
2505
|
+
previewSnapshotFile: URL
|
|
2506
|
+
) -> Bool {
|
|
2507
|
+
defaultChannelStateLock.lock()
|
|
2508
|
+
defer { defaultChannelStateLock.unlock() }
|
|
2509
|
+
let didPersist: Bool
|
|
2510
|
+
do {
|
|
2511
|
+
try self.persistDefaultChannelStateWithoutLock(channel: channel, file: stateFile)
|
|
2512
|
+
try self.persistDefaultChannelPreviewSnapshotWithoutLock(
|
|
2513
|
+
channel: nil,
|
|
2514
|
+
isValid: false,
|
|
2515
|
+
file: previewSnapshotFile
|
|
2516
|
+
)
|
|
2517
|
+
didPersist = true
|
|
2518
|
+
} catch {
|
|
2519
|
+
let persistedState = self.defaultChannelStateWithoutLock(file: stateFile)
|
|
2520
|
+
let persistedSnapshot = self.defaultChannelPreviewSnapshotWithoutLock(file: previewSnapshotFile)
|
|
2521
|
+
if persistedState.exists,
|
|
2522
|
+
persistedState.isReadable,
|
|
2523
|
+
persistedState.channel == channel,
|
|
2524
|
+
persistedSnapshot == .invalidated {
|
|
2525
|
+
logger.warn("Default channel preview restore committed before metadata update failed")
|
|
2526
|
+
didPersist = true
|
|
2527
|
+
} else {
|
|
2528
|
+
logger.warn("Cannot durably restore default channel after preview: \(error.localizedDescription)")
|
|
2529
|
+
didPersist = false
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
self.applyRestoredDefaultChannelFromPreview(channel)
|
|
2533
|
+
return didPersist
|
|
2064
2534
|
}
|
|
2065
2535
|
|
|
2066
2536
|
private func normalizedPreviewAppId(_ rawAppId: String?) -> String? {
|
|
@@ -2240,7 +2710,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2240
2710
|
self.shakeMenuGesture = Self.normalizedShakeMenuGesture(self.getStringConfig("shakeMenuGesture", defaultValue: Self.shakeMenuGestureShake))
|
|
2241
2711
|
self.syncShakeMenuGestureRecognizer()
|
|
2242
2712
|
self.restorePreviewPreviousAppId()
|
|
2243
|
-
self.restorePreviewPreviousDefaultChannel()
|
|
2713
|
+
if !self.restorePreviewPreviousDefaultChannel() {
|
|
2714
|
+
logger.warn("Default channel preview restore will retry on next launch")
|
|
2715
|
+
}
|
|
2244
2716
|
_ = self.implementation.setPreviewFallbackBundle(fallback: nil)
|
|
2245
2717
|
_ = self.implementation.setNextBundle(next: Optional<String>.none)
|
|
2246
2718
|
self.clearPreviewSessionPreferences()
|
|
@@ -2485,6 +2957,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2485
2957
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
2486
2958
|
])
|
|
2487
2959
|
} else {
|
|
2960
|
+
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
2961
|
+
self.rejectCall(call, message: "Channel override removed but local persistence failed", code: "UNSETCHANNEL_PERSISTENCE_FAILED")
|
|
2962
|
+
return
|
|
2963
|
+
}
|
|
2488
2964
|
if self._isAutoUpdateEnabled() && triggerAutoUpdate {
|
|
2489
2965
|
self.logger.info("Calling autoupdater after channel change!")
|
|
2490
2966
|
// Check if download is already in progress (with timeout protection)
|
|
@@ -2531,6 +3007,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2531
3007
|
"error": res.error.contains("Channel URL") ? "missing_config" : (res.error.contains("cannot_update_via_private_channel") || res.error.contains("channel_self_set_not_allowed")) ? "channel_private" : "request_failed"
|
|
2532
3008
|
])
|
|
2533
3009
|
} else {
|
|
3010
|
+
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
3011
|
+
self.rejectCall(call, message: "Channel changed but local persistence failed", code: "SETCHANNEL_PERSISTENCE_FAILED")
|
|
3012
|
+
return
|
|
3013
|
+
}
|
|
2534
3014
|
if self._isAutoUpdateEnabled() && triggerAutoUpdate {
|
|
2535
3015
|
self.logger.info("Calling autoupdater after channel change!")
|
|
2536
3016
|
// Check if download is already in progress (with timeout protection)
|
|
@@ -2555,6 +3035,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2555
3035
|
"error": res.error.contains("Channel URL") ? "missing_config" : "request_failed"
|
|
2556
3036
|
])
|
|
2557
3037
|
} else {
|
|
3038
|
+
guard self.persistDefaultChannelStateFromDefaults() else {
|
|
3039
|
+
self.rejectCall(call, message: "Channel synchronized but local persistence failed", code: "GETCHANNEL_PERSISTENCE_FAILED")
|
|
3040
|
+
return
|
|
3041
|
+
}
|
|
2558
3042
|
self.resolveCall(call, data: res.toDict())
|
|
2559
3043
|
}
|
|
2560
3044
|
}
|