From f7013a98a9d053f828eb009d4646e78b61daad17 Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 8 Dec 2024 18:56:39 +0100 Subject: [PATCH] Separate App/Tunnel responsibilities (#984) Sort out the increasing mess coming from: - AppContext* - Dependencies - Shared* by doing the following: - Keep in the "Shared" folder only the entities actually shared by App/Tunnel - Create TunnelContext - Move AppContext and related to the App/Context folder - Move TunnelContext and related to the Tunnel/Context folder - Delete Shared+* extensions, use AppContext/TunnelContext singletons from the app - Create a Dependencies factory singleton to create entities in a single place - Split extensions by domain - Make it clear with `func` vs `var` when a dependency method returns a new instance --- .../CommonLibrary/Strategy/Processors.swift | 6 +- .../UILibrary/Business/AppContext.swift | 2 +- .../UILibrary/Business/ProfileEditor.swift | 4 +- Passepartout.xcodeproj/project.pbxproj | 84 ++++-- Passepartout/App/AppDelegate.swift | 3 +- .../App/Context/AppContext+Shared.swift | 259 ++++++++++++++++++ .../Context}/AppContext+Testing.swift | 15 +- .../Context}/DefaultAppProcessor.swift | 19 +- .../App/Context/Dependencies+Processors.swift | 43 +++ .../Context}/ProfileManager+Testing.swift | 4 +- Passepartout/Shared/AppContext+Shared.swift | 218 --------------- .../Shared/Dependencies+CoreData.swift | 44 +++ .../Shared/Dependencies+IAPManager.swift | 65 +++++ ...ift => Dependencies+PassepartoutKit.swift} | 89 ++---- .../Dependencies+PreferencesManager.swift | 50 ++++ Passepartout/Shared/Dependencies.swift | 69 +---- Passepartout/Shared/Shared+App.swift | 84 ------ .../Context}/DefaultTunnelProcessor.swift | 0 .../Context/TunnelContext+Shared.swift} | 39 +-- .../Tunnel/Context/TunnelContext.swift | 34 +++ .../Tunnel/PacketTunnelProvider.swift | 26 +- 21 files changed, 647 insertions(+), 510 deletions(-) create mode 100644 Passepartout/App/Context/AppContext+Shared.swift rename Passepartout/{Shared/Testing => App/Context}/AppContext+Testing.swift (85%) rename Passepartout/{Shared => App/Context}/DefaultAppProcessor.swift (86%) create mode 100644 Passepartout/App/Context/Dependencies+Processors.swift rename Passepartout/{Shared/Testing => App/Context}/ProfileManager+Testing.swift (94%) delete mode 100644 Passepartout/Shared/AppContext+Shared.swift create mode 100644 Passepartout/Shared/Dependencies+CoreData.swift create mode 100644 Passepartout/Shared/Dependencies+IAPManager.swift rename Passepartout/Shared/{Shared.swift => Dependencies+PassepartoutKit.swift} (62%) create mode 100644 Passepartout/Shared/Dependencies+PreferencesManager.swift delete mode 100644 Passepartout/Shared/Shared+App.swift rename Passepartout/{Shared => Tunnel/Context}/DefaultTunnelProcessor.swift (100%) rename Passepartout/{Shared/Shared+Tunnel.swift => Tunnel/Context/TunnelContext+Shared.swift} (57%) create mode 100644 Passepartout/Tunnel/Context/TunnelContext.swift diff --git a/Library/Sources/CommonLibrary/Strategy/Processors.swift b/Library/Sources/CommonLibrary/Strategy/Processors.swift index 1aba6bc7..95635d58 100644 --- a/Library/Sources/CommonLibrary/Strategy/Processors.swift +++ b/Library/Sources/CommonLibrary/Strategy/Processors.swift @@ -27,7 +27,7 @@ import Foundation import PassepartoutKit @MainActor -public protocol ProfileProcessor { +public protocol ProfileProcessor: Sendable { func isIncluded(_ profile: Profile) -> Bool func preview(from profile: Profile) -> ProfilePreview @@ -38,12 +38,12 @@ public protocol ProfileProcessor { } @MainActor -public protocol AppTunnelProcessor { +public protocol AppTunnelProcessor: Sendable { func title(for profile: Profile) -> String func willInstall(_ profile: Profile) throws -> Profile } -public protocol PacketTunnelProcessor { +public protocol PacketTunnelProcessor: Sendable { nonisolated func willStart(_ profile: Profile) throws -> Profile } diff --git a/Library/Sources/UILibrary/Business/AppContext.swift b/Library/Sources/UILibrary/Business/AppContext.swift index d0901307..bde070f5 100644 --- a/Library/Sources/UILibrary/Business/AppContext.swift +++ b/Library/Sources/UILibrary/Business/AppContext.swift @@ -31,7 +31,7 @@ import PassepartoutKit import UITesting @MainActor -public final class AppContext: ObservableObject { +public final class AppContext: ObservableObject, Sendable { public let iapManager: IAPManager public let migrationManager: MigrationManager diff --git a/Library/Sources/UILibrary/Business/ProfileEditor.swift b/Library/Sources/UILibrary/Business/ProfileEditor.swift index 12429566..a1529a4d 100644 --- a/Library/Sources/UILibrary/Business/ProfileEditor.swift +++ b/Library/Sources/UILibrary/Business/ProfileEditor.swift @@ -213,7 +213,7 @@ extension ProfileEditor { preferences = try preferencesManager.preferences(forProfile: profile) } catch { preferences = [:] - pp_log(.app, .error, "Unable to load preferences for profile \(profile.id): \(error)") + pp_log(.App.profiles, .error, "Unable to load preferences for profile \(profile.id): \(error)") } removedModules = [:] } @@ -233,7 +233,7 @@ extension ProfileEditor { } return newProfile } catch { - pp_log(.app, .fault, "Unable to save edited profile: \(error)") + pp_log(.App.profiles, .fault, "Unable to save edited profile: \(error)") throw error } } diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index d034a1ac..6a0b6799 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -13,8 +13,7 @@ 0E3E22982CE53510005135DF /* AppUITV in Frameworks */ = {isa = PBXBuildFile; platformFilters = (tvos, ); productRef = 0E3E22972CE53510005135DF /* AppUITV */; }; 0E3FF4BA2CE3AFBC00BFF640 /* Profiles.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 0E3FF4B72CE3AFBC00BFF640 /* Profiles.sqlite */; }; 0E3FF4BB2CE3AFBC00BFF640 /* MigrationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3FF4B92CE3AFBC00BFF640 /* MigrationManagerTests.swift */; }; - 0E483E812CE64D6B00584B32 /* Shared+Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E483E7F2CE64D6B00584B32 /* Shared+Tunnel.swift */; }; - 0E483E842CE6501100584B32 /* Shared+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E483E822CE6501100584B32 /* Shared+App.swift */; }; + 0E483E812CE64D6B00584B32 /* TunnelContext+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E483E7F2CE64D6B00584B32 /* TunnelContext+Shared.swift */; }; 0E60512C2CE5393C00F763D4 /* PassepartoutImplementations in Frameworks */ = {isa = PBXBuildFile; productRef = 0E60512B2CE5393C00F763D4 /* PassepartoutImplementations */; }; 0E6EEEE32CF8CABA0076E2B0 /* AppContext+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6EEEE12CF8CABA0076E2B0 /* AppContext+Testing.swift */; }; 0E6EEEE42CF8CABA0076E2B0 /* ProfileManager+Testing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6EEEE22CF8CABA0076E2B0 /* ProfileManager+Testing.swift */; }; @@ -29,6 +28,16 @@ 0E7F46122CF7F44C00B1C53A /* AppScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7F46102CF7F44C00B1C53A /* AppScreen.swift */; }; 0E81955A2CFDA75200CC8FFD /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8195592CFDA75200CC8FFD /* Dependencies.swift */; }; 0E81955B2CFDA7BF00CC8FFD /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8195592CFDA75200CC8FFD /* Dependencies.swift */; }; + 0E8DFD482D05FA7000531CDE /* TunnelContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD472D05FA7000531CDE /* TunnelContext.swift */; }; + 0E8DFD4E2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD4C2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift */; }; + 0E8DFD4F2D05FE5A00531CDE /* Dependencies+IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD4B2D05FE5A00531CDE /* Dependencies+IAPManager.swift */; }; + 0E8DFD502D05FE5A00531CDE /* Dependencies+Processors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD4D2D05FE5A00531CDE /* Dependencies+Processors.swift */; }; + 0E8DFD512D05FE5A00531CDE /* Dependencies+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD492D05FE5A00531CDE /* Dependencies+CoreData.swift */; }; + 0E8DFD532D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD4C2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift */; }; + 0E8DFD542D05FE5A00531CDE /* Dependencies+IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD4B2D05FE5A00531CDE /* Dependencies+IAPManager.swift */; }; + 0E8DFD562D05FE5A00531CDE /* Dependencies+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD492D05FE5A00531CDE /* Dependencies+CoreData.swift */; }; + 0E8DFD592D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */; }; + 0E8DFD5A2D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */; }; 0E916B782CF80FD60072921A /* ProfileEditorScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E916B772CF80FD60072921A /* ProfileEditorScreen.swift */; }; 0E916B7C2CF811EB0072921A /* XCUIElement+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E916B7B2CF811EB0072921A /* XCUIElement+Extensions.swift */; }; 0E94EE582B93554B00588243 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E3D672B9345FD002BBDB4 /* PacketTunnelProvider.swift */; }; @@ -42,8 +51,6 @@ 0EC332D22B8A1808000B9C2F /* PassepartoutTunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0EC332C82B8A1808000B9C2F /* PassepartoutTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 0EC418D22CF86B7400AC6F2F /* ProfileMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC418D12CF86B7400AC6F2F /* ProfileMenuScreen.swift */; }; 0EC797422B9378E000C093B7 /* AppContext+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797402B9378E000C093B7 /* AppContext+Shared.swift */; }; - 0EC797432B9378E000C093B7 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797412B9378E000C093B7 /* Shared.swift */; }; - 0EC797442B93790600C093B7 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC797412B9378E000C093B7 /* Shared.swift */; }; 0ED61CF82CD0418C008FE259 /* App+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED61CF72CD0418C008FE259 /* App+macOS.swift */; }; 0ED61CFA2CD04192008FE259 /* App+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED61CF92CD04192008FE259 /* App+iOS.swift */; }; 0EDE56EA2CABE40D0082D21C /* Intents.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0EDE56E62CABE40D0082D21C /* Intents.plist */; }; @@ -137,8 +144,7 @@ 0E3FF4AE2CE3AF6F00BFF640 /* PassepartoutTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PassepartoutTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0E3FF4B72CE3AFBC00BFF640 /* Profiles.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = Profiles.sqlite; sourceTree = ""; }; 0E3FF4B92CE3AFBC00BFF640 /* MigrationManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationManagerTests.swift; sourceTree = ""; }; - 0E483E7F2CE64D6B00584B32 /* Shared+Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shared+Tunnel.swift"; sourceTree = ""; }; - 0E483E822CE6501100584B32 /* Shared+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shared+App.swift"; sourceTree = ""; }; + 0E483E7F2CE64D6B00584B32 /* TunnelContext+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelContext+Shared.swift"; sourceTree = ""; }; 0E5DFDDC2CDB8F9100F2DE70 /* Passepartout.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Passepartout.storekit; sourceTree = ""; }; 0E6EEEE12CF8CABA0076E2B0 /* AppContext+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+Testing.swift"; sourceTree = ""; }; 0E6EEEE22CF8CABA0076E2B0 /* ProfileManager+Testing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileManager+Testing.swift"; sourceTree = ""; }; @@ -159,6 +165,12 @@ 0E7F46102CF7F44C00B1C53A /* AppScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppScreen.swift; sourceTree = ""; }; 0E8195592CFDA75200CC8FFD /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = ""; }; 0E8D852F2C328CA1005493DE /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; + 0E8DFD472D05FA7000531CDE /* TunnelContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelContext.swift; sourceTree = ""; }; + 0E8DFD492D05FE5A00531CDE /* Dependencies+CoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+CoreData.swift"; sourceTree = ""; }; + 0E8DFD4B2D05FE5A00531CDE /* Dependencies+IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+IAPManager.swift"; sourceTree = ""; }; + 0E8DFD4C2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+PassepartoutKit.swift"; sourceTree = ""; }; + 0E8DFD4D2D05FE5A00531CDE /* Dependencies+Processors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+Processors.swift"; sourceTree = ""; }; + 0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dependencies+PreferencesManager.swift"; sourceTree = ""; }; 0E916B772CF80FD60072921A /* ProfileEditorScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditorScreen.swift; sourceTree = ""; }; 0E916B7B2CF811EB0072921A /* XCUIElement+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElement+Extensions.swift"; sourceTree = ""; }; 0E94EE5C2B93570600588243 /* Tunnel.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Tunnel.plist; sourceTree = ""; }; @@ -172,7 +184,6 @@ 0EC332C92B8A1808000B9C2F /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; 0EC418D12CF86B7400AC6F2F /* ProfileMenuScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileMenuScreen.swift; sourceTree = ""; }; 0EC797402B9378E000C093B7 /* AppContext+Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppContext+Shared.swift"; sourceTree = ""; }; - 0EC797412B9378E000C093B7 /* Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; 0ED1EFDA2C33059600CBD9BD /* App.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = App.plist; sourceTree = ""; }; 0ED61CF72CD0418C008FE259 /* App+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+macOS.swift"; sourceTree = ""; }; 0ED61CF92CD04192008FE259 /* App+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+iOS.swift"; sourceTree = ""; }; @@ -289,15 +300,6 @@ path = Resources; sourceTree = ""; }; - 0E6EEEE62CF8CB090076E2B0 /* Testing */ = { - isa = PBXGroup; - children = ( - 0E6EEEE12CF8CABA0076E2B0 /* AppContext+Testing.swift */, - 0E6EEEE22CF8CABA0076E2B0 /* ProfileManager+Testing.swift */, - ); - path = Testing; - sourceTree = ""; - }; 0E757F112CD0CFFC006E13E1 /* LoginItem */ = { isa = PBXGroup; children = ( @@ -328,6 +330,7 @@ 0E7E3D5A2B9345FD002BBDB4 /* App */ = { isa = PBXGroup; children = ( + 0E8DFD462D05F77200531CDE /* Context */, 0ED61CF62CD04174008FE259 /* Platforms */, 0ED1EFDA2C33059600CBD9BD /* App.plist */, 0E7E3D5B2B9345FD002BBDB4 /* App.entitlements */, @@ -343,14 +346,11 @@ 0E7E3D612B9345FD002BBDB4 /* Shared */ = { isa = PBXGroup; children = ( - 0E6EEEE62CF8CB090076E2B0 /* Testing */, - 0EC797402B9378E000C093B7 /* AppContext+Shared.swift */, - 0EAEC8A62D05DB8D001AA50C /* DefaultAppProcessor.swift */, - 0EAEC8A72D05DB8D001AA50C /* DefaultTunnelProcessor.swift */, 0E8195592CFDA75200CC8FFD /* Dependencies.swift */, - 0EC797412B9378E000C093B7 /* Shared.swift */, - 0E483E822CE6501100584B32 /* Shared+App.swift */, - 0E483E7F2CE64D6B00584B32 /* Shared+Tunnel.swift */, + 0E8DFD492D05FE5A00531CDE /* Dependencies+CoreData.swift */, + 0E8DFD4B2D05FE5A00531CDE /* Dependencies+IAPManager.swift */, + 0E8DFD4C2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift */, + 0E8DFD582D05FF0400531CDE /* Dependencies+PreferencesManager.swift */, ); path = Shared; sourceTree = ""; @@ -358,6 +358,7 @@ 0E7E3D652B9345FD002BBDB4 /* Tunnel */ = { isa = PBXGroup; children = ( + 0E8DFD452D05F76900531CDE /* Context */, 0E94EE5C2B93570600588243 /* Tunnel.plist */, 0E7E3D662B9345FD002BBDB4 /* Tunnel.entitlements */, 0E7E3D672B9345FD002BBDB4 /* PacketTunnelProvider.swift */, @@ -376,6 +377,28 @@ path = UITests; sourceTree = ""; }; + 0E8DFD452D05F76900531CDE /* Context */ = { + isa = PBXGroup; + children = ( + 0EAEC8A72D05DB8D001AA50C /* DefaultTunnelProcessor.swift */, + 0E8DFD472D05FA7000531CDE /* TunnelContext.swift */, + 0E483E7F2CE64D6B00584B32 /* TunnelContext+Shared.swift */, + ); + path = Context; + sourceTree = ""; + }; + 0E8DFD462D05F77200531CDE /* Context */ = { + isa = PBXGroup; + children = ( + 0EC797402B9378E000C093B7 /* AppContext+Shared.swift */, + 0E6EEEE12CF8CABA0076E2B0 /* AppContext+Testing.swift */, + 0EAEC8A62D05DB8D001AA50C /* DefaultAppProcessor.swift */, + 0E8DFD4D2D05FE5A00531CDE /* Dependencies+Processors.swift */, + 0E6EEEE22CF8CABA0076E2B0 /* ProfileManager+Testing.swift */, + ); + path = Context; + sourceTree = ""; + }; 0E916B7A2CF811DE0072921A /* Extensions */ = { isa = PBXGroup; children = ( @@ -684,6 +707,10 @@ buildActionMask = 2147483647; files = ( 0ED61CF82CD0418C008FE259 /* App+macOS.swift in Sources */, + 0E8DFD4E2D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift in Sources */, + 0E8DFD4F2D05FE5A00531CDE /* Dependencies+IAPManager.swift in Sources */, + 0E8DFD502D05FE5A00531CDE /* Dependencies+Processors.swift in Sources */, + 0E8DFD512D05FE5A00531CDE /* Dependencies+CoreData.swift in Sources */, 0E7C3CCD2C9AF44600B72E69 /* AppDelegate.swift in Sources */, 0ED61CFA2CD04192008FE259 /* App+iOS.swift in Sources */, 0E7E3D6B2B9345FD002BBDB4 /* PassepartoutApp.swift in Sources */, @@ -693,8 +720,7 @@ 0E81955A2CFDA75200CC8FFD /* Dependencies.swift in Sources */, 0E6EEEE32CF8CABA0076E2B0 /* AppContext+Testing.swift in Sources */, 0E6EEEE42CF8CABA0076E2B0 /* ProfileManager+Testing.swift in Sources */, - 0E483E842CE6501100584B32 /* Shared+App.swift in Sources */, - 0EC797432B9378E000C093B7 /* Shared.swift in Sources */, + 0E8DFD592D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -738,8 +764,12 @@ 0E81955B2CFDA7BF00CC8FFD /* Dependencies.swift in Sources */, 0EAEC8AA2D05DB8D001AA50C /* DefaultTunnelProcessor.swift in Sources */, 0E94EE582B93554B00588243 /* PacketTunnelProvider.swift in Sources */, - 0E483E812CE64D6B00584B32 /* Shared+Tunnel.swift in Sources */, - 0EC797442B93790600C093B7 /* Shared.swift in Sources */, + 0E483E812CE64D6B00584B32 /* TunnelContext+Shared.swift in Sources */, + 0E8DFD482D05FA7000531CDE /* TunnelContext.swift in Sources */, + 0E8DFD532D05FE5A00531CDE /* Dependencies+PassepartoutKit.swift in Sources */, + 0E8DFD542D05FE5A00531CDE /* Dependencies+IAPManager.swift in Sources */, + 0E8DFD562D05FE5A00531CDE /* Dependencies+CoreData.swift in Sources */, + 0E8DFD5A2D05FF0400531CDE /* Dependencies+PreferencesManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Passepartout/App/AppDelegate.swift b/Passepartout/App/AppDelegate.swift index 37f04089..5c4322e8 100644 --- a/Passepartout/App/AppDelegate.swift +++ b/Passepartout/App/AppDelegate.swift @@ -33,8 +33,9 @@ import UITesting final class AppDelegate: NSObject { let context: AppContext = { if AppCommandLine.contains(.uiTesting) { + let dependencies: Dependencies = .shared pp_log(.app, .info, "UI tests: mock AppContext") - return .forUITesting(withRegistry: .shared) + return .forUITesting(withRegistry: dependencies.registry) } return .shared }() diff --git a/Passepartout/App/Context/AppContext+Shared.swift b/Passepartout/App/Context/AppContext+Shared.swift new file mode 100644 index 00000000..863b23ad --- /dev/null +++ b/Passepartout/App/Context/AppContext+Shared.swift @@ -0,0 +1,259 @@ +// +// AppContext+Shared.swift +// Passepartout +// +// Created by Davide De Rosa on 2/24/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 AppDataProfiles +import AppDataProviders +import CommonLibrary +import CommonUtils +import Foundation +import LegacyV2 +import PassepartoutKit +import UILibrary +import UITesting + +extension AppContext { + static let shared: AppContext = { + let dependencies: Dependencies = .shared + + let iapManager = IAPManager( + customUserLevel: dependencies.customUserLevel, + inAppHelper: dependencies.simulatedAppProductHelper(), + receiptReader: dependencies.simulatedAppReceiptReader(), + betaChecker: dependencies.betaChecker(), + productsAtBuild: dependencies.productsAtBuild() + ) + + let processor = dependencies.appProcessor(with: iapManager) + + let profileManager: ProfileManager = { + let remoteRepositoryBlock: (Bool) -> ProfileRepository = { + let remoteStore = CoreDataPersistentStore( + logger: dependencies.coreDataLogger(), + containerName: Constants.shared.containers.remoteProfiles, + model: AppData.cdProfilesModel, + cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil, + author: nil + ) + return AppData.cdProfileRepositoryV3( + registry: dependencies.registry, + coder: CodableProfileCoder(), + context: remoteStore.context, + observingResults: true, + onResultError: { + pp_log(.App.profiles, .error, "Unable to decode remote profile: \($0)") + return .ignore + } + ) + } + return ProfileManager( + repository: dependencies.mainProfileRepository(), + backupRepository: dependencies.backupProfileRepository(), + remoteRepositoryBlock: remoteRepositoryBlock, + mirrorsRemoteRepository: dependencies.mirrorsRemoteRepository, + processor: processor + ) + }() + + let tunnel = ExtendedTunnel( + tunnel: Tunnel(strategy: dependencies.tunnelStrategy()), + environment: dependencies.tunnelEnvironment(), + processor: processor, + interval: Constants.shared.tunnel.refreshInterval + ) + + let providerManager: ProviderManager = { + let store = CoreDataPersistentStore( + logger: dependencies.coreDataLogger(), + containerName: Constants.shared.containers.providers, + model: AppData.cdProvidersModel, + cloudKitIdentifier: nil, + author: nil + ) + let repository = AppData.cdProviderRepositoryV3(context: store.backgroundContext) + return ProviderManager(repository: repository) + }() + + let migrationManager: MigrationManager = { + let profileStrategy = ProfileV2MigrationStrategy( + coreDataLogger: dependencies.coreDataLogger(), + profilesContainer: .init( + Constants.shared.containers.legacyV2, + BundleConfiguration.mainString(for: .legacyV2CloudKitId) + ), + tvProfilesContainer: .init( + Constants.shared.containers.legacyV2TV, + BundleConfiguration.mainString(for: .legacyV2TVCloudKitId) + ) + ) + let migrationSimulation: MigrationManager.Simulation? + if AppCommandLine.contains(.fakeMigration) { + migrationSimulation = MigrationManager.Simulation( + fakeProfiles: true, + maxMigrationTime: 3.0, + randomFailures: true + ) + } else { + migrationSimulation = nil + } + return MigrationManager(profileStrategy: profileStrategy, simulation: migrationSimulation) + }() + + let preferencesManager = dependencies.preferencesManager(withCloudKit: true) + + return AppContext( + iapManager: iapManager, + migrationManager: migrationManager, + profileManager: profileManager, + providerManager: providerManager, + preferencesManager: preferencesManager, + registry: dependencies.registry, + tunnel: tunnel, + tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt + ) + }() +} + +// MARK: - Dependencies + +private extension Dependencies { + var customUserLevel: AppUserLevel? { + guard let userLevelString = BundleConfiguration.mainIntegerIfPresent(for: .userLevel), + let userLevel = AppUserLevel(rawValue: userLevelString) else { + return nil + } + return userLevel + } + + func simulatedAppProductHelper() -> any AppProductHelper { + if AppCommandLine.contains(.fakeIAP) { + return FakeAppProductHelper() + } + return appProductHelper() + } + + func simulatedAppReceiptReader() -> AppReceiptReader { + if AppCommandLine.contains(.fakeIAP) { + guard let mockHelper = simulatedAppProductHelper() as? FakeAppProductHelper else { + fatalError("When .isFakeIAP, simulatedInAppHelper is expected to be MockAppProductHelper") + } + return mockHelper.receiptReader + } + return FallbackReceiptReader( + main: StoreKitReceiptReader(), + beta: betaReceiptURL.map { + KvittoReceiptReader(url: $0) + } + ) + } + + var betaReceiptURL: URL? { + Bundle.main.appStoreProductionReceiptURL + } +} + +// MARK: Simulator + +#if targetEnvironment(simulator) + +private extension Dependencies { + func tunnelStrategy() -> TunnelObservableStrategy { + FakeTunnelStrategy(environment: .shared, dataCountInterval: 1000) + } + + func mainProfileRepository() -> ProfileRepository { + coreDataProfileRepository(observingResults: true) + } + + func backupProfileRepository() -> ProfileRepository? { + nil + } +} + +#else + +// MARK: Device + +private extension Dependencies { + func tunnelStrategy() -> TunnelObservableStrategy { + neStrategy() + } + + func mainProfileRepository() -> ProfileRepository { + neProfileRepository() + } + + func backupProfileRepository() -> ProfileRepository? { + coreDataProfileRepository(observingResults: false) + } +} + +#endif + +// MARK: Common + +private extension Dependencies { + var mirrorsRemoteRepository: Bool { +#if os(tvOS) + true +#else + false +#endif + } + + func neProfileRepository() -> ProfileRepository { + NEProfileRepository(repository: neStrategy()) { + profileTitle($0) + } + } + + func neStrategy() -> NETunnelStrategy { + NETunnelStrategy( + bundleIdentifier: BundleConfiguration.mainString(for: .tunnelId), + coder: neProtocolCoder(), + environment: tunnelEnvironment() + ) + } + + func coreDataProfileRepository(observingResults: Bool) -> ProfileRepository { + let store = CoreDataPersistentStore( + logger: coreDataLogger(), + containerName: Constants.shared.containers.localProfiles, + model: AppData.cdProfilesModel, + cloudKitIdentifier: nil, + author: nil + ) + return AppData.cdProfileRepositoryV3( + registry: registry, + coder: CodableProfileCoder(), + context: store.context, + observingResults: observingResults, + onResultError: { + pp_log(.App.profiles, .error, "Unable to decode local profile: \($0)") + return .ignore + } + ) + } +} diff --git a/Passepartout/Shared/Testing/AppContext+Testing.swift b/Passepartout/App/Context/AppContext+Testing.swift similarity index 85% rename from Passepartout/Shared/Testing/AppContext+Testing.swift rename to Passepartout/App/Context/AppContext+Testing.swift index 7ec9f00f..85bb621d 100644 --- a/Passepartout/Shared/Testing/AppContext+Testing.swift +++ b/Passepartout/App/Context/AppContext+Testing.swift @@ -31,21 +31,19 @@ import UILibrary extension AppContext { static func forUITesting(withRegistry registry: Registry) -> AppContext { + let dependencies: Dependencies = .shared let iapManager = IAPManager( customUserLevel: .subscriber, - inAppHelper: Dependencies.IAPManager.inAppHelper, + inAppHelper: dependencies.appProductHelper(), receiptReader: FakeAppReceiptReader(), betaChecker: TestFlightChecker(), productsAtBuild: { _ in [] } ) - let processor = DefaultAppProcessor(iapManager: iapManager) { - $0.localizedPreview - } - - let profileManager: ProfileManager = .forTesting( - withRegistry: .shared, + let processor = dependencies.appProcessor(with: iapManager) + let profileManager: ProfileManager = .forUITesting( + withRegistry: dependencies.registry, processor: processor ) let tunnelEnvironment = InMemoryEnvironment() @@ -59,13 +57,14 @@ extension AppContext { repository: InMemoryProviderRepository() ) let migrationManager = MigrationManager() + let preferencesManager = PreferencesManager() return AppContext( iapManager: iapManager, migrationManager: migrationManager, profileManager: profileManager, providerManager: providerManager, - preferencesManager: PreferencesManager(), + preferencesManager: preferencesManager, registry: registry, tunnel: tunnel, tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt diff --git a/Passepartout/Shared/DefaultAppProcessor.swift b/Passepartout/App/Context/DefaultAppProcessor.swift similarity index 86% rename from Passepartout/Shared/DefaultAppProcessor.swift rename to Passepartout/App/Context/DefaultAppProcessor.swift index 86c48b68..76108356 100644 --- a/Passepartout/Shared/DefaultAppProcessor.swift +++ b/Passepartout/App/Context/DefaultAppProcessor.swift @@ -30,21 +30,28 @@ import PassepartoutKit final class DefaultAppProcessor: Sendable { private let iapManager: IAPManager - private let preview: @Sendable (Profile) -> ProfilePreview + private let title: @Sendable (Profile) -> String - init(iapManager: IAPManager, preview: @escaping @Sendable (Profile) -> ProfilePreview) { + init( + iapManager: IAPManager, + title: @escaping @Sendable (Profile) -> String + ) { self.iapManager = iapManager - self.preview = preview + self.title = title } } extension DefaultAppProcessor: ProfileProcessor { func isIncluded(_ profile: Profile) -> Bool { - Dependencies.ProfileManager.isIncluded(iapManager, profile) +#if os(tvOS) + profile.attributes.isAvailableForTV == true +#else + true +#endif } func preview(from profile: Profile) -> ProfilePreview { - preview(profile) + profile.localizedPreview } func requiredFeatures(_ profile: Profile) -> Set? { @@ -65,7 +72,7 @@ extension DefaultAppProcessor: ProfileProcessor { extension DefaultAppProcessor: AppTunnelProcessor { func title(for profile: Profile) -> String { - Dependencies.ProfileManager.sharedTitle(profile) + title(profile) } func willInstall(_ profile: Profile) throws -> Profile { diff --git a/Passepartout/App/Context/Dependencies+Processors.swift b/Passepartout/App/Context/Dependencies+Processors.swift new file mode 100644 index 00000000..bf8172ea --- /dev/null +++ b/Passepartout/App/Context/Dependencies+Processors.swift @@ -0,0 +1,43 @@ +// +// Dependencies+Processors.swift +// Passepartout +// +// Created by Davide De Rosa on 12/2/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 +import PassepartoutKit +import UILibrary + +extension Dependencies { + func appProcessor(with iapManager: IAPManager) -> DefaultAppProcessor { + DefaultAppProcessor( + iapManager: iapManager, + title: profileTitle + ) + } + + @Sendable + nonisolated func profileTitle(_ profile: Profile) -> String { + String(format: Constants.shared.tunnel.profileTitleFormat, profile.name) + } +} diff --git a/Passepartout/Shared/Testing/ProfileManager+Testing.swift b/Passepartout/App/Context/ProfileManager+Testing.swift similarity index 94% rename from Passepartout/Shared/Testing/ProfileManager+Testing.swift rename to Passepartout/App/Context/ProfileManager+Testing.swift index d462b8a3..82ec025b 100644 --- a/Passepartout/Shared/Testing/ProfileManager+Testing.swift +++ b/Passepartout/App/Context/ProfileManager+Testing.swift @@ -28,7 +28,7 @@ import Foundation import PassepartoutKit extension ProfileManager { - public static func forTesting(withRegistry registry: Registry, processor: ProfileProcessor) -> ProfileManager { + public static func forUITesting(withRegistry registry: Registry, processor: ProfileProcessor) -> ProfileManager { let repository = InMemoryProfileRepository() let remoteRepository = InMemoryProfileRepository() let manager = ProfileManager(repository: repository, remoteRepositoryBlock: { _ in @@ -67,7 +67,7 @@ extension ProfileManager { try await manager.save(profile, isLocal: true, remotelyShared: parameters.isShared) } } catch { - pp_log(.App.profiles, .error, "Unable to build mock ProfileManager: \(error)") + pp_log(.App.profiles, .error, "Unable to build ProfileManager for UI testing: \(error)") } } diff --git a/Passepartout/Shared/AppContext+Shared.swift b/Passepartout/Shared/AppContext+Shared.swift deleted file mode 100644 index 548adbb5..00000000 --- a/Passepartout/Shared/AppContext+Shared.swift +++ /dev/null @@ -1,218 +0,0 @@ -// -// AppContext+Shared.swift -// Passepartout -// -// Created by Davide De Rosa on 2/24/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 AppDataProfiles -import AppDataProviders -import CommonLibrary -import CommonUtils -import Foundation -import LegacyV2 -import PassepartoutKit -import UILibrary -import UITesting - -// shared registry and environment are picked from Shared.swift - -extension AppContext { - static let shared: AppContext = { - let iapManager: IAPManager = .sharedForApp - let processor = DefaultAppProcessor(iapManager: iapManager) { - $0.localizedPreview - } - - // MARK: ProfileManager - - let remoteRepositoryBlock: (Bool) -> ProfileRepository = { - let remoteStore = CoreDataPersistentStore( - logger: .default, - containerName: Constants.shared.containers.remoteProfiles, - model: AppData.cdProfilesModel, - cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil, - author: nil - ) - return AppData.cdProfileRepositoryV3( - registry: .shared, - coder: CodableProfileCoder(), - context: remoteStore.context, - observingResults: true - ) { error in - pp_log(.app, .error, "Unable to decode remote result: \(error)") - return .ignore - } - } - let profileManager = ProfileManager( - repository: Dependencies.ProfileManager.mainProfileRepository, - backupRepository: Dependencies.ProfileManager.backupProfileRepository, - remoteRepositoryBlock: remoteRepositoryBlock, - mirrorsRemoteRepository: Dependencies.ProfileManager.mirrorsRemoteRepository, - processor: processor - ) - - // MARK: ExtendedTunnel - - let tunnel = ExtendedTunnel( - tunnel: Tunnel(strategy: Dependencies.ExtendedTunnel.strategy), - environment: .shared, - processor: processor, - interval: Constants.shared.tunnel.refreshInterval - ) - - // MARK: ProviderManager - - let providerManager: ProviderManager = { - let store = CoreDataPersistentStore( - logger: .default, - containerName: Constants.shared.containers.providers, - model: AppData.cdProvidersModel, - cloudKitIdentifier: nil, - author: nil - ) - let repository = AppData.cdProviderRepositoryV3(context: store.backgroundContext) - return ProviderManager(repository: repository) - }() - - // MARK: MigrationManager - - let profileStrategy = ProfileV2MigrationStrategy( - coreDataLogger: .default, - profilesContainer: .init( - Constants.shared.containers.legacyV2, - BundleConfiguration.mainString(for: .legacyV2CloudKitId) - ), - tvProfilesContainer: .init( - Constants.shared.containers.legacyV2TV, - BundleConfiguration.mainString(for: .legacyV2TVCloudKitId) - ) - ) - let migrationSimulation: MigrationManager.Simulation? - if AppCommandLine.contains(.fakeMigration) { - migrationSimulation = MigrationManager.Simulation( - fakeProfiles: true, - maxMigrationTime: 3.0, - randomFailures: true - ) - } else { - migrationSimulation = nil - } - let migrationManager = MigrationManager(profileStrategy: profileStrategy, simulation: migrationSimulation) - - return AppContext( - iapManager: iapManager, - migrationManager: migrationManager, - profileManager: profileManager, - providerManager: providerManager, - preferencesManager: .sharedForApp, - registry: .shared, - tunnel: tunnel, - tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt - ) - }() -} - -// MARK: - Dependencies - -// MARK: Simulator - -#if targetEnvironment(simulator) - -private extension Dependencies.ExtendedTunnel { - static var strategy: TunnelObservableStrategy { - FakeTunnelStrategy(environment: .shared, dataCountInterval: 1000) - } -} - -@MainActor -private extension Dependencies.ProfileManager { - static var mainProfileRepository: ProfileRepository { - coreDataProfileRepository(observingResults: true) - } - - static var backupProfileRepository: ProfileRepository? { - nil - } -} - -#else - -// MARK: Device - -@MainActor -private extension Dependencies.ExtendedTunnel { - static var strategy: TunnelObservableStrategy { - Dependencies.ProfileManager.neStrategy - } -} - -@MainActor -private extension Dependencies.ProfileManager { - static var mainProfileRepository: ProfileRepository { - neProfileRepository - } - - static var backupProfileRepository: ProfileRepository? { - coreDataProfileRepository(observingResults: false) - } -} - -#endif - -// MARK: Common - -@MainActor -private extension Dependencies.ProfileManager { - static let neProfileRepository: ProfileRepository = { - NEProfileRepository(repository: neStrategy) { - sharedTitle($0) - } - }() - - static let neStrategy: NETunnelStrategy = { - NETunnelStrategy( - bundleIdentifier: BundleConfiguration.mainString(for: .tunnelId), - coder: Registry.sharedProtocolCoder, - environment: .shared - ) - }() - - static func coreDataProfileRepository(observingResults: Bool) -> ProfileRepository { - let store = CoreDataPersistentStore( - logger: .default, - containerName: Constants.shared.containers.localProfiles, - model: AppData.cdProfilesModel, - cloudKitIdentifier: nil, - author: nil - ) - return AppData.cdProfileRepositoryV3( - registry: .shared, - coder: CodableProfileCoder(), - context: store.context, - observingResults: observingResults - ) { error in - pp_log(.app, .error, "Unable to decode local result: \(error)") - return .ignore - } - } -} diff --git a/Passepartout/Shared/Dependencies+CoreData.swift b/Passepartout/Shared/Dependencies+CoreData.swift new file mode 100644 index 00000000..fdff75ea --- /dev/null +++ b/Passepartout/Shared/Dependencies+CoreData.swift @@ -0,0 +1,44 @@ +// +// Dependencies+CoreData.swift +// Passepartout +// +// Created by Davide De Rosa on 12/2/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 + +extension Dependencies { + func coreDataLogger() -> 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/Dependencies+IAPManager.swift b/Passepartout/Shared/Dependencies+IAPManager.swift new file mode 100644 index 00000000..64485be3 --- /dev/null +++ b/Passepartout/Shared/Dependencies+IAPManager.swift @@ -0,0 +1,65 @@ +// +// Dependencies+IAPManager.swift +// Passepartout +// +// Created by Davide De Rosa on 12/2/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 CommonUtils +import Foundation +import PassepartoutKit + +extension Dependencies { + func appProductHelper() -> any AppProductHelper { + StoreKitHelper( + products: AppProduct.all, + inAppIdentifier: { + let prefix = BundleConfiguration.mainString(for: .iapBundlePrefix) + return "\(prefix).\($0.rawValue)" + } + ) + } + + func betaChecker() -> BetaChecker { + TestFlightChecker() + } + + func productsAtBuild() -> BuildProducts { + { +#if os(iOS) + if $0 <= 2016 { + return [.Full.iOS] + } else if $0 <= 3000 { + return [.Features.networkSettings] + } + return [] +#elseif os(macOS) + if $0 <= 3000 { + return [.Features.networkSettings] + } + return [] +#else + return [] +#endif + } + } +} diff --git a/Passepartout/Shared/Shared.swift b/Passepartout/Shared/Dependencies+PassepartoutKit.swift similarity index 62% rename from Passepartout/Shared/Shared.swift rename to Passepartout/Shared/Dependencies+PassepartoutKit.swift index 081bf7ed..77afc192 100644 --- a/Passepartout/Shared/Shared.swift +++ b/Passepartout/Shared/Dependencies+PassepartoutKit.swift @@ -1,8 +1,8 @@ // -// Shared.swift +// Dependencies+PassepartoutKit.swift // Passepartout // -// Created by Davide De Rosa on 2/25/24. +// Created by Davide De Rosa on 12/2/24. // Copyright (c) 2024 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn @@ -23,17 +23,35 @@ // along with Passepartout. If not, see . // -import AppData -import AppDataPreferences -import CommonLibrary -import CommonUtils import CPassepartoutOpenVPNOpenSSL import Foundation import PassepartoutKit import PassepartoutWireGuardGo -extension Registry { - static let shared = Registry( +extension Dependencies { + var registry: Registry { + Self.sharedRegistry + } + + func neProtocolCoder() -> NEProtocolCoder { + KeychainNEProtocolCoder( + tunnelBundleIdentifier: BundleConfiguration.mainString(for: .tunnelId), + registry: registry, + coder: CodableProfileCoder(), + keychain: AppleKeychain(group: BundleConfiguration.mainString(for: .keychainGroupId)) + ) + } + + func tunnelEnvironment() -> TunnelEnvironment { + AppGroupEnvironment( + appGroup: BundleConfiguration.mainString(for: .groupId), + prefix: "PassepartoutKit." + ) + } +} + +private extension Dependencies { + static let sharedRegistry = Registry( withKnownHandlers: true, allImplementations: [ OpenVPNModule.Implementation( @@ -69,59 +87,4 @@ extension Registry { ) ] ) - - static var sharedProtocolCoder: KeychainNEProtocolCoder { - KeychainNEProtocolCoder( - tunnelBundleIdentifier: BundleConfiguration.mainString(for: .tunnelId), - registry: .shared, - coder: CodableProfileCoder(), - keychain: AppleKeychain(group: BundleConfiguration.mainString(for: .keychainGroupId)) - ) - } -} - -extension TunnelEnvironment where Self == AppGroupEnvironment { - static var shared: Self { - AppGroupEnvironment( - appGroup: BundleConfiguration.mainString(for: .groupId), - prefix: "PassepartoutKit." - ) - } -} - -extension PreferencesManager { - static func sharedImplementation(withCloudKit: Bool) -> PreferencesManager { - let preferencesStore = CoreDataPersistentStore( - logger: .default, - containerName: Constants.shared.containers.preferences, - baseURL: BundleConfiguration.urlForGroupDocuments, - model: AppData.cdPreferencesModel, - cloudKitIdentifier: withCloudKit ? BundleConfiguration.mainString(for: .cloudKitPreferencesId) : nil, - author: nil - ) - return PreferencesManager( - modulesRepository: AppData.cdModulePreferencesRepositoryV3(context: preferencesStore.context), - providersFactory: { - try AppData.cdProviderPreferencesRepositoryV3(context: preferencesStore.context, providerId: $0) - } - ) - } -} - -// 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/Dependencies+PreferencesManager.swift b/Passepartout/Shared/Dependencies+PreferencesManager.swift new file mode 100644 index 00000000..d96592fe --- /dev/null +++ b/Passepartout/Shared/Dependencies+PreferencesManager.swift @@ -0,0 +1,50 @@ +// +// Dependencies+PreferencesManager.swift +// Passepartout +// +// Created by Davide De Rosa on 12/2/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 AppDataPreferences +import CommonLibrary +import CommonUtils +import Foundation +import PassepartoutKit + +extension Dependencies { + func preferencesManager(withCloudKit: Bool) -> PreferencesManager { + let preferencesStore = CoreDataPersistentStore( + logger: coreDataLogger(), + containerName: Constants.shared.containers.preferences, + baseURL: BundleConfiguration.urlForGroupDocuments, + model: AppData.cdPreferencesModel, + cloudKitIdentifier: withCloudKit ? BundleConfiguration.mainString(for: .cloudKitPreferencesId) : nil, + author: nil + ) + return PreferencesManager( + modulesRepository: AppData.cdModulePreferencesRepositoryV3(context: preferencesStore.context), + providersFactory: { + try AppData.cdProviderPreferencesRepositoryV3(context: preferencesStore.context, providerId: $0) + } + ) + } +} diff --git a/Passepartout/Shared/Dependencies.swift b/Passepartout/Shared/Dependencies.swift index 138094f8..107b01d3 100644 --- a/Passepartout/Shared/Dependencies.swift +++ b/Passepartout/Shared/Dependencies.swift @@ -23,72 +23,9 @@ // along with Passepartout. If not, see . // -import CommonLibrary -import CommonUtils import Foundation -import PassepartoutKit -enum Dependencies { - enum ExtendedTunnel { - } - - enum IAPManager { - } - - enum ProfileManager { - } -} - -extension Dependencies.IAPManager { - - @MainActor - static let inAppHelper = StoreKitHelper( - products: AppProduct.all, - inAppIdentifier: { - let prefix = BundleConfiguration.mainString(for: .iapBundlePrefix) - return "\(prefix).\($0.rawValue)" - } - ) - - static var betaChecker: BetaChecker { - TestFlightChecker() - } - - static let productsAtBuild: BuildProducts = { -#if os(iOS) - if $0 <= 2016 { - return [.Full.iOS] - } else if $0 <= 3000 { - return [.Features.networkSettings] - } - return [] -#elseif os(macOS) - if $0 <= 3000 { - return [.Features.networkSettings] - } - return [] -#else - return [] -#endif - } -} - -extension Dependencies.ProfileManager { - static let sharedTitle: @Sendable (Profile) -> String = { - String(format: Constants.shared.tunnel.profileTitleFormat, $0.name) - } - -#if os(tvOS) - static let mirrorsRemoteRepository = true - - static let isIncluded: @MainActor @Sendable (CommonLibrary.IAPManager, Profile) -> Bool = { - $1.attributes.isAvailableForTV == true - } -#else - static let mirrorsRemoteRepository = false - - static let isIncluded: @MainActor @Sendable (CommonLibrary.IAPManager, Profile) -> Bool = { _, _ in - true - } -#endif +@MainActor +struct Dependencies { + static let shared = Dependencies() } diff --git a/Passepartout/Shared/Shared+App.swift b/Passepartout/Shared/Shared+App.swift deleted file mode 100644 index 5720906c..00000000 --- a/Passepartout/Shared/Shared+App.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// Shared+App.swift -// Passepartout -// -// Created by Davide De Rosa on 11/14/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 CommonUtils -import Foundation -import PassepartoutKit -import UITesting - -extension IAPManager { - static let sharedForApp = IAPManager( - customUserLevel: Dependencies.IAPManager.customUserLevel, - inAppHelper: Dependencies.IAPManager.simulatedInAppHelper, - receiptReader: Dependencies.IAPManager.simulatedAppReceiptReader, - betaChecker: Dependencies.IAPManager.betaChecker, - productsAtBuild: Dependencies.IAPManager.productsAtBuild - ) -} - -extension PreferencesManager { - static let sharedForApp = PreferencesManager.sharedImplementation(withCloudKit: true) -} - -// MARK: - Dependencies - -private extension Dependencies.IAPManager { - static var customUserLevel: AppUserLevel? { - guard let userLevelString = BundleConfiguration.mainIntegerIfPresent(for: .userLevel), - let userLevel = AppUserLevel(rawValue: userLevelString) else { - return nil - } - return userLevel - } - - @MainActor - static let simulatedInAppHelper: any AppProductHelper = { - if AppCommandLine.contains(.fakeIAP) { - return FakeAppProductHelper() - } - return inAppHelper - }() - - @MainActor - static var simulatedAppReceiptReader: AppReceiptReader { - if AppCommandLine.contains(.fakeIAP) { - guard let mockHelper = simulatedInAppHelper as? FakeAppProductHelper else { - fatalError("When .isFakeIAP, simulatedInAppHelper is expected to be MockAppProductHelper") - } - return mockHelper.receiptReader - } - return FallbackReceiptReader( - main: StoreKitReceiptReader(), - beta: betaReceiptURL.map { - KvittoReceiptReader(url: $0) - } - ) - } - - static var betaReceiptURL: URL? { - Bundle.main.appStoreProductionReceiptURL - } -} diff --git a/Passepartout/Shared/DefaultTunnelProcessor.swift b/Passepartout/Tunnel/Context/DefaultTunnelProcessor.swift similarity index 100% rename from Passepartout/Shared/DefaultTunnelProcessor.swift rename to Passepartout/Tunnel/Context/DefaultTunnelProcessor.swift diff --git a/Passepartout/Shared/Shared+Tunnel.swift b/Passepartout/Tunnel/Context/TunnelContext+Shared.swift similarity index 57% rename from Passepartout/Shared/Shared+Tunnel.swift rename to Passepartout/Tunnel/Context/TunnelContext+Shared.swift index 563e6847..c67a3701 100644 --- a/Passepartout/Shared/Shared+Tunnel.swift +++ b/Passepartout/Tunnel/Context/TunnelContext+Shared.swift @@ -1,5 +1,5 @@ // -// Shared+Tunnel.swift +// TunnelContext+Shared.swift // Passepartout // // Created by Davide De Rosa on 11/14/24. @@ -28,32 +28,37 @@ import CommonUtils import Foundation import PassepartoutKit -extension IAPManager { - static let sharedForTunnel = IAPManager( - inAppHelper: Dependencies.IAPManager.inAppHelper, - receiptReader: Dependencies.IAPManager.tunnelReceiptReader, - betaChecker: Dependencies.IAPManager.betaChecker, - productsAtBuild: Dependencies.IAPManager.productsAtBuild - ) -} - -extension PreferencesManager { - static let sharedForTunnel = PreferencesManager.sharedImplementation(withCloudKit: false) +extension TunnelContext { + static let shared: TunnelContext = { + let dependencies: Dependencies = .shared + let iapManager = IAPManager( + inAppHelper: dependencies.appProductHelper(), + receiptReader: dependencies.tunnelReceiptReader(), + betaChecker: dependencies.betaChecker(), + productsAtBuild: dependencies.productsAtBuild() + ) + let processor: PacketTunnelProcessor = { + let preferencesManager = dependencies.preferencesManager(withCloudKit: false) + return DefaultTunnelProcessor(preferencesManager: preferencesManager) + }() + return TunnelContext( + iapManager: iapManager, + processor: processor + ) + }() } // MARK: - Dependencies -private extension Dependencies.IAPManager { - - @MainActor - static var tunnelReceiptReader: AppReceiptReader { +private extension Dependencies { + func tunnelReceiptReader() -> AppReceiptReader { FallbackReceiptReader( main: StoreKitReceiptReader(), beta: KvittoReceiptReader(url: betaReceiptURL) ) } - static var betaReceiptURL: URL { + var betaReceiptURL: URL { BundleConfiguration.urlForBetaReceipt // copied by AppContext.onLaunch } } diff --git a/Passepartout/Tunnel/Context/TunnelContext.swift b/Passepartout/Tunnel/Context/TunnelContext.swift new file mode 100644 index 00000000..d5e911a6 --- /dev/null +++ b/Passepartout/Tunnel/Context/TunnelContext.swift @@ -0,0 +1,34 @@ +// +// TunnelContext.swift +// Passepartout +// +// Created by Davide De Rosa on 12/8/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 +struct TunnelContext { + let iapManager: IAPManager + + let processor: PacketTunnelProcessor +} diff --git a/Passepartout/Tunnel/PacketTunnelProvider.swift b/Passepartout/Tunnel/PacketTunnelProvider.swift index af2a9e89..388b0c59 100644 --- a/Passepartout/Tunnel/PacketTunnelProvider.swift +++ b/Passepartout/Tunnel/PacketTunnelProvider.swift @@ -28,6 +28,13 @@ import CommonLibrary import PassepartoutKit final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { + + @MainActor + private let context: TunnelContext = .shared + + @MainActor + private let dependencies: Dependencies = .shared + private var fwd: NEPTPForwarder? override func startTunnel(options: [String: NSObject]? = nil) async throws { @@ -36,19 +43,18 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { parameters: Constants.shared.log, logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key) ) - let processor = DefaultTunnelProcessor(preferencesManager: .sharedForTunnel) do { fwd = try await NEPTPForwarder( provider: self, - decoder: Registry.sharedProtocolCoder, - registry: .shared, - environment: .shared, - profileBlock: processor.willStart + decoder: dependencies.neProtocolCoder(), + registry: dependencies.registry, + environment: dependencies.tunnelEnvironment(), + profileBlock: context.processor.willStart ) guard let fwd else { fatalError("NEPTPForwarder nil without throwing error?") } - try await checkEligibility(of: fwd.profile, environment: .shared) + try await checkEligibility(of: fwd.profile, environment: dependencies.tunnelEnvironment()) try await fwd.startTunnel(options: options) } catch { pp_log(.app, .fault, "Unable to start tunnel: \(error)") @@ -85,14 +91,10 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { @MainActor private extension PacketTunnelProvider { - var iapManager: IAPManager { - .sharedForTunnel - } - func checkEligibility(of profile: Profile, environment: TunnelEnvironment) async throws { - await iapManager.reloadReceipt() + await context.iapManager.reloadReceipt() do { - try iapManager.verify(profile) + try context.iapManager.verify(profile) } catch { let error = PassepartoutError(.App.ineligibleProfile) environment.setEnvironmentValue(error.code, forKey: TunnelEnvironmentKeys.lastErrorCode)