Allow erasing remote iCloud store (#351)
Convenient for those with privacy concerns.
This commit is contained in:
parent
a4ca8cc996
commit
791b6be7d5
|
@ -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 {
|
enum Plugins {
|
||||||
static let macBridgeName = "PassepartoutMac.bundle"
|
static let macBridgeName = "PassepartoutMac.bundle"
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,4 +137,8 @@ extension AppContext {
|
||||||
lastIsCloudSyncingEnabled = isCloudSyncingEnabled
|
lastIsCloudSyncingEnabled = isCloudSyncingEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func eraseCloudKitStore() async {
|
||||||
|
await coreContext.eraseCloudKitStore()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CloudKit
|
||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutLibrary
|
import PassepartoutLibrary
|
||||||
|
@ -35,6 +36,8 @@ final class CoreContext {
|
||||||
|
|
||||||
private let persistenceManager: PersistenceManager
|
private let persistenceManager: PersistenceManager
|
||||||
|
|
||||||
|
private(set) var vpnPersistence: VPNPersistence
|
||||||
|
|
||||||
let upgradeManager: UpgradeManager
|
let upgradeManager: UpgradeManager
|
||||||
|
|
||||||
let providerManager: ProviderManager
|
let providerManager: ProviderManager
|
||||||
|
@ -63,7 +66,7 @@ final class CoreContext {
|
||||||
upgradeManager.migrate(toVersion: Constants.Global.appVersionNumber)
|
upgradeManager.migrate(toVersion: Constants.Global.appVersionNumber)
|
||||||
|
|
||||||
persistenceManager = PersistenceManager(store: store)
|
persistenceManager = PersistenceManager(store: store)
|
||||||
let vpnPersistence = persistenceManager.vpnPersistence(
|
vpnPersistence = persistenceManager.vpnPersistence(
|
||||||
withName: Constants.Persistence.profilesContainerName,
|
withName: Constants.Persistence.profilesContainerName,
|
||||||
cloudKit: store.isCloudSyncingEnabled
|
cloudKit: store.isCloudSyncingEnabled
|
||||||
)
|
)
|
||||||
|
@ -140,14 +143,17 @@ private extension CoreContext {
|
||||||
|
|
||||||
extension CoreContext {
|
extension CoreContext {
|
||||||
func reloadCloudKitObjects(isEnabled: Bool) {
|
func reloadCloudKitObjects(isEnabled: Bool) {
|
||||||
let vpnPersistence = persistenceManager.vpnPersistence(
|
vpnPersistence = persistenceManager.vpnPersistence(
|
||||||
withName: Constants.Persistence.profilesContainerName,
|
withName: Constants.Persistence.profilesContainerName,
|
||||||
cloudKit: isEnabled
|
cloudKit: isEnabled
|
||||||
)
|
)
|
||||||
profileManager.swapProfileRepository(vpnPersistence.profileRepository())
|
profileManager.swapProfileRepository(vpnPersistence.profileRepository())
|
||||||
}
|
}
|
||||||
|
|
||||||
func eraseCloudKitStore() {
|
func eraseCloudKitStore() async {
|
||||||
// TODO: iCloud, erase remote records
|
await vpnPersistence.eraseCloudKitStore(
|
||||||
|
fromContainerWithId: Constants.CloudKit.containerId,
|
||||||
|
zoneId: .init(zoneName: Constants.CloudKit.coreDataZone)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,6 +101,8 @@
|
||||||
<string>$(CFG_APPSTORE_ID)</string>
|
<string>$(CFG_APPSTORE_ID)</string>
|
||||||
<key>group_id</key>
|
<key>group_id</key>
|
||||||
<string>group.$(CFG_GROUP_ID)</string>
|
<string>group.$(CFG_GROUP_ID)</string>
|
||||||
|
<key>cloudkit_id</key>
|
||||||
|
<string>iCloud.$(CFG_GROUP_ID)</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -257,6 +257,8 @@ enum Unlocalized {
|
||||||
enum Other {
|
enum Other {
|
||||||
static let siri = "Siri"
|
static let siri = "Siri"
|
||||||
|
|
||||||
|
static let iCloud = "iCloud"
|
||||||
|
|
||||||
static let totp = "TOTP"
|
static let totp = "TOTP"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,8 @@ struct SettingsView: View {
|
||||||
|
|
||||||
@Binding private var shouldEnableCloudSyncing: Bool
|
@Binding private var shouldEnableCloudSyncing: Bool
|
||||||
|
|
||||||
|
@State private var isErasingCloudStore = false
|
||||||
|
|
||||||
private let versionString = Constants.Global.appVersionString
|
private let versionString = Constants.Global.appVersionString
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
@ -45,14 +47,20 @@ struct SettingsView: View {
|
||||||
|
|
||||||
_shouldEnableCloudSyncing = .init {
|
_shouldEnableCloudSyncing = .init {
|
||||||
AppContext.shared.shouldEnableCloudSyncing
|
AppContext.shared.shouldEnableCloudSyncing
|
||||||
} set: {
|
} set: { isEnabled in
|
||||||
AppContext.shared.shouldEnableCloudSyncing = $0
|
withAnimation {
|
||||||
|
AppContext.shared.shouldEnableCloudSyncing = isEnabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
preferencesSection
|
preferencesSection
|
||||||
|
#endif
|
||||||
|
iCloudSection
|
||||||
|
diagnosticsSection
|
||||||
aboutSection
|
aboutSection
|
||||||
}.toolbar {
|
}.toolbar {
|
||||||
themeCloseItem(presentationMode: presentationMode)
|
themeCloseItem(presentationMode: presentationMode)
|
||||||
|
@ -66,29 +74,48 @@ struct SettingsView: View {
|
||||||
private extension SettingsView {
|
private extension SettingsView {
|
||||||
var preferencesSection: some View {
|
var preferencesSection: some View {
|
||||||
Section {
|
Section {
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
Toggle(L10n.Settings.Items.LocksInBackground.caption, isOn: $locksInBackground)
|
Toggle(L10n.Settings.Items.LocksInBackground.caption, isOn: $locksInBackground)
|
||||||
#endif
|
|
||||||
Toggle(L10n.Settings.Items.ShouldEnableCloudSyncing.caption, isOn: $shouldEnableCloudSyncing)
|
|
||||||
} header: {
|
} header: {
|
||||||
Text(L10n.Preferences.title)
|
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 {
|
var aboutSection: some View {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink {
|
|
||||||
AboutView()
|
|
||||||
} label: {
|
|
||||||
Text(L10n.About.title)
|
|
||||||
}
|
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
DonateView()
|
DonateView()
|
||||||
} label: {
|
} label: {
|
||||||
Text(L10n.Settings.Items.Donate.caption)
|
Text(L10n.Settings.Items.Donate.caption)
|
||||||
}.disabled(!productManager.canMakePayments())
|
}.disabled(!productManager.canMakePayments())
|
||||||
|
NavigationLink {
|
||||||
DiagnosticsRow(currentProfile: profileManager.currentProfile)
|
AboutView()
|
||||||
|
} label: {
|
||||||
|
Text(L10n.About.title)
|
||||||
|
}
|
||||||
} footer: {
|
} footer: {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
|
@ -331,8 +331,10 @@
|
||||||
/* MARK: SettingsView */
|
/* MARK: SettingsView */
|
||||||
|
|
||||||
"settings.title" = "Settings";
|
"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.locks_in_background.caption" = "Lock app access";
|
||||||
"settings.items.should_enable_cloud_syncing.caption" = "Sync with iCloud";
|
"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";
|
"settings.items.donate.caption" = "Make a donation";
|
||||||
|
|
||||||
/* MARK: AboutView */
|
/* MARK: AboutView */
|
||||||
|
|
|
@ -921,6 +921,10 @@ internal enum L10n {
|
||||||
/// Make a donation
|
/// Make a donation
|
||||||
internal static let caption = L10n.tr("Localizable", "settings.items.donate.caption", fallback: "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 {
|
internal enum LocksInBackground {
|
||||||
/// Lock app access
|
/// Lock app access
|
||||||
internal static let caption = L10n.tr("Localizable", "settings.items.locks_in_background.caption", fallback: "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 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 Shortcuts {
|
||||||
internal enum Add {
|
internal enum Add {
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CloudKit
|
||||||
import Combine
|
import Combine
|
||||||
import CoreData
|
import CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
|
@ -69,7 +70,25 @@ public final class CoreDataPersistentStore {
|
||||||
container.viewContext.transactionAuthor = author
|
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]? {
|
public var containerURLs: [URL]? {
|
||||||
guard let url = container.persistentStoreDescriptions.first?.url else {
|
guard let url = container.persistentStoreDescriptions.first?.url else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -104,13 +123,3 @@ public final class CoreDataPersistentStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CoreDataPersistentStore {
|
|
||||||
public var context: NSManagedObjectContext {
|
|
||||||
container.viewContext
|
|
||||||
}
|
|
||||||
|
|
||||||
public var coordinator: NSPersistentStoreCoordinator {
|
|
||||||
container.persistentStoreCoordinator
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CloudKit
|
||||||
import CoreData
|
import CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
import PassepartoutCore
|
import PassepartoutCore
|
||||||
|
@ -54,4 +55,15 @@ public final class VPNPersistence {
|
||||||
public func profileRepository() -> ProfileRepository {
|
public func profileRepository() -> ProfileRepository {
|
||||||
CDProfileRepository(store.context)
|
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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue