Support iCloud sync as an option (#350)
Sync will be enabled on upgrade for consistency with current behavior, and disabled for new installs. Fixes #227
This commit is contained in:
parent
2d046181b0
commit
a4ca8cc996
|
@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Changed
|
||||
|
||||
- Make iCloud an opt-in preference. [#227](https://github.com/passepartoutvpn/passepartout-apple/issues/227)
|
||||
- OpenVPN: Endpoint UX. [#332](https://github.com/passepartoutvpn/passepartout-apple/pull/332)
|
||||
- Convert trusted networks to on-demand activation. [#119](https://github.com/passepartoutvpn/passepartout-apple/issues/119)
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
0E3A3C132AAB7C480003A5F6 /* UpgradeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C112AAB7C470003A5F6 /* UpgradeManager.swift */; };
|
||||
0E3A3C142AAB7C480003A5F6 /* DefaultUpgradeManagerStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C122AAB7C480003A5F6 /* DefaultUpgradeManagerStrategy.swift */; };
|
||||
0E3A3C162AAB8AB80003A5F6 /* UpgradeManagerStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C152AAB8AB80003A5F6 /* UpgradeManagerStrategy.swift */; };
|
||||
0E3A3C102AA9AA530003A5F6 /* KeyValueStore+CloudKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C0F2AA9AA530003A5F6 /* KeyValueStore+CloudKit.swift */; };
|
||||
0E3A593C2A50975700B3FE40 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A593B2A50975700B3FE40 /* ErrorHandler.swift */; };
|
||||
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FCC27E47B3700C66F13 /* AddHostView+Name.swift */; };
|
||||
0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */; };
|
||||
|
@ -346,6 +347,7 @@
|
|||
0E3A3C112AAB7C470003A5F6 /* UpgradeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeManager.swift; sourceTree = "<group>"; };
|
||||
0E3A3C122AAB7C480003A5F6 /* DefaultUpgradeManagerStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultUpgradeManagerStrategy.swift; sourceTree = "<group>"; };
|
||||
0E3A3C152AAB8AB80003A5F6 /* UpgradeManagerStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeManagerStrategy.swift; sourceTree = "<group>"; };
|
||||
0E3A3C0F2AA9AA530003A5F6 /* KeyValueStore+CloudKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyValueStore+CloudKit.swift"; sourceTree = "<group>"; };
|
||||
0E3A593B2A50975700B3FE40 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
|
||||
0E3B7FCC27E47B3700C66F13 /* AddHostView+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AddHostView+Name.swift"; sourceTree = "<group>"; };
|
||||
0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+VPN.swift"; sourceTree = "<group>"; };
|
||||
|
@ -744,6 +746,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0EBC075C27EC529000208AD9 /* DebugLog+Constants.swift */,
|
||||
0E3A3C0F2AA9AA530003A5F6 /* KeyValueStore+CloudKit.swift */,
|
||||
0EB17EB927D2560300D473B5 /* PassepartoutProviders+Extensions.swift */,
|
||||
0E35C099280E95BB0071FA35 /* ProviderProfileAvailability.swift */,
|
||||
0E2DE71B27DCCFE80067B9E1 /* TunnelKit+Extensions.swift */,
|
||||
|
@ -1542,6 +1545,7 @@
|
|||
0E53249927D26B51002565C3 /* ProductManager.swift in Sources */,
|
||||
0E9C233027F47032007D5FC7 /* IntentsManager.swift in Sources */,
|
||||
0EB4042C27CA0E8C00378B1A /* Unlocalized.swift in Sources */,
|
||||
0E3A3C102AA9AA530003A5F6 /* KeyValueStore+CloudKit.swift in Sources */,
|
||||
0EB4042E27CA136300378B1A /* AddingTextField.swift in Sources */,
|
||||
0EE8B7E327FF340F00B68621 /* VPNProtocolType+FileExtensions.swift in Sources */,
|
||||
0EF2212B27E667EA001D0BD7 /* AddProviderView+Name.swift in Sources */,
|
||||
|
|
|
@ -31,6 +31,8 @@ import PassepartoutLibrary
|
|||
final class AppContext {
|
||||
private let coreContext: CoreContext
|
||||
|
||||
private var lastIsCloudSyncingEnabled: Bool?
|
||||
|
||||
let productManager: ProductManager
|
||||
|
||||
private let reviewer: Reviewer
|
||||
|
@ -114,3 +116,25 @@ private extension AppContext {
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: CloudKit
|
||||
|
||||
extension AppContext {
|
||||
var shouldEnableCloudSyncing: Bool {
|
||||
get {
|
||||
coreContext.store.shouldEnableCloudSyncing
|
||||
}
|
||||
set {
|
||||
coreContext.store.shouldEnableCloudSyncing = newValue
|
||||
|
||||
// iCloud may be externally disabled from the device settings
|
||||
let isCloudSyncingEnabled = coreContext.store.isCloudSyncingEnabled
|
||||
guard isCloudSyncingEnabled != lastIsCloudSyncingEnabled else {
|
||||
pp_log.debug("CloudKit state did not change")
|
||||
return
|
||||
}
|
||||
coreContext.reloadCloudKitObjects(isEnabled: isCloudSyncingEnabled)
|
||||
lastIsCloudSyncingEnabled = isCloudSyncingEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@ import TunnelKitManager
|
|||
|
||||
@MainActor
|
||||
final class CoreContext {
|
||||
let store: KeyValueStore
|
||||
|
||||
private let persistenceManager: PersistenceManager
|
||||
|
||||
let upgradeManager: UpgradeManager
|
||||
|
||||
let providerManager: ProviderManager
|
||||
|
@ -42,6 +46,8 @@ final class CoreContext {
|
|||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
init(store: KeyValueStore) {
|
||||
self.store = store
|
||||
|
||||
let logger = SwiftyBeaverLogger(
|
||||
logFile: Constants.Log.App.url,
|
||||
logLevel: Constants.Log.level,
|
||||
|
@ -50,18 +56,20 @@ final class CoreContext {
|
|||
Passepartout.shared.logger = logger
|
||||
pp_log.info("Logging to: \(logger.logFile!)")
|
||||
|
||||
let persistenceManager = PersistenceManager(store: store)
|
||||
let vpnPersistence = persistenceManager.vpnPersistence(
|
||||
withName: Constants.Persistence.profilesContainerName
|
||||
)
|
||||
let providersPersistence = persistenceManager.providersPersistence(
|
||||
withName: Constants.Persistence.providersContainerName
|
||||
)
|
||||
|
||||
upgradeManager = UpgradeManager(
|
||||
store: store,
|
||||
strategy: DefaultUpgradeManagerStrategy()
|
||||
)
|
||||
upgradeManager.migrate(toVersion: Constants.Global.appVersionNumber)
|
||||
|
||||
persistenceManager = PersistenceManager(store: store)
|
||||
let vpnPersistence = persistenceManager.vpnPersistence(
|
||||
withName: Constants.Persistence.profilesContainerName,
|
||||
cloudKit: store.isCloudSyncingEnabled
|
||||
)
|
||||
let providersPersistence = persistenceManager.providersPersistence(
|
||||
withName: Constants.Persistence.providersContainerName
|
||||
)
|
||||
|
||||
let remoteProvidersStrategy = APIRemoteProvidersStrategy(
|
||||
appBuild: Constants.Global.appBuildNumber,
|
||||
|
@ -127,3 +135,19 @@ private extension CoreContext {
|
|||
}.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: CloudKit
|
||||
|
||||
extension CoreContext {
|
||||
func reloadCloudKitObjects(isEnabled: Bool) {
|
||||
let vpnPersistence = persistenceManager.vpnPersistence(
|
||||
withName: Constants.Persistence.profilesContainerName,
|
||||
cloudKit: isEnabled
|
||||
)
|
||||
profileManager.swapProfileRepository(vpnPersistence.profileRepository())
|
||||
}
|
||||
|
||||
func eraseCloudKitStore() {
|
||||
// TODO: iCloud, erase remote records
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,8 @@ import PassepartoutLibrary
|
|||
enum AppPreference: String, KeyStoreDomainLocation {
|
||||
case launchesOnLogin
|
||||
|
||||
case shouldEnableCloudSyncing
|
||||
|
||||
case isShowingFavorites
|
||||
|
||||
case didHandleSubreddit
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// KeyValueStore+CloudKit.swift
|
||||
// Passepartout
|
||||
//
|
||||
// Created by Davide De Rosa on 9/7/23.
|
||||
// Copyright (c) 2023 Davide De Rosa. All rights reserved.
|
||||
//
|
||||
// https://github.com/passepartoutvpn
|
||||
//
|
||||
// This file is part of Passepartout.
|
||||
//
|
||||
// Passepartout is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Passepartout is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PassepartoutLibrary
|
||||
|
||||
extension KeyValueStore {
|
||||
|
||||
// MARK: Support
|
||||
|
||||
private var cloudKitToken: Any? {
|
||||
FileManager.default.ubiquityIdentityToken
|
||||
}
|
||||
|
||||
var isCloudKitSupported: Bool {
|
||||
cloudKitToken != nil
|
||||
}
|
||||
|
||||
// MARK: Preference
|
||||
|
||||
var shouldEnableCloudSyncing: Bool {
|
||||
get {
|
||||
value(forLocation: AppPreference.shouldEnableCloudSyncing) ?? false
|
||||
}
|
||||
set {
|
||||
setValue(newValue, forLocation: AppPreference.shouldEnableCloudSyncing)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Computed state
|
||||
|
||||
var isCloudSyncingEnabled: Bool {
|
||||
guard isCloudKitSupported else {
|
||||
pp_log.debug("CloudKit unavailable")
|
||||
return false
|
||||
}
|
||||
let isEnabled = shouldEnableCloudSyncing
|
||||
pp_log.debug("CloudKit enabled: \(isEnabled)")
|
||||
return isEnabled
|
||||
}
|
||||
}
|
|
@ -30,11 +30,22 @@ public final class DefaultUpgradeManagerStrategy: UpgradeManagerStrategy {
|
|||
public init() {
|
||||
}
|
||||
|
||||
public func doMigrate(store: KeyValueStore, lastVersion: String?) {
|
||||
public func migrate(store: KeyValueStore, lastVersion: String?) {
|
||||
|
||||
// legacy check before lastVersion was even stored
|
||||
let isUpgradeFromBefore_2_2_0: Bool? = store.value(forLocation: UpgradeManager.StoreKey.existingKeyBefore_2_2_0)
|
||||
if isUpgradeFromBefore_2_2_0 != nil {
|
||||
pp_log.debug("Upgrading from < 2.2.0, iCloud syncing defaults to enabled")
|
||||
store.setValue(true, forLocation: AppPreference.shouldEnableCloudSyncing)
|
||||
}
|
||||
|
||||
guard let lastVersion else {
|
||||
pp_log.debug("Fresh install")
|
||||
return
|
||||
}
|
||||
pp_log.debug("Upgrade from \(lastVersion)")
|
||||
}
|
||||
|
||||
public func migrateData() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,8 +39,8 @@ final class PersistenceManager {
|
|||
}
|
||||
}
|
||||
|
||||
func vpnPersistence(withName containerName: String) -> VPNPersistence {
|
||||
VPNPersistence(withName: containerName, cloudKit: true, author: persistenceAuthor)
|
||||
func vpnPersistence(withName containerName: String, cloudKit: Bool) -> VPNPersistence {
|
||||
VPNPersistence(withName: containerName, cloudKit: cloudKit, author: persistenceAuthor)
|
||||
}
|
||||
|
||||
func providersPersistence(withName containerName: String) -> ProvidersPersistence {
|
||||
|
|
|
@ -47,11 +47,14 @@ public final class UpgradeManager: ObservableObject {
|
|||
self.strategy = strategy
|
||||
}
|
||||
|
||||
public func doMigrations(toVersion currentVersion: String, profileManager: ProfileManager) {
|
||||
public func migrate(toVersion currentVersion: String) {
|
||||
if let lastVersion, currentVersion > lastVersion {
|
||||
strategy.doMigrate(store: store, lastVersion: lastVersion)
|
||||
strategy.migrate(store: store, lastVersion: lastVersion)
|
||||
}
|
||||
lastVersion = currentVersion
|
||||
}
|
||||
|
||||
public func migrateData(profileManager: ProfileManager) {
|
||||
isDoingMigrations = false
|
||||
}
|
||||
}
|
||||
|
@ -69,8 +72,12 @@ private extension UpgradeManager {
|
|||
}
|
||||
}
|
||||
|
||||
private extension UpgradeManager {
|
||||
extension UpgradeManager {
|
||||
enum StoreKey: String, KeyStoreDomainLocation {
|
||||
|
||||
@available(*, deprecated, message: "Retain temporarily for migrations")
|
||||
case existingKeyBefore_2_2_0 = "didMigrateToV2"
|
||||
|
||||
case lastVersion
|
||||
|
||||
var domain: String {
|
||||
|
|
|
@ -27,5 +27,6 @@ import Foundation
|
|||
import PassepartoutCore
|
||||
|
||||
public protocol UpgradeManagerStrategy {
|
||||
func doMigrate(store: KeyValueStore, lastVersion: String?)
|
||||
func migrate(store: KeyValueStore, lastVersion: String?)
|
||||
func migrateData()
|
||||
}
|
||||
|
|
|
@ -26,16 +26,14 @@
|
|||
import PassepartoutLibrary
|
||||
import SwiftUI
|
||||
|
||||
struct DiagnosticsSection: View {
|
||||
struct DiagnosticsRow: View {
|
||||
@ObservedObject var currentProfile: ObservableProfile
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
NavigationLink {
|
||||
DiagnosticsView(profile: currentProfile.value)
|
||||
} label: {
|
||||
Text(L10n.Diagnostics.title)
|
||||
}
|
||||
NavigationLink {
|
||||
DiagnosticsView(profile: currentProfile.value)
|
||||
} label: {
|
||||
Text(L10n.Diagnostics.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -183,10 +183,7 @@ private extension OrganizerView.ProfilesList {
|
|||
|
||||
func performMigrationsIfNeeded() {
|
||||
Task { @MainActor in
|
||||
UpgradeManager.shared.doMigrations(
|
||||
toVersion: Constants.Global.appVersionNumber,
|
||||
profileManager: profileManager
|
||||
)
|
||||
UpgradeManager.shared.migrateData(profileManager: profileManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,7 +105,9 @@ private extension ProfileView {
|
|||
modalType: $modalType
|
||||
)
|
||||
ExtraSection(currentProfile: currentProfile)
|
||||
DiagnosticsSection(currentProfile: currentProfile)
|
||||
Section {
|
||||
DiagnosticsRow(currentProfile: currentProfile)
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
}
|
||||
|
|
|
@ -35,19 +35,24 @@ struct SettingsView: View {
|
|||
|
||||
@AppStorage(AppPreference.locksInBackground.key) private var locksInBackground = false
|
||||
|
||||
@Binding private var shouldEnableCloudSyncing: Bool
|
||||
|
||||
private let versionString = Constants.Global.appVersionString
|
||||
|
||||
init() {
|
||||
profileManager = .shared
|
||||
productManager = .shared
|
||||
|
||||
_shouldEnableCloudSyncing = .init {
|
||||
AppContext.shared.shouldEnableCloudSyncing
|
||||
} set: {
|
||||
AppContext.shared.shouldEnableCloudSyncing = $0
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
preferencesSection
|
||||
#endif
|
||||
DiagnosticsSection(currentProfile: profileManager.currentProfile)
|
||||
aboutSection
|
||||
}.toolbar {
|
||||
themeCloseItem(presentationMode: presentationMode)
|
||||
|
@ -61,7 +66,12 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,6 +87,8 @@ private extension SettingsView {
|
|||
} label: {
|
||||
Text(L10n.Settings.Items.Donate.caption)
|
||||
}.disabled(!productManager.canMakePayments())
|
||||
|
||||
DiagnosticsRow(currentProfile: profileManager.currentProfile)
|
||||
} footer: {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
|
|
@ -332,6 +332,7 @@
|
|||
|
||||
"settings.title" = "Settings";
|
||||
"settings.items.locks_in_background.caption" = "Lock app access";
|
||||
"settings.items.should_enable_cloud_syncing.caption" = "Sync with iCloud";
|
||||
"settings.items.donate.caption" = "Make a donation";
|
||||
|
||||
/* MARK: AboutView */
|
||||
|
|
|
@ -925,6 +925,10 @@ internal enum L10n {
|
|||
/// Lock app access
|
||||
internal static let caption = L10n.tr("Localizable", "settings.items.locks_in_background.caption", fallback: "Lock app access")
|
||||
}
|
||||
internal enum ShouldEnableCloudSyncing {
|
||||
/// Sync with iCloud
|
||||
internal static let caption = L10n.tr("Localizable", "settings.items.should_enable_cloud_syncing.caption", fallback: "Sync with iCloud")
|
||||
}
|
||||
}
|
||||
}
|
||||
internal enum Shortcuts {
|
||||
|
|
|
@ -33,7 +33,7 @@ public protocol KeyStoreDomainLocation: KeyStoreLocation {
|
|||
var domain: String { get }
|
||||
}
|
||||
|
||||
public protocol KeyValueStore {
|
||||
public protocol KeyValueStore: AnyObject {
|
||||
func setValue<L, V>(_ value: V?, forLocation location: L) where L: KeyStoreLocation
|
||||
|
||||
func value<L, V>(forLocation location: L) -> V? where L: KeyStoreLocation
|
||||
|
|
|
@ -42,7 +42,7 @@ public final class ProfileManager: ObservableObject {
|
|||
|
||||
private let providerManager: ProviderManager
|
||||
|
||||
private let profileRepository: ProfileRepository
|
||||
private var profileRepository: ProfileRepository
|
||||
|
||||
private let keychain: SecretRepository
|
||||
|
||||
|
@ -488,6 +488,18 @@ extension ProfileManager {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Repository
|
||||
|
||||
extension ProfileManager {
|
||||
public func swapProfileRepository(_ newProfileRepository: ProfileRepository) {
|
||||
cancellables.removeAll()
|
||||
|
||||
objectWillChange.send()
|
||||
profileRepository = newProfileRepository
|
||||
observeUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: KeyValueStore
|
||||
|
||||
extension ProfileManager {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
import Foundation
|
||||
import PassepartoutCore
|
||||
|
||||
public struct UserDefaultsStore: KeyValueStore {
|
||||
public class UserDefaultsStore: KeyValueStore {
|
||||
private let defaults: UserDefaults
|
||||
|
||||
private let key: (any KeyStoreLocation) -> String
|
||||
|
@ -41,11 +41,11 @@ public struct UserDefaultsStore: KeyValueStore {
|
|||
defaults.removeObject(forKey: key(location))
|
||||
return
|
||||
}
|
||||
defaults.setValue(value, forKey: key(location))
|
||||
defaults.set(value, forKey: key(location))
|
||||
}
|
||||
|
||||
public func value<L, V>(forLocation location: L) -> V? where L: KeyStoreLocation {
|
||||
defaults.value(forKey: key(location)) as? V
|
||||
defaults.object(forKey: key(location)) as? V
|
||||
}
|
||||
|
||||
public func removeValue<L>(forLocation location: L) where L: KeyStoreLocation {
|
||||
|
|
Loading…
Reference in New Issue