diff --git a/Passepartout/App/Constants/Constants+App.swift b/Passepartout/App/Constants/Constants+App.swift
index dfdef48a..b65d87b7 100644
--- a/Passepartout/App/Constants/Constants+App.swift
+++ b/Passepartout/App/Constants/Constants+App.swift
@@ -45,6 +45,12 @@ extension Constants {
}()
}
+ enum CloudKit {
+ static let containerId: String = bundleConfig("cloudkit_id")
+
+ static let coreDataZone = "com.apple.coredata.cloudkit.zone"
+ }
+
enum Plugins {
static let macBridgeName = "PassepartoutMac.bundle"
}
diff --git a/Passepartout/App/Context/AppContext.swift b/Passepartout/App/Context/AppContext.swift
index 06c1419e..a917c702 100644
--- a/Passepartout/App/Context/AppContext.swift
+++ b/Passepartout/App/Context/AppContext.swift
@@ -137,4 +137,8 @@ extension AppContext {
lastIsCloudSyncingEnabled = isCloudSyncingEnabled
}
}
+
+ func eraseCloudKitStore() async {
+ await coreContext.eraseCloudKitStore()
+ }
}
diff --git a/Passepartout/App/Context/CoreContext.swift b/Passepartout/App/Context/CoreContext.swift
index 21760d4f..a243795f 100644
--- a/Passepartout/App/Context/CoreContext.swift
+++ b/Passepartout/App/Context/CoreContext.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import CloudKit
import Combine
import Foundation
import PassepartoutLibrary
@@ -35,6 +36,8 @@ final class CoreContext {
private let persistenceManager: PersistenceManager
+ private(set) var vpnPersistence: VPNPersistence
+
let upgradeManager: UpgradeManager
let providerManager: ProviderManager
@@ -63,7 +66,7 @@ final class CoreContext {
upgradeManager.migrate(toVersion: Constants.Global.appVersionNumber)
persistenceManager = PersistenceManager(store: store)
- let vpnPersistence = persistenceManager.vpnPersistence(
+ vpnPersistence = persistenceManager.vpnPersistence(
withName: Constants.Persistence.profilesContainerName,
cloudKit: store.isCloudSyncingEnabled
)
@@ -140,14 +143,17 @@ private extension CoreContext {
extension CoreContext {
func reloadCloudKitObjects(isEnabled: Bool) {
- let vpnPersistence = persistenceManager.vpnPersistence(
+ vpnPersistence = persistenceManager.vpnPersistence(
withName: Constants.Persistence.profilesContainerName,
cloudKit: isEnabled
)
profileManager.swapProfileRepository(vpnPersistence.profileRepository())
}
- func eraseCloudKitStore() {
- // TODO: iCloud, erase remote records
+ func eraseCloudKitStore() async {
+ await vpnPersistence.eraseCloudKitStore(
+ fromContainerWithId: Constants.CloudKit.containerId,
+ zoneId: .init(zoneName: Constants.CloudKit.coreDataZone)
+ )
}
}
diff --git a/Passepartout/App/Info.plist b/Passepartout/App/Info.plist
index a5f31457..d6b6dbf8 100644
--- a/Passepartout/App/Info.plist
+++ b/Passepartout/App/Info.plist
@@ -101,6 +101,8 @@
$(CFG_APPSTORE_ID)
group_id
group.$(CFG_GROUP_ID)
+ cloudkit_id
+ iCloud.$(CFG_GROUP_ID)
diff --git a/Passepartout/App/L10n/Unlocalized.swift b/Passepartout/App/L10n/Unlocalized.swift
index ddc3a9ec..9f9fe7ff 100644
--- a/Passepartout/App/L10n/Unlocalized.swift
+++ b/Passepartout/App/L10n/Unlocalized.swift
@@ -257,6 +257,8 @@ enum Unlocalized {
enum Other {
static let siri = "Siri"
+ static let iCloud = "iCloud"
+
static let totp = "TOTP"
}
}
diff --git a/Passepartout/App/Views/SettingsView.swift b/Passepartout/App/Views/SettingsView.swift
index 78bdbf7d..4c832627 100644
--- a/Passepartout/App/Views/SettingsView.swift
+++ b/Passepartout/App/Views/SettingsView.swift
@@ -37,6 +37,8 @@ struct SettingsView: View {
@Binding private var shouldEnableCloudSyncing: Bool
+ @State private var isErasingCloudStore = false
+
private let versionString = Constants.Global.appVersionString
init() {
@@ -45,14 +47,20 @@ struct SettingsView: View {
_shouldEnableCloudSyncing = .init {
AppContext.shared.shouldEnableCloudSyncing
- } set: {
- AppContext.shared.shouldEnableCloudSyncing = $0
+ } set: { isEnabled in
+ withAnimation {
+ AppContext.shared.shouldEnableCloudSyncing = isEnabled
+ }
}
}
var body: some View {
List {
+ #if !targetEnvironment(macCatalyst)
preferencesSection
+ #endif
+ iCloudSection
+ diagnosticsSection
aboutSection
}.toolbar {
themeCloseItem(presentationMode: presentationMode)
@@ -66,29 +74,48 @@ struct SettingsView: View {
private extension SettingsView {
var preferencesSection: some View {
Section {
- #if !targetEnvironment(macCatalyst)
Toggle(L10n.Settings.Items.LocksInBackground.caption, isOn: $locksInBackground)
- #endif
- Toggle(L10n.Settings.Items.ShouldEnableCloudSyncing.caption, isOn: $shouldEnableCloudSyncing)
} header: {
Text(L10n.Preferences.title)
}
}
+ var iCloudSection: some View {
+ Section {
+ Toggle(L10n.Settings.Items.ShouldEnableCloudSyncing.caption, isOn: $shouldEnableCloudSyncing)
+ Button(L10n.Settings.Items.EraseCloudStore.caption) {
+ isErasingCloudStore = true
+ Task {
+ await AppContext.shared.eraseCloudKitStore()
+ isErasingCloudStore = false
+ }
+ }.withTrailingProgress(when: isErasingCloudStore)
+ .disabled(shouldEnableCloudSyncing || isErasingCloudStore)
+ } header: {
+ Text(Unlocalized.Other.iCloud)
+ } footer: {
+ Text(L10n.Settings.Sections.Icloud.footer)
+ }
+ }
+
+ var diagnosticsSection: some View {
+ Section {
+ DiagnosticsRow(currentProfile: profileManager.currentProfile)
+ }
+ }
+
var aboutSection: some View {
Section {
- NavigationLink {
- AboutView()
- } label: {
- Text(L10n.About.title)
- }
NavigationLink {
DonateView()
} label: {
Text(L10n.Settings.Items.Donate.caption)
}.disabled(!productManager.canMakePayments())
-
- DiagnosticsRow(currentProfile: profileManager.currentProfile)
+ NavigationLink {
+ AboutView()
+ } label: {
+ Text(L10n.About.title)
+ }
} footer: {
HStack {
Spacer()
diff --git a/Passepartout/App/en.lproj/Localizable.strings b/Passepartout/App/en.lproj/Localizable.strings
index 4a890f92..3f847701 100644
--- a/Passepartout/App/en.lproj/Localizable.strings
+++ b/Passepartout/App/en.lproj/Localizable.strings
@@ -331,8 +331,10 @@
/* MARK: SettingsView */
"settings.title" = "Settings";
+"settings.sections.icloud.footer" = "Disable sync to allow erase. To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles.";
"settings.items.locks_in_background.caption" = "Lock app access";
"settings.items.should_enable_cloud_syncing.caption" = "Sync with iCloud";
+"settings.items.erase_cloud_store.caption" = "Erase iCloud store";
"settings.items.donate.caption" = "Make a donation";
/* MARK: AboutView */
diff --git a/Passepartout/AppShared/Constants/SwiftGen+Strings.swift b/Passepartout/AppShared/Constants/SwiftGen+Strings.swift
index 31b9a02c..da819ff6 100644
--- a/Passepartout/AppShared/Constants/SwiftGen+Strings.swift
+++ b/Passepartout/AppShared/Constants/SwiftGen+Strings.swift
@@ -921,6 +921,10 @@ internal enum L10n {
/// Make a donation
internal static let caption = L10n.tr("Localizable", "settings.items.donate.caption", fallback: "Make a donation")
}
+ internal enum EraseCloudStore {
+ /// Erase iCloud store
+ internal static let caption = L10n.tr("Localizable", "settings.items.erase_cloud_store.caption", fallback: "Erase iCloud store")
+ }
internal enum LocksInBackground {
/// Lock app access
internal static let caption = L10n.tr("Localizable", "settings.items.locks_in_background.caption", fallback: "Lock app access")
@@ -930,6 +934,12 @@ internal enum L10n {
internal static let caption = L10n.tr("Localizable", "settings.items.should_enable_cloud_syncing.caption", fallback: "Sync with iCloud")
}
}
+ internal enum Sections {
+ internal enum Icloud {
+ /// Disable sync to allow erase. To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles.
+ internal static let footer = L10n.tr("Localizable", "settings.sections.icloud.footer", fallback: "Disable sync to allow erase. To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles.")
+ }
+ }
}
internal enum Shortcuts {
internal enum Add {
diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/CoreDataPersistentStore.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/CoreDataPersistentStore.swift
index 1e4eddaa..4b4d85ef 100644
--- a/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/CoreDataPersistentStore.swift
+++ b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/CoreDataPersistentStore.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import CloudKit
import Combine
import CoreData
import Foundation
@@ -69,7 +70,25 @@ public final class CoreDataPersistentStore {
container.viewContext.transactionAuthor = author
}
}
+}
+extension CoreDataPersistentStore {
+ public var context: NSManagedObjectContext {
+ container.viewContext
+ }
+
+ public var backgroundContext: NSManagedObjectContext {
+ container.newBackgroundContext()
+ }
+
+ public var coordinator: NSPersistentStoreCoordinator {
+ container.persistentStoreCoordinator
+ }
+}
+
+// MARK: Development
+
+extension CoreDataPersistentStore {
public var containerURLs: [URL]? {
guard let url = container.persistentStoreDescriptions.first?.url else {
return nil
@@ -104,13 +123,3 @@ public final class CoreDataPersistentStore {
}
}
}
-
-extension CoreDataPersistentStore {
- public var context: NSManagedObjectContext {
- container.viewContext
- }
-
- public var coordinator: NSPersistentStoreCoordinator {
- container.persistentStoreCoordinator
- }
-}
diff --git a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Data/VPNPersistence.swift b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Data/VPNPersistence.swift
index 89eeb61c..0855ad88 100644
--- a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Data/VPNPersistence.swift
+++ b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Data/VPNPersistence.swift
@@ -23,6 +23,7 @@
// along with Passepartout. If not, see .
//
+import CloudKit
import CoreData
import Foundation
import PassepartoutCore
@@ -54,4 +55,15 @@ public final class VPNPersistence {
public func profileRepository() -> ProfileRepository {
CDProfileRepository(store.context)
}
+
+ // WARNING: this is not running on main actor
+ public func eraseCloudKitStore(fromContainerWithId containerId: String, zoneId: CKRecordZone.ID) async {
+ do {
+ let container = CKContainer(identifier: containerId)
+ let db = container.privateCloudDatabase
+ try await db.deleteRecordZone(withID: zoneId)
+ } catch {
+ pp_log.error("Unable to erase CloudKit store: \(error)")
+ }
+ }
}