From dfae6afcb4459a007c4b44580d1677c2bc170851 Mon Sep 17 00:00:00 2001 From: Davide Date: Fri, 6 Dec 2024 11:24:51 +0100 Subject: [PATCH] Store complex preferences to Core Data (#981) Replace favorites entities with a PreferencesManager, that returns observables for: - Module preferences (by module UUID) - Provider preferences (by ProviderID) Automate preferences availability in: - Module views (empty for now) - VPN server view (favorites) Synchronize preferences by making this a CloudKit container. Preferences are also available in the Tunnel by storing the container in the App Group. --- Library/Package.swift | 31 ++++- .../AppData+Preferences.swift | 39 ++++++ .../Domain/CDModulePreferencesV3.swift | 36 ++++++ .../Domain/CDProviderPreferencesV3.swift | 37 ++++++ .../Preferences.xcdatamodel/contents | 10 ++ .../CDModulePreferencesRepositoryV3.swift | 89 ++++++++++++++ .../CDProviderPreferencesRepositoryV3.swift | 114 ++++++++++++++++++ .../ProfilesV3.xcdatamodel/contents | 2 +- .../Strategy/CDProviderRepositoryV3.swift | 2 +- .../AppUIMain/Views/Modules/DNSView.swift | 7 +- .../Extensions/DNSModule+Extensions.swift | 5 +- .../HTTPProxyModule+Extensions.swift | 5 +- .../Extensions/IPModule+Extensions.swift | 5 +- .../OnDemandModule+Extensions.swift | 4 +- .../Extensions/OpenVPNModule+Extensions.swift | 4 +- .../WireGuardModule+Extensions.swift | 4 +- .../Views/Modules/HTTPProxyView.swift | 7 +- .../AppUIMain/Views/Modules/IPView.swift | 8 +- .../Views/Modules/OnDemandView.swift | 16 ++- .../AppUIMain/Views/Modules/OpenVPNView.swift | 16 ++- .../Views/Modules/WireGuardView.swift | 10 +- .../Views/Profile/ModuleDetailView.swift | 24 +++- .../Views/VPN/VPNProviderServerView.swift | 25 ++-- .../iOS/VPNProviderServer+Content+iOS.swift | 4 +- .../VPNProviderServer+Content+macOS.swift | 4 +- .../Business/PreferencesManager.swift | 77 ++++++++++++ .../Domain/BundleConfiguration+AppGroup.swift | 20 ++- .../Domain/BundleConfiguration+Main.swift | 2 + .../CommonLibrary/Domain/Constants.swift | 6 +- .../Domain/ModulePreferences.swift | 49 ++++++++ .../Domain/ProviderPreferences.swift | 61 ++++++++++ .../CommonLibrary/Resources/Constants.json | 5 +- .../ModulePreferencesRepository.swift | 31 +++++ .../ProviderPreferencesRepository.swift | 32 +++++ .../Extensions/UUID+RawRepresentable.swift} | 35 ++---- .../UILibrary/Business/AppContext.swift | 4 + .../Business/ProviderFavoritesManager.swift | 64 ---------- .../UILibrary/Domain/UIPreference.swift | 2 - .../Extensions/ModuleBuilder+Previews.swift | 12 +- .../Extensions/View+Environment.swift | 1 + .../Previews/AppContext+Previews.swift | 1 + .../Strategy/ModuleViewFactory+Default.swift | 11 +- .../Strategy/ModuleViewFactory.swift | 3 +- .../Strategy/ModuleViewProviding.swift | 22 +++- Passepartout/App/App.entitlements | 1 + Passepartout/App/App.plist | 2 + Passepartout/Config.xcconfig | 1 + Passepartout/Shared/AppContext+Shared.swift | 23 +--- Passepartout/Shared/Shared.swift | 41 ++++++- .../Shared/Testing/AppContext+Testing.swift | 1 + Passepartout/Tunnel/Tunnel.plist | 2 + 51 files changed, 832 insertions(+), 185 deletions(-) create mode 100644 Library/Sources/AppDataPreferences/AppData+Preferences.swift create mode 100644 Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift create mode 100644 Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift create mode 100644 Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents create mode 100644 Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift create mode 100644 Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift create mode 100644 Library/Sources/CommonLibrary/Business/PreferencesManager.swift create mode 100644 Library/Sources/CommonLibrary/Domain/ModulePreferences.swift create mode 100644 Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift create mode 100644 Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift create mode 100644 Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift rename Library/Sources/{UILibrary/Domain/ProviderFavoriteServers.swift => CommonUtils/Extensions/UUID+RawRepresentable.swift} (55%) delete mode 100644 Library/Sources/UILibrary/Business/ProviderFavoritesManager.swift diff --git a/Library/Package.swift b/Library/Package.swift index e76e7cdb..1611be44 100644 --- a/Library/Package.swift +++ b/Library/Package.swift @@ -15,11 +15,21 @@ let package = Package( // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "AppUIMain", - targets: ["AppUIMainWrapper"] + targets: [ + "AppDataPreferences", + "AppDataProfiles", + "AppDataProviders", + "AppUIMainWrapper" + ] ), .library( name: "AppUITV", - targets: ["AppUITVWrapper"] + targets: [ + "AppDataPreferences", + "AppDataProfiles", + "AppDataProviders", + "AppUITVWrapper" + ] ), .library( name: "CommonIAP", @@ -39,7 +49,10 @@ let package = Package( ), .library( name: "TunnelLibrary", - targets: ["CommonLibrary"] + targets: [ + "AppDataPreferences", + "CommonLibrary" + ] ), .library( name: "UILibrary", @@ -69,6 +82,16 @@ let package = Package( name: "AppData", dependencies: [] ), + .target( + name: "AppDataPreferences", + dependencies: [ + "AppData", + "CommonLibrary" + ], + resources: [ + .process("Preferences.xcdatamodeld") + ] + ), .target( name: "AppDataProfiles", dependencies: [ @@ -166,8 +189,6 @@ let package = Package( .target( name: "UILibrary", dependencies: [ - "AppDataProfiles", - "AppDataProviders", "CommonAPI", "CommonLibrary", "UITesting" diff --git a/Library/Sources/AppDataPreferences/AppData+Preferences.swift b/Library/Sources/AppDataPreferences/AppData+Preferences.swift new file mode 100644 index 00000000..25758ae3 --- /dev/null +++ b/Library/Sources/AppDataPreferences/AppData+Preferences.swift @@ -0,0 +1,39 @@ +// +// AppData+Preferences.swift +// Passepartout +// +// Created by Davide De Rosa on 12/6/24. +// Copyright (c) 2024 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 . +// + +import AppData +import CoreData +import Foundation + +extension AppData { + + @MainActor + public static let cdPreferencesModel: NSManagedObjectModel = { + guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else { + fatalError("Unable to build Core Data model (Preferences v3)") + } + return model + }() +} diff --git a/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift b/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift new file mode 100644 index 00000000..18d43570 --- /dev/null +++ b/Library/Sources/AppDataPreferences/Domain/CDModulePreferencesV3.swift @@ -0,0 +1,36 @@ +// +// CDModulePreferencesV3.swift +// Passepartout +// +// Created by Davide De Rosa on 12/5/24. +// Copyright (c) 2024 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 . +// + +import CoreData +import Foundation + +@objc(CDModulePreferencesV3) +final class CDModulePreferencesV3: NSManagedObject { + @nonobjc static func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "CDModulePreferencesV3") + } + + @NSManaged var uuid: UUID? +} diff --git a/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift b/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift new file mode 100644 index 00000000..2062996a --- /dev/null +++ b/Library/Sources/AppDataPreferences/Domain/CDProviderPreferencesV3.swift @@ -0,0 +1,37 @@ +// +// CDProviderPreferencesV3.swift +// Passepartout +// +// Created by Davide De Rosa on 12/5/24. +// Copyright (c) 2024 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 . +// + +import CoreData +import Foundation + +@objc(CDProviderPreferencesV3) +final class CDProviderPreferencesV3: NSManagedObject { + @nonobjc static func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "CDProviderPreferencesV3") + } + + @NSManaged var providerId: String? + @NSManaged var favoriteServerIds: Data? +} diff --git a/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents b/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents new file mode 100644 index 00000000..4ad3589c --- /dev/null +++ b/Library/Sources/AppDataPreferences/Preferences.xcdatamodeld/Preferences.xcdatamodel/contents @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift b/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift new file mode 100644 index 00000000..007fd49b --- /dev/null +++ b/Library/Sources/AppDataPreferences/Strategy/CDModulePreferencesRepositoryV3.swift @@ -0,0 +1,89 @@ +// +// CDModulePreferencesRepositoryV3.swift +// Passepartout +// +// Created by Davide De Rosa on 12/5/24. +// Copyright (c) 2024 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 . +// + +import AppData +import CommonLibrary +import CoreData +import Foundation +import PassepartoutKit + +extension AppData { + + @MainActor + public static func cdModulePreferencesRepositoryV3(context: NSManagedObjectContext) -> ModulePreferencesRepository { + CDModulePreferencesRepositoryV3(context: context) + } +} + +// MARK: - Repository + +private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository { + private nonisolated let context: NSManagedObjectContext + + init(context: NSManagedObjectContext) { + self.context = context + } + + func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy { + let entity = try context.performAndWait { + let request = CDModulePreferencesV3.fetchRequest() + request.predicate = NSPredicate(format: "uuid == %@", moduleId.uuidString) + do { + let entity = try request.execute().first ?? CDModulePreferencesV3(context: context) + entity.uuid = moduleId + return entity + } catch { + pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)") + throw error + } + } + return CDModulePreferencesProxy(context: context, entity: entity) + } +} + +// MARK: - Preference + +private final class CDModulePreferencesProxy: ModulePreferencesProxy { + private let context: NSManagedObjectContext + + private let entity: CDModulePreferencesV3 + + init(context: NSManagedObjectContext, entity: CDModulePreferencesV3) { + self.context = context + self.entity = entity + } + + func save() throws { + guard context.hasChanges else { + return + } + do { + try context.save() + } catch { + context.rollback() + throw error + } + } +} diff --git a/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift b/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift new file mode 100644 index 00000000..ace7bc1b --- /dev/null +++ b/Library/Sources/AppDataPreferences/Strategy/CDProviderPreferencesRepositoryV3.swift @@ -0,0 +1,114 @@ +// +// CDProviderPreferencesRepositoryV3.swift +// Passepartout +// +// Created by Davide De Rosa on 12/5/24. +// Copyright (c) 2024 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 . +// + +import AppData +import CommonLibrary +import CoreData +import Foundation +import PassepartoutKit + +extension AppData { + + @MainActor + public static func cdProviderPreferencesRepositoryV3(context: NSManagedObjectContext) -> ProviderPreferencesRepository { + CDProviderPreferencesRepositoryV3(context: context) + } +} + +// MARK: - Repository + +private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesRepository { + private nonisolated let context: NSManagedObjectContext + + init(context: NSManagedObjectContext) { + self.context = context + } + + func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy { + let entity = try context.performAndWait { + let request = CDProviderPreferencesV3.fetchRequest() + request.predicate = NSPredicate(format: "providerId == %@", providerId.rawValue) + do { + let entity = try request.execute().first ?? CDProviderPreferencesV3(context: context) + entity.providerId = providerId.rawValue + return entity + } catch { + pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)") + throw error + } + } + return CDProviderPreferencesProxy(context: context, entity: entity) + } +} + +// MARK: - Preference + +private final class CDProviderPreferencesProxy: ProviderPreferencesProxy { + private let context: NSManagedObjectContext + + private let entity: CDProviderPreferencesV3 + + init(context: NSManagedObjectContext, entity: CDProviderPreferencesV3) { + self.context = context + self.entity = entity + } + + var favoriteServers: Set { + get { + do { + return try context.performAndWait { + guard let data = entity.favoriteServerIds else { + return [] + } + return try JSONDecoder().decode(Set.self, from: data) + } + } catch { + pp_log(.app, .error, "Unable to get favoriteServers: \(error)") + return [] + } + } + set { + do { + try context.performAndWait { + entity.favoriteServerIds = try JSONEncoder().encode(newValue) + } + } catch { + pp_log(.app, .error, "Unable to set favoriteServers: \(error)") + } + } + } + + func save() throws { + guard context.hasChanges else { + return + } + do { + try context.save() + } catch { + context.rollback() + throw error + } + } +} diff --git a/Library/Sources/AppDataProfiles/Profiles.xcdatamodeld/ProfilesV3.xcdatamodel/contents b/Library/Sources/AppDataProfiles/Profiles.xcdatamodeld/ProfilesV3.xcdatamodel/contents index 05026434..2f65bbfd 100644 --- a/Library/Sources/AppDataProfiles/Profiles.xcdatamodeld/ProfilesV3.xcdatamodel/contents +++ b/Library/Sources/AppDataProfiles/Profiles.xcdatamodeld/ProfilesV3.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift b/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift index ef237453..0fff2ec9 100644 --- a/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift +++ b/Library/Sources/AppDataProviders/Strategy/CDProviderRepositoryV3.swift @@ -36,7 +36,7 @@ extension AppData { } } -actor CDProviderRepositoryV3: NSObject, ProviderRepository { +private actor CDProviderRepositoryV3: NSObject, ProviderRepository { private nonisolated let context: NSManagedObjectContext private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never> diff --git a/Library/Sources/AppUIMain/Views/Modules/DNSView.swift b/Library/Sources/AppUIMain/Views/Modules/DNSView.swift index 506cd1a1..7f2b8d11 100644 --- a/Library/Sources/AppUIMain/Views/Modules/DNSView.swift +++ b/Library/Sources/AppUIMain/Views/Modules/DNSView.swift @@ -33,10 +33,15 @@ struct DNSView: View, ModuleDraftEditing { @EnvironmentObject private var theme: Theme + let module: DNSModule.Builder + @ObservedObject var editor: ProfileEditor - let module: DNSModule.Builder + init(module: DNSModule.Builder, parameters: ModuleViewParameters) { + self.module = module + editor = parameters.editor + } var body: some View { debugChanges() diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/DNSModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/DNSModule+Extensions.swift index d51bd724..46c96ffe 100644 --- a/Library/Sources/AppUIMain/Views/Modules/Extensions/DNSModule+Extensions.swift +++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/DNSModule+Extensions.swift @@ -23,12 +23,13 @@ // along with Passepartout. If not, see . // +import CommonLibrary import PassepartoutKit import SwiftUI import UILibrary extension DNSModule.Builder: ModuleViewProviding { - public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { - DNSView(editor: editor, module: self) + public func moduleView(with parameters: ModuleViewParameters) -> some View { + DNSView(module: self, parameters: parameters) } } diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/HTTPProxyModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/HTTPProxyModule+Extensions.swift index 0b331206..6ef4f0e2 100644 --- a/Library/Sources/AppUIMain/Views/Modules/Extensions/HTTPProxyModule+Extensions.swift +++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/HTTPProxyModule+Extensions.swift @@ -23,12 +23,13 @@ // along with Passepartout. If not, see . // +import CommonLibrary import PassepartoutKit import SwiftUI import UILibrary extension HTTPProxyModule.Builder: ModuleViewProviding { - public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { - HTTPProxyView(editor: editor, module: self) + public func moduleView(with parameters: ModuleViewParameters) -> some View { + HTTPProxyView(module: self, parameters: parameters) } } diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/IPModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/IPModule+Extensions.swift index 62b381f4..fffb89e9 100644 --- a/Library/Sources/AppUIMain/Views/Modules/Extensions/IPModule+Extensions.swift +++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/IPModule+Extensions.swift @@ -23,12 +23,13 @@ // along with Passepartout. If not, see . // +import CommonLibrary import PassepartoutKit import SwiftUI import UILibrary extension IPModule.Builder: ModuleViewProviding { - public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { - IPView(editor: editor, module: self) + public func moduleView(with parameters: ModuleViewParameters) -> some View { + IPView(module: self, parameters: parameters) } } diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/OnDemandModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/OnDemandModule+Extensions.swift index 49d21ae7..88b862b8 100644 --- a/Library/Sources/AppUIMain/Views/Modules/Extensions/OnDemandModule+Extensions.swift +++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/OnDemandModule+Extensions.swift @@ -28,7 +28,7 @@ import SwiftUI import UILibrary extension OnDemandModule.Builder: ModuleViewProviding { - public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { - OnDemandView(editor: editor, module: self) + public func moduleView(with parameters: ModuleViewParameters) -> some View { + OnDemandView(module: self, parameters: parameters) } } diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift index bf3dcb6f..5b510c61 100644 --- a/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift +++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/OpenVPNModule+Extensions.swift @@ -29,8 +29,8 @@ import SwiftUI import UILibrary extension OpenVPNModule.Builder: ModuleViewProviding { - public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { - OpenVPNView(editor: editor, module: self, impl: impl as? OpenVPNModule.Implementation) + public func moduleView(with parameters: ModuleViewParameters) -> some View { + OpenVPNView(module: self, parameters: parameters) } } diff --git a/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift b/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift index 347a673b..cf1d8014 100644 --- a/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift +++ b/Library/Sources/AppUIMain/Views/Modules/Extensions/WireGuardModule+Extensions.swift @@ -29,8 +29,8 @@ import SwiftUI import UILibrary extension WireGuardModule.Builder: ModuleViewProviding { - public func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> some View { - WireGuardView(editor: editor, module: self, impl: impl as? WireGuardModule.Implementation) + public func moduleView(with parameters: ModuleViewParameters) -> some View { + WireGuardView(module: self, parameters: parameters) } } diff --git a/Library/Sources/AppUIMain/Views/Modules/HTTPProxyView.swift b/Library/Sources/AppUIMain/Views/Modules/HTTPProxyView.swift index 5656d264..dfdde2a9 100644 --- a/Library/Sources/AppUIMain/Views/Modules/HTTPProxyView.swift +++ b/Library/Sources/AppUIMain/Views/Modules/HTTPProxyView.swift @@ -32,10 +32,15 @@ struct HTTPProxyView: View, ModuleDraftEditing { @EnvironmentObject private var theme: Theme + let module: HTTPProxyModule.Builder + @ObservedObject var editor: ProfileEditor - let module: HTTPProxyModule.Builder + init(module: HTTPProxyModule.Builder, parameters: ModuleViewParameters) { + self.module = module + editor = parameters.editor + } var body: some View { Group { diff --git a/Library/Sources/AppUIMain/Views/Modules/IPView.swift b/Library/Sources/AppUIMain/Views/Modules/IPView.swift index 4296b11b..3703750b 100644 --- a/Library/Sources/AppUIMain/Views/Modules/IPView.swift +++ b/Library/Sources/AppUIMain/Views/Modules/IPView.swift @@ -28,15 +28,19 @@ import PassepartoutKit import SwiftUI struct IPView: View, ModuleDraftEditing { + let module: IPModule.Builder @ObservedObject var editor: ProfileEditor - let module: IPModule.Builder - @State private var routePresentation: RoutePresentation? + init(module: IPModule.Builder, parameters: ModuleViewParameters) { + self.module = module + editor = parameters.editor + } + var body: some View { Group { ipSections(for: .v4) diff --git a/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift b/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift index 92a90d37..f027a843 100644 --- a/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift +++ b/Library/Sources/AppUIMain/Views/Modules/OnDemandView.swift @@ -33,23 +33,23 @@ struct OnDemandView: View, ModuleDraftEditing { @EnvironmentObject private var theme: Theme + let module: OnDemandModule.Builder + @ObservedObject var editor: ProfileEditor - let module: OnDemandModule.Builder - private let wifi: Wifi @State private var paywallReason: PaywallReason? init( - editor: ProfileEditor, module: OnDemandModule.Builder, + parameters: ModuleViewParameters, observer: WifiObserver? = nil ) { - self.editor = editor self.module = module + editor = parameters.editor wifi = Wifi(observer: observer ?? CoreLocationWifiObserver()) } @@ -239,8 +239,12 @@ private extension OnDemandView { ] return module.preview { OnDemandView( - editor: $0, - module: $1, + module: $0, + parameters: .init( + editor: $1, + preferences: nil, + impl: nil + ), observer: MockWifi() ) } diff --git a/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift b/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift index 9e7e74c3..b2aa002f 100644 --- a/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift +++ b/Library/Sources/AppUIMain/Views/Modules/OpenVPNView.swift @@ -33,11 +33,11 @@ struct OpenVPNView: View, ModuleDraftEditing { @Environment(\.navigationPath) private var path + let module: OpenVPNModule.Builder + @ObservedObject var editor: ProfileEditor - let module: OpenVPNModule.Builder - let impl: OpenVPNModule.Implementation? private let isServerPushed: Bool @@ -61,20 +61,18 @@ struct OpenVPNView: View, ModuleDraftEditing { private var errorHandler: ErrorHandler = .default() init(serverConfiguration: OpenVPN.Configuration) { - let module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder()) - let editor = ProfileEditor(modules: [module]) + module = OpenVPNModule.Builder(configurationBuilder: serverConfiguration.builder()) + editor = ProfileEditor(modules: [module]) assert(module.configurationBuilder != nil, "isServerPushed must imply module.configurationBuilder != nil") - self.editor = editor - self.module = module impl = nil isServerPushed = true } - init(editor: ProfileEditor, module: OpenVPNModule.Builder, impl: OpenVPNModule.Implementation?) { - self.editor = editor + init(module: OpenVPNModule.Builder, parameters: ModuleViewParameters) { self.module = module - self.impl = impl + editor = parameters.editor + impl = parameters.impl as? OpenVPNModule.Implementation isServerPushed = false } diff --git a/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift b/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift index 025189b0..5ac8bdc5 100644 --- a/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift +++ b/Library/Sources/AppUIMain/Views/Modules/WireGuardView.swift @@ -33,11 +33,11 @@ struct WireGuardView: View, ModuleDraftEditing { @Environment(\.navigationPath) private var path + let module: WireGuardModule.Builder + @ObservedObject var editor: ProfileEditor - let module: WireGuardModule.Builder - let impl: WireGuardModule.Implementation? @State @@ -46,6 +46,12 @@ struct WireGuardView: View, ModuleDraftEditing { @State private var errorHandler: ErrorHandler = .default() + init(module: WireGuardModule.Builder, parameters: ModuleViewParameters) { + self.module = module + editor = parameters.editor + impl = parameters.impl as? WireGuardModule.Implementation + } + var body: some View { contentView .moduleView(editor: editor, draft: draft.wrappedValue) diff --git a/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift b/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift index 02a138f9..d13fe879 100644 --- a/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift +++ b/Library/Sources/AppUIMain/Views/Profile/ModuleDetailView.swift @@ -23,16 +23,24 @@ // along with Passepartout. If not, see . // +import CommonLibrary import PassepartoutKit import SwiftUI struct ModuleDetailView: View { + + @EnvironmentObject + private var preferencesManager: PreferencesManager + let profileEditor: ProfileEditor let moduleId: UUID? let moduleViewFactory: any ModuleViewFactory + @StateObject + private var modulePreferences = ModulePreferences(proxy: nil) + var body: some View { debugChanges() return Group { @@ -42,6 +50,16 @@ struct ModuleDetailView: View { emptyView } } + .onLoad { + guard let moduleId else { + return + } + do { + modulePreferences.proxy = try preferencesManager.modulePreferencesProxy(in: moduleId) + } catch { + pp_log(.app, .error, "Unable to load module preferences: \(error)") + } + } } } @@ -49,7 +67,11 @@ private extension ModuleDetailView { @MainActor func editorView(forModuleWithId moduleId: UUID) -> some View { - AnyView(moduleViewFactory.view(with: profileEditor, moduleId: moduleId)) + AnyView(moduleViewFactory.view( + with: profileEditor, + preferences: modulePreferences, + moduleId: moduleId + )) } var emptyView: some View { diff --git a/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift b/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift index 0d59b032..c1a0b90f 100644 --- a/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift +++ b/Library/Sources/AppUIMain/Views/VPN/VPNProviderServerView.swift @@ -34,6 +34,9 @@ struct VPNProviderServerView: View where Configuration: Identifia @EnvironmentObject private var providerManager: ProviderManager + @EnvironmentObject + private var preferencesManager: PreferencesManager + var apis: [APIMapper] = API.shared let moduleId: UUID @@ -65,10 +68,10 @@ struct VPNProviderServerView: View where Configuration: Identifia private var onlyShowsFavorites = false @StateObject - private var filtersViewModel = VPNFiltersView.Model() + private var providerPreferences = ProviderPreferences(proxy: nil) @StateObject - private var favoritesManager = ProviderFavoritesManager() + private var filtersViewModel = VPNFiltersView.Model() @StateObject private var errorHandler: ErrorHandler = .default() @@ -94,7 +97,7 @@ extension VPNProviderServerView { selectedServer: selectedEntity?.server, isFiltering: isFiltering, filtersViewModel: filtersViewModel, - favoritesManager: favoritesManager, + providerPreferences: providerPreferences, selectTitle: selectTitle, onSelect: onSelectServer ) @@ -123,7 +126,7 @@ private extension VPNProviderServerView { var filteredServers: [VPNServer] { if onlyShowsFavorites { return servers.filter { - favoritesManager.serverIds.contains($0.serverId) + providerPreferences.favoriteServers.contains($0.serverId) } } return servers @@ -156,7 +159,11 @@ private extension VPNProviderServerView { private extension VPNProviderServerView { func loadInitialServers() async { do { - favoritesManager.moduleId = moduleId + providerPreferences.proxy = try preferencesManager.providerPreferencesProxy(in: providerId) + } catch { + pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)") + } + do { let repository = try await providerManager.vpnServerRepository( from: apis, for: providerId @@ -165,7 +172,7 @@ private extension VPNProviderServerView { filtersViewModel.load(options: vpnManager.options, initialFilters: initialFilters) await reloadServers(filters: filtersViewModel.filters) } catch { - pp_log(.app, .error, "Unable to load VPN repository: \(error)") + pp_log(.app, .error, "Unable to load VPN servers for provider \(providerId): \(error)") errorHandler.handle(error, title: Strings.Global.Nouns.servers) } } @@ -194,7 +201,11 @@ private extension VPNProviderServerView { } func onDisappear() { - favoritesManager.save() + do { + try providerPreferences.save() + } catch { + pp_log(.app, .error, "Unable to save preferences: \(error)") + } } func onSelectServer(_ server: VPNServer) { diff --git a/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift b/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift index e06458ce..a36a394d 100644 --- a/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift +++ b/Library/Sources/AppUIMain/Views/VPN/iOS/VPNProviderServer+Content+iOS.swift @@ -46,7 +46,7 @@ extension VPNProviderServerView { var filtersViewModel: VPNFiltersView.Model @ObservedObject - var favoritesManager: ProviderFavoritesManager + var providerPreferences: ProviderPreferences let selectTitle: String @@ -151,7 +151,7 @@ private extension VPNProviderServerView.ContentView { Spacer() FavoriteToggle( value: server.serverId, - selection: $favoritesManager.serverIds + selection: $providerPreferences.favoriteServers ) } } diff --git a/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift b/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift index 1af171b9..5be6bf19 100644 --- a/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift +++ b/Library/Sources/AppUIMain/Views/VPN/macOS/VPNProviderServer+Content+macOS.swift @@ -50,7 +50,7 @@ extension VPNProviderServerView { var filtersViewModel: VPNFiltersView.Model @ObservedObject - var favoritesManager: ProviderFavoritesManager + var providerPreferences: ProviderPreferences let selectTitle: String @@ -87,7 +87,7 @@ private extension VPNProviderServerView.ContentView { TableColumn("􀋂") { server in FavoriteToggle( value: server.serverId, - selection: $favoritesManager.serverIds + selection: $providerPreferences.favoriteServers ) .environmentObject(theme) // TODO: #873, Table loses environment } diff --git a/Library/Sources/CommonLibrary/Business/PreferencesManager.swift b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift new file mode 100644 index 00000000..8db9d4f5 --- /dev/null +++ b/Library/Sources/CommonLibrary/Business/PreferencesManager.swift @@ -0,0 +1,77 @@ +// +// PreferencesManager.swift +// Passepartout +// +// Created by Davide De Rosa on 12/4/24. +// Copyright (c) 2024 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 . +// + +import CommonUtils +import Foundation +import PassepartoutKit + +@MainActor +public final class PreferencesManager: ObservableObject { + private let modulesRepository: ModulePreferencesRepository + + private let providersRepository: ProviderPreferencesRepository + + public init( + modulesRepository: ModulePreferencesRepository? = nil, + providersRepository: ProviderPreferencesRepository? = nil + ) { + self.modulesRepository = modulesRepository ?? DummyModulePreferencesRepository() + self.providersRepository = providersRepository ?? DummyProviderPreferencesRepository() + } + + public func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy { + try modulesRepository.modulePreferencesProxy(in: moduleId) + } + + public func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy { + try providersRepository.providerPreferencesProxy(in: providerId) + } +} + +// MARK: - Dummy + +private final class DummyModulePreferencesRepository: ModulePreferencesRepository { + private final class Proxy: ModulePreferencesProxy { + func save() throws { + } + } + + func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy { + Proxy() + } +} + +private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository { + private final class Proxy: ProviderPreferencesProxy { + var favoriteServers: Set = [] + + func save() throws { + } + } + + func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy { + Proxy() + } +} diff --git a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift index 5a35cb56..5dc30bfd 100644 --- a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift +++ b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+AppGroup.swift @@ -30,25 +30,35 @@ import PassepartoutKit extension BundleConfiguration { public static var urlForAppLog: URL { - cachesURL.appending(path: Constants.shared.log.appPath) + urlForGroupCaches.appending(path: Constants.shared.log.appPath) } public static var urlForTunnelLog: URL { - cachesURL.appending(path: Constants.shared.log.tunnelPath) + urlForGroupCaches.appending(path: Constants.shared.log.tunnelPath) } public static var urlForBetaReceipt: URL { - cachesURL.appending(path: Constants.shared.tunnel.betaReceiptPath) + urlForGroupCaches.appending(path: Constants.shared.tunnel.betaReceiptPath) + } +} + +extension BundleConfiguration { + public static var urlForGroupCaches: URL { + appGroupURL.appending(components: "Library", "Caches") + } + + public static var urlForGroupDocuments: URL { + appGroupURL.appending(components: "Library", "Documents") } } private extension BundleConfiguration { - static var cachesURL: URL { + static var appGroupURL: URL { let groupId = mainString(for: .groupId) guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupId) else { pp_log(.app, .error, "Unable to access App Group container") return FileManager.default.temporaryDirectory } - return url.appending(components: "Library", "Caches") + return url } } diff --git a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift index d49150e4..9a9272e3 100644 --- a/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift +++ b/Library/Sources/CommonLibrary/Domain/BundleConfiguration+Main.swift @@ -34,6 +34,8 @@ extension BundleConfiguration { case cloudKitId + case cloudKitPreferencesId + case userLevel case groupId diff --git a/Library/Sources/CommonLibrary/Domain/Constants.swift b/Library/Sources/CommonLibrary/Domain/Constants.swift index d67517b9..83319b8e 100644 --- a/Library/Sources/CommonLibrary/Domain/Constants.swift +++ b/Library/Sources/CommonLibrary/Domain/Constants.swift @@ -28,12 +28,14 @@ import PassepartoutKit public struct Constants: Decodable, Sendable { public struct Containers: Decodable, Sendable { - public let local: String + public let localProfiles: String - public let remote: String + public let remoteProfiles: String public let providers: String + public let preferences: String + public let legacyV2: String public let legacyV2TV: String diff --git a/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift b/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift new file mode 100644 index 00000000..5a5ad185 --- /dev/null +++ b/Library/Sources/CommonLibrary/Domain/ModulePreferences.swift @@ -0,0 +1,49 @@ +// +// ModulePreferences.swift +// Passepartout +// +// Created by Davide De Rosa on 12/5/24. +// Copyright (c) 2024 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 . +// + +import Foundation +import PassepartoutKit + +@MainActor +public final class ModulePreferences: ObservableObject { + public var proxy: ModulePreferencesProxy? { + didSet { + objectWillChange.send() + } + } + + public init(proxy: ModulePreferencesProxy?) { + self.proxy = proxy + } + + public func save() throws { + try proxy?.save() + } +} + +@MainActor +public protocol ModulePreferencesProxy { + func save() throws +} diff --git a/Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift b/Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift new file mode 100644 index 00000000..81f42bc8 --- /dev/null +++ b/Library/Sources/CommonLibrary/Domain/ProviderPreferences.swift @@ -0,0 +1,61 @@ +// +// ProviderPreferences.swift +// Passepartout +// +// Created by Davide De Rosa on 12/5/24. +// Copyright (c) 2024 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 . +// + +import Foundation +import PassepartoutKit + +@MainActor +public final class ProviderPreferences: ObservableObject { + public var proxy: ProviderPreferencesProxy? { + didSet { + objectWillChange.send() + } + } + + public init(proxy: ProviderPreferencesProxy?) { + self.proxy = proxy + } + + public var favoriteServers: Set { + get { + proxy?.favoriteServers ?? [] + } + set { + objectWillChange.send() + proxy?.favoriteServers = newValue + } + } + + public func save() throws { + try proxy?.save() + } +} + +@MainActor +public protocol ProviderPreferencesProxy { + var favoriteServers: Set { get set } + + func save() throws +} diff --git a/Library/Sources/CommonLibrary/Resources/Constants.json b/Library/Sources/CommonLibrary/Resources/Constants.json index d4a1e6e9..677fea5c 100644 --- a/Library/Sources/CommonLibrary/Resources/Constants.json +++ b/Library/Sources/CommonLibrary/Resources/Constants.json @@ -1,9 +1,10 @@ { "bundleKey": "AppConfig", "containers": { - "local": "Profiles-v3", - "remote": "Profiles-v3.remote", + "localProfiles": "Profiles-v3", + "remoteProfiles": "Profiles-v3.remote", "providers": "Providers-v3", + "preferences": "Preferences-v3", "legacyV2": "Profiles", "legacyV2TV": "SharedProfiles" }, diff --git a/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift b/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift new file mode 100644 index 00000000..5f911a29 --- /dev/null +++ b/Library/Sources/CommonLibrary/Strategy/ModulePreferencesRepository.swift @@ -0,0 +1,31 @@ +// +// ModulePreferencesRepository.swift +// Passepartout +// +// Created by Davide De Rosa on 12/5/24. +// Copyright (c) 2024 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 . +// + +import Foundation + +@MainActor +public protocol ModulePreferencesRepository { + func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy +} diff --git a/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift b/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift new file mode 100644 index 00000000..7454a77d --- /dev/null +++ b/Library/Sources/CommonLibrary/Strategy/ProviderPreferencesRepository.swift @@ -0,0 +1,32 @@ +// +// ProviderPreferencesRepository.swift +// Passepartout +// +// Created by Davide De Rosa on 12/5/24. +// Copyright (c) 2024 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 . +// + +import Foundation +import PassepartoutKit + +@MainActor +public protocol ProviderPreferencesRepository { + func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy +} diff --git a/Library/Sources/UILibrary/Domain/ProviderFavoriteServers.swift b/Library/Sources/CommonUtils/Extensions/UUID+RawRepresentable.swift similarity index 55% rename from Library/Sources/UILibrary/Domain/ProviderFavoriteServers.swift rename to Library/Sources/CommonUtils/Extensions/UUID+RawRepresentable.swift index 14dee1a7..2c1c2fe0 100644 --- a/Library/Sources/UILibrary/Domain/ProviderFavoriteServers.swift +++ b/Library/Sources/CommonUtils/Extensions/UUID+RawRepresentable.swift @@ -1,8 +1,8 @@ // -// ProviderFavoriteServers.swift +// UUID+RawRepresentable.swift // Passepartout // -// Created by Davide De Rosa on 10/25/24. +// Created by Davide De Rosa on 12/4/24. // Copyright (c) 2024 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn @@ -25,31 +25,12 @@ import Foundation -public struct ProviderFavoriteServers { - private var map: [UUID: Set] - - public init() { - map = [:] - } - - public func servers(forModuleWithId moduleId: UUID) -> Set { - map[moduleId] ?? [] - } - - public mutating func setServers(_ servers: Set, forModuleWithId moduleId: UUID) { - map[moduleId] = servers - } -} - -extension ProviderFavoriteServers: RawRepresentable { - public var rawValue: String { - (try? JSONEncoder().encode(map))?.base64EncodedString() ?? "" - } - +extension UUID: @retroactive RawRepresentable { public init?(rawValue: String) { - guard let data = Data(base64Encoded: rawValue) else { - return nil - } - map = (try? JSONDecoder().decode([UUID: Set].self, from: data)) ?? [:] + self.init(uuidString: rawValue) + } + + public var rawValue: String { + uuidString } } diff --git a/Library/Sources/UILibrary/Business/AppContext.swift b/Library/Sources/UILibrary/Business/AppContext.swift index 4b48c642..d0901307 100644 --- a/Library/Sources/UILibrary/Business/AppContext.swift +++ b/Library/Sources/UILibrary/Business/AppContext.swift @@ -40,6 +40,8 @@ public final class AppContext: ObservableObject { public let providerManager: ProviderManager + public let preferencesManager: PreferencesManager + public let registry: Registry public let tunnel: ExtendedTunnel @@ -57,6 +59,7 @@ public final class AppContext: ObservableObject { migrationManager: MigrationManager, profileManager: ProfileManager, providerManager: ProviderManager, + preferencesManager: PreferencesManager, registry: Registry, tunnel: ExtendedTunnel, tunnelReceiptURL: URL @@ -65,6 +68,7 @@ public final class AppContext: ObservableObject { self.migrationManager = migrationManager self.profileManager = profileManager self.providerManager = providerManager + self.preferencesManager = preferencesManager self.registry = registry self.tunnel = tunnel self.tunnelReceiptURL = tunnelReceiptURL diff --git a/Library/Sources/UILibrary/Business/ProviderFavoritesManager.swift b/Library/Sources/UILibrary/Business/ProviderFavoritesManager.swift deleted file mode 100644 index a41858f8..00000000 --- a/Library/Sources/UILibrary/Business/ProviderFavoritesManager.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// ProviderFavoritesManager.swift -// Passepartout -// -// Created by Davide De Rosa on 10/26/24. -// Copyright (c) 2024 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 . -// - -import CommonLibrary -import Foundation - -@MainActor -public final class ProviderFavoritesManager: ObservableObject { - private let defaults: UserDefaults - - private var allFavorites: ProviderFavoriteServers - - public var moduleId: UUID { - didSet { - guard let rawValue = defaults.string(forKey: UIPreference.providerFavoriteServers.key) else { - allFavorites = ProviderFavoriteServers() - return - } - allFavorites = ProviderFavoriteServers(rawValue: rawValue) ?? ProviderFavoriteServers() - } - } - - public var serverIds: Set { - get { - allFavorites.servers(forModuleWithId: moduleId) - } - set { - objectWillChange.send() - allFavorites.setServers(newValue, forModuleWithId: moduleId) - } - } - - public init(defaults: UserDefaults = .standard) { - self.defaults = defaults - allFavorites = ProviderFavoriteServers() - moduleId = UUID() - } - - public func save() { - defaults.set(allFavorites.rawValue, forKey: UIPreference.providerFavoriteServers.key) - } -} diff --git a/Library/Sources/UILibrary/Domain/UIPreference.swift b/Library/Sources/UILibrary/Domain/UIPreference.swift index a6b1e0cd..f246df3b 100644 --- a/Library/Sources/UILibrary/Domain/UIPreference.swift +++ b/Library/Sources/UILibrary/Domain/UIPreference.swift @@ -37,8 +37,6 @@ public enum UIPreference: String, PreferenceProtocol { case profilesLayout - case providerFavoriteServers - public var key: String { "UI.\(rawValue)" } diff --git a/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift b/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift index 54189a31..49610f19 100644 --- a/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift +++ b/Library/Sources/UILibrary/Extensions/ModuleBuilder+Previews.swift @@ -31,16 +31,20 @@ extension ModuleBuilder where Self: ModuleViewProviding { @MainActor public func preview(title: String = "") -> some View { NavigationStack { - moduleView(with: ProfileEditor(modules: [self]), impl: nil) - .navigationTitle(title) + moduleView(with: .init( + editor: ProfileEditor(modules: [self]), + preferences: nil, + impl: nil + )) + .navigationTitle(title) } .withMockEnvironment() } @MainActor - public func preview(with content: (ProfileEditor, Self) -> C) -> some View { + public func preview(with content: (Self, ProfileEditor) -> C) -> some View { NavigationStack { - content(ProfileEditor(modules: [self]), self) + content(self, ProfileEditor(modules: [self])) } .withMockEnvironment() } diff --git a/Library/Sources/UILibrary/Extensions/View+Environment.swift b/Library/Sources/UILibrary/Extensions/View+Environment.swift index 04a2c08a..22925468 100644 --- a/Library/Sources/UILibrary/Extensions/View+Environment.swift +++ b/Library/Sources/UILibrary/Extensions/View+Environment.swift @@ -31,6 +31,7 @@ extension View { environmentObject(theme) .environmentObject(context.iapManager) .environmentObject(context.migrationManager) + .environmentObject(context.preferencesManager) .environmentObject(context.providerManager) } diff --git a/Library/Sources/UILibrary/Previews/AppContext+Previews.swift b/Library/Sources/UILibrary/Previews/AppContext+Previews.swift index 40a003ab..acd8d2d5 100644 --- a/Library/Sources/UILibrary/Previews/AppContext+Previews.swift +++ b/Library/Sources/UILibrary/Previews/AppContext+Previews.swift @@ -83,6 +83,7 @@ extension AppContext { migrationManager: migrationManager, profileManager: profileManager, providerManager: providerManager, + preferencesManager: PreferencesManager(), registry: Registry(), tunnel: tunnel, tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt diff --git a/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift b/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift index 502bd528..b41d7763 100644 --- a/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift +++ b/Library/Sources/UILibrary/Strategy/ModuleViewFactory+Default.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +import CommonLibrary import Foundation import PassepartoutKit import SwiftUI @@ -35,11 +36,15 @@ public final class DefaultModuleViewFactory: ModuleViewFactory { } @ViewBuilder - public func view(with editor: ProfileEditor, moduleId: UUID) -> some View { + public func view(with editor: ProfileEditor, preferences: ModulePreferences, moduleId: UUID) -> some View { let result = editor.moduleViewProvider(withId: moduleId, registry: registry) if let result { - AnyView(result.provider.moduleView(with: editor, impl: result.impl)) - .navigationTitle(result.title) + AnyView(result.provider.moduleView(with: .init( + editor: editor, + preferences: preferences, + impl: result.impl + ))) + .navigationTitle(result.title) } } } diff --git a/Library/Sources/UILibrary/Strategy/ModuleViewFactory.swift b/Library/Sources/UILibrary/Strategy/ModuleViewFactory.swift index 620b3c33..1b46d1ae 100644 --- a/Library/Sources/UILibrary/Strategy/ModuleViewFactory.swift +++ b/Library/Sources/UILibrary/Strategy/ModuleViewFactory.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +import CommonLibrary import Foundation import SwiftUI @@ -30,5 +31,5 @@ public protocol ModuleViewFactory: AnyObject { associatedtype Content: View @MainActor - func view(with editor: ProfileEditor, moduleId: UUID) -> Content + func view(with editor: ProfileEditor, preferences: ModulePreferences, moduleId: UUID) -> Content } diff --git a/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift b/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift index e0b07b86..bfc47208 100644 --- a/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift +++ b/Library/Sources/UILibrary/Strategy/ModuleViewProviding.swift @@ -23,6 +23,7 @@ // along with Passepartout. If not, see . // +import CommonLibrary import PassepartoutKit import SwiftUI @@ -30,5 +31,24 @@ public protocol ModuleViewProviding { associatedtype Content: View @MainActor - func moduleView(with editor: ProfileEditor, impl: ModuleImplementation?) -> Content + func moduleView(with parameters: ModuleViewParameters) -> Content +} + +public struct ModuleViewParameters { + public let editor: ProfileEditor + + public let preferences: ModulePreferences + + public let impl: (any ModuleImplementation)? + + @MainActor + public init( + editor: ProfileEditor, + preferences: ModulePreferences?, + impl: (any ModuleImplementation)? + ) { + self.editor = editor + self.preferences = preferences ?? ModulePreferences(proxy: nil) + self.impl = impl + } } diff --git a/Passepartout/App/App.entitlements b/Passepartout/App/App.entitlements index e567aaa8..1f592f6a 100644 --- a/Passepartout/App/App.entitlements +++ b/Passepartout/App/App.entitlements @@ -9,6 +9,7 @@ com.apple.developer.icloud-container-identifiers $(CFG_CLOUDKIT_ID) + $(CFG_CLOUDKIT_PREFERENCES_ID) $(CFG_LEGACY_V2_CLOUDKIT_ID) $(CFG_LEGACY_V2_TV_CLOUDKIT_ID) diff --git a/Passepartout/App/App.plist b/Passepartout/App/App.plist index b2e85be7..35ccd315 100644 --- a/Passepartout/App/App.plist +++ b/Passepartout/App/App.plist @@ -8,6 +8,8 @@ $(CFG_APP_STORE_ID) cloudKitId $(CFG_CLOUDKIT_ID) + cloudKitPreferencesId + $(CFG_CLOUDKIT_PREFERENCES_ID) groupId $(CFG_GROUP_ID) iapBundlePrefix diff --git a/Passepartout/Config.xcconfig b/Passepartout/Config.xcconfig index d31e9408..8156f511 100644 --- a/Passepartout/Config.xcconfig +++ b/Passepartout/Config.xcconfig @@ -29,6 +29,7 @@ CFG_APP_ID = com.algoritmico.ios.Passepartout CFG_APP_STORE_ID = 1433648537 CFG_CLOUDKIT_ID = iCloud.com.algoritmico.Passepartout.v3 +CFG_CLOUDKIT_PREFERENCES_ID = iCloud.com.algoritmico.Passepartout.v3.Preferences CFG_COPYRIGHT = Copyright © 2024 Davide De Rosa. All rights reserved. CFG_DISPLAY_NAME = Passepartout CFG_GROUP_ID[sdk=appletvos*] = $(CFG_RAW_GROUP_ID) diff --git a/Passepartout/Shared/AppContext+Shared.swift b/Passepartout/Shared/AppContext+Shared.swift index dbc2593d..fd2beef1 100644 --- a/Passepartout/Shared/AppContext+Shared.swift +++ b/Passepartout/Shared/AppContext+Shared.swift @@ -48,7 +48,7 @@ extension AppContext { let remoteRepositoryBlock: (Bool) -> ProfileRepository = { let remoteStore = CoreDataPersistentStore( logger: .default, - containerName: Constants.shared.containers.remote, + containerName: Constants.shared.containers.remoteProfiles, model: AppData.cdProfilesModel, cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil, author: nil @@ -124,6 +124,7 @@ extension AppContext { migrationManager: migrationManager, profileManager: profileManager, providerManager: providerManager, + preferencesManager: .shared, registry: .shared, tunnel: tunnel, tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt @@ -199,7 +200,7 @@ private extension Dependencies.ProfileManager { static func coreDataProfileRepository(observingResults: Bool) -> ProfileRepository { let store = CoreDataPersistentStore( logger: .default, - containerName: Constants.shared.containers.local, + containerName: Constants.shared.containers.localProfiles, model: AppData.cdProfilesModel, cloudKitIdentifier: nil, author: nil @@ -215,21 +216,3 @@ private extension Dependencies.ProfileManager { } } } - -// MARK: - Logging - -private extension CoreDataPersistentStoreLogger where Self == DefaultCoreDataPersistentStoreLogger { - static var `default`: CoreDataPersistentStoreLogger { - DefaultCoreDataPersistentStoreLogger() - } -} - -private struct DefaultCoreDataPersistentStoreLogger: CoreDataPersistentStoreLogger { - func debug(_ msg: String) { - pp_log(.app, .info, msg) - } - - func warning(_ msg: String) { - pp_log(.app, .error, msg) - } -} diff --git a/Passepartout/Shared/Shared.swift b/Passepartout/Shared/Shared.swift index a757d960..002789b4 100644 --- a/Passepartout/Shared/Shared.swift +++ b/Passepartout/Shared/Shared.swift @@ -23,6 +23,8 @@ // along with Passepartout. If not, see . // +import AppData +import AppDataPreferences import CommonLibrary import CommonUtils import CPassepartoutOpenVPNOpenSSL @@ -30,8 +32,6 @@ import Foundation import PassepartoutKit import PassepartoutWireGuardGo -// MARK: Registry - extension Registry { static let shared = Registry( withKnownHandlers: true, @@ -130,3 +130,40 @@ extension InAppProcessor { ) } } + +extension PreferencesManager { + static let shared: PreferencesManager = { + let preferencesStore = CoreDataPersistentStore( + logger: .default, + containerName: Constants.shared.containers.preferences, + baseURL: BundleConfiguration.urlForGroupDocuments, + model: AppData.cdPreferencesModel, + cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitPreferencesId), + author: nil + ) + let modulePreferencesRepository = AppData.cdModulePreferencesRepositoryV3(context: preferencesStore.context) + let providerPreferencesRepository = AppData.cdProviderPreferencesRepositoryV3(context: preferencesStore.context) + return PreferencesManager( + modulesRepository: modulePreferencesRepository, + providersRepository: providerPreferencesRepository + ) + }() +} + +// MARK: - Logging + +extension CoreDataPersistentStoreLogger where Self == DefaultCoreDataPersistentStoreLogger { + static var `default`: CoreDataPersistentStoreLogger { + DefaultCoreDataPersistentStoreLogger() + } +} + +struct DefaultCoreDataPersistentStoreLogger: CoreDataPersistentStoreLogger { + func debug(_ msg: String) { + pp_log(.app, .info, msg) + } + + func warning(_ msg: String) { + pp_log(.app, .error, msg) + } +} diff --git a/Passepartout/Shared/Testing/AppContext+Testing.swift b/Passepartout/Shared/Testing/AppContext+Testing.swift index e84397e3..1452a7ae 100644 --- a/Passepartout/Shared/Testing/AppContext+Testing.swift +++ b/Passepartout/Shared/Testing/AppContext+Testing.swift @@ -65,6 +65,7 @@ extension AppContext { migrationManager: migrationManager, profileManager: profileManager, providerManager: providerManager, + preferencesManager: PreferencesManager(), registry: registry, tunnel: tunnel, tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt diff --git a/Passepartout/Tunnel/Tunnel.plist b/Passepartout/Tunnel/Tunnel.plist index db76be5d..22a65386 100644 --- a/Passepartout/Tunnel/Tunnel.plist +++ b/Passepartout/Tunnel/Tunnel.plist @@ -10,6 +10,8 @@ $(CFG_KEYCHAIN_GROUP_ID) tunnelId $(CFG_TUNNEL_ID) + cloudKitPreferencesId + $(CFG_CLOUDKIT_PREFERENCES_ID) NSExtension