From a0da930d982faf5dbffb4c0e1937647c3db7c199 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Wed, 20 Dec 2023 20:43:39 +0100 Subject: [PATCH] Refactor and test ProductManager (#437) Carefully drop the StoreKit and Kvitto dependencies for ProductManager to be testable. Rebuild test target completely to start writing meaningful tests in general. --- .github/workflows/test.yml | 3 +- Passepartout.xcodeproj/project.pbxproj | 267 +++------------- .../xcschemes/Passepartout.xcscheme | 51 +-- .../xcschemes/PassepartoutTests.xcscheme | 118 ------- Passepartout.xctestplan | 73 ----- .../App/Constants/Constants+App.swift | 9 +- Passepartout/App/Context/AppContext.swift | 2 + .../App/Domain/StoreKitReceiptReader.swift | 79 +++++ .../DefaultUpgradeManagerStrategy.swift | 8 +- .../App/Managers/IntentsManager.swift | 2 +- .../App/Managers/UpgradeManager.swift | 10 +- .../App/Managers/UpgradeManagerStrategy.swift | 2 +- Passepartout/App/Views/DonateView.swift | 9 +- .../App/Views/PaywallView+Purchase.swift | 19 +- Passepartout/App/Views/PaywallView.swift | 1 + .../.swiftpm/PassepartoutLibrary.xctestplan | 45 --- .../xcschemes/PassepartoutCoreTests.xcscheme | 53 ---- .../PassepartoutLibrary-Package.xcscheme | 216 ++----------- .../xcschemes/PassepartoutLibrary.xcscheme | 11 +- .../PassepartoutProvidersTests.xcscheme | 53 ---- .../xcschemes/PassepartoutVPNTests.xcscheme | 53 ---- PassepartoutLibrary/Package.swift | 30 +- .../PassepartoutCore/Reusable/InApp.swift | 222 +++---------- .../Reusable/StoreKitInApp.swift | 297 ++++++++++++++++++ .../Domain/AppType.swift} | 27 +- .../Domain/BuildProducts.swift | 8 +- .../Domain/LocalProduct.swift | 60 ++-- .../Domain/ReceiptReader.swift | 16 +- .../Managers/ProductManager.swift | 194 +++++------- .../Sources/PassepartoutLibrary/Exports.swift | 1 + .../Extensions/OnDemand+Rules.swift | 8 + .../Mock/MockInApp.swift | 71 +++++ .../Mock/MockReceiptReader.swift} | 30 +- .../ProductManagerTests.swift | 140 +++++++++ .../ProviderManagerTests.swift | 173 ++++++++++ .../ProvidersTests.swift | 173 ---------- .../ServicesTests.swift | 96 ------ .../WebServicesTests.swift | 96 ++++++ fastlane/Scanfile | 2 - 39 files changed, 1213 insertions(+), 1515 deletions(-) delete mode 100644 Passepartout.xcodeproj/xcshareddata/xcschemes/PassepartoutTests.xcscheme delete mode 100644 Passepartout.xctestplan create mode 100644 Passepartout/App/Domain/StoreKitReceiptReader.swift delete mode 100644 PassepartoutLibrary/.swiftpm/PassepartoutLibrary.xctestplan delete mode 100644 PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutCoreTests.xcscheme delete mode 100644 PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutProvidersTests.xcscheme delete mode 100644 PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutVPNTests.xcscheme create mode 100644 PassepartoutLibrary/Sources/PassepartoutCore/Reusable/StoreKitInApp.swift rename PassepartoutLibrary/{Tests/PassepartoutVPNTests/ProfilesTests.swift => Sources/PassepartoutFrontend/Domain/AppType.swift} (69%) rename {Passepartout/App => PassepartoutLibrary/Sources/PassepartoutFrontend}/Domain/BuildProducts.swift (81%) rename {Passepartout/App => PassepartoutLibrary/Sources/PassepartoutFrontend}/Domain/LocalProduct.swift (63%) rename PassepartoutTests/PassepartoutTestsApp.swift => PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/ReceiptReader.swift (79%) rename {Passepartout/App => PassepartoutLibrary/Sources/PassepartoutFrontend}/Managers/ProductManager.swift (62%) create mode 100644 PassepartoutLibrary/Tests/PassepartoutFrontendTests/Mock/MockInApp.swift rename PassepartoutLibrary/Tests/{PassepartoutVPNTests/VPNTests.swift => PassepartoutFrontendTests/Mock/MockReceiptReader.swift} (52%) create mode 100644 PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift create mode 100644 PassepartoutLibrary/Tests/PassepartoutProvidersTests/ProviderManagerTests.swift delete mode 100644 PassepartoutLibrary/Tests/PassepartoutProvidersTests/ProvidersTests.swift delete mode 100644 PassepartoutLibrary/Tests/PassepartoutProvidersTests/ServicesTests.swift create mode 100644 PassepartoutLibrary/Tests/PassepartoutServicesTests/WebServicesTests.swift delete mode 100644 fastlane/Scanfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 136dcdf1..332d2f72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,4 +35,5 @@ jobs: go-version: "^1.17" - name: Run tests run: | - bundle exec fastlane --env beta,ios scan + cd PassepartoutLibrary + swift test diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index 53db7a98..6384610c 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 0E021D9C284E68580077EF5D /* CoreContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E021D9A284E68580077EF5D /* CoreContext.swift */; }; 0E021D9D284E68580077EF5D /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E021D9B284E68580077EF5D /* AppContext.swift */; }; - 0E0392772818732D00827C10 /* BuildProducts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0392762818732D00827C10 /* BuildProducts.swift */; }; 0E039279281890B100827C10 /* AddHostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E039278281890B100827C10 /* AddHostView.swift */; }; 0E04F0062883462E00BFCE1C /* LightUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E04F0052883462E00BFCE1C /* LightUtils.swift */; }; 0E04F0072883462E00BFCE1C /* LightUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E04F0052883462E00BFCE1C /* LightUtils.swift */; }; @@ -29,12 +28,10 @@ 0E0F4C5C29C76B790022E884 /* SceneDelegate+Shortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C5B29C76B790022E884 /* SceneDelegate+Shortcuts.swift */; }; 0E0F4C6429C84B5A0022E884 /* LockableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C6329C84B5A0022E884 /* LockableView.swift */; }; 0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0F4C6529C84CF60022E884 /* LogoView.swift */; }; - 0E1AD5CC2A2682DA002AE6E6 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1AD5CB2A2682DA002AE6E6 /* AppError.swift */; }; 0E1AD5CE2A268645002AE6E6 /* Errors+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1AD5CD2A268645002AE6E6 /* Errors+L10n.swift */; }; 0E1B5F5C29C506AD00FE7D18 /* DiagnosticsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1B5F5B29C506AC00FE7D18 /* DiagnosticsSection.swift */; }; 0E1F5628287F0ECB00F8ADD7 /* ProviderProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1F5627287F0ECB00F8ADD7 /* ProviderProfileItem.swift */; }; 0E1F562B287F0EF100F8ADD7 /* ProviderProfileItem+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1F5629287F0EEE00F8ADD7 /* ProviderProfileItem+ViewModel.swift */; }; - 0E293851285A70AC002A6E0E /* AppPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E293850285A70AC002A6E0E /* AppPreference.swift */; }; 0E293857285A73BC002A6E0E /* AppContext+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E293856285A73BC002A6E0E /* AppContext+Shared.swift */; }; 0E2A8D4927ADF87F00207D04 /* PassepartoutApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */; }; 0E2A8D4F27B04BBA00207D04 /* OrganizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */; }; @@ -44,15 +41,18 @@ 0E2DE71C27DCCFE80067B9E1 /* TunnelKit+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2DE71B27DCCFE80067B9E1 /* TunnelKit+Extensions.swift */; }; 0E2DE71F27DCD0290067B9E1 /* TunnelKit+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2DE71E27DCD0290067B9E1 /* TunnelKit+L10n.swift */; }; 0E2DE72527DCDF550067B9E1 /* WireGuard+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2DE72427DCDF550067B9E1 /* WireGuard+L10n.swift */; }; + 0E2E0B6F2B335A8E00E3204A /* AppPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B6D2B335A8E00E3204A /* AppPreference.swift */; }; + 0E2E0B702B335A8E00E3204A /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B6E2B335A8E00E3204A /* AppError.swift */; }; + 0E2E0B752B335AAB00E3204A /* IntentsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B712B335AAB00E3204A /* IntentsManager.swift */; }; + 0E2E0B762B335AAB00E3204A /* UpgradeManagerStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B722B335AAB00E3204A /* UpgradeManagerStrategy.swift */; }; + 0E2E0B772B335AAB00E3204A /* UpgradeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B732B335AAB00E3204A /* UpgradeManager.swift */; }; + 0E2E0B782B335AAB00E3204A /* PersistenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2E0B742B335AAB00E3204A /* PersistenceManager.swift */; }; 0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2B527CAA8CC00C73B67 /* Core+L10n.swift */; }; 0E34A2B927CAA96A00C73B67 /* OpenVPN+L10n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2AF27CAA84500C73B67 /* OpenVPN+L10n.swift */; }; 0E34A2CF27CADA6300C73B67 /* GenericVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34A2CE27CADA6300C73B67 /* GenericVersionView.swift */; }; 0E34AC7827F840890042F2AB /* OrganizerView+Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */; }; 0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */; }; 0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E35C099280E95BB0071FA35 /* ProviderProfileAvailability.swift */; }; - 0E3A3C132AAB7C480003A5F6 /* UpgradeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C112AAB7C470003A5F6 /* UpgradeManager.swift */; }; - 0E3A3C142AAB7C480003A5F6 /* DefaultUpgradeManagerStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C122AAB7C480003A5F6 /* DefaultUpgradeManagerStrategy.swift */; }; - 0E3A3C162AAB8AB80003A5F6 /* UpgradeManagerStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A3C152AAB8AB80003A5F6 /* UpgradeManagerStrategy.swift */; }; 0E3A593C2A50975700B3FE40 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3A593B2A50975700B3FE40 /* ErrorHandler.swift */; }; 0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FCC27E47B3700C66F13 /* AddHostView+Name.swift */; }; 0E3B7FD627E5173A00C66F13 /* ProfileView+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */; }; @@ -68,7 +68,6 @@ 0E49F6BB27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E49F6BA27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift */; }; 0E49F6BD27D7639000385834 /* EndpointAdvancedView+WireGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E49F6BC27D7639000385834 /* EndpointAdvancedView+WireGuard.swift */; }; 0E49F6BF27D764AF00385834 /* EndpointAdvancedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E49F6BE27D764AF00385834 /* EndpointAdvancedView.swift */; }; - 0E53249927D26B51002565C3 /* ProductManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E53249627D26B51002565C3 /* ProductManager.swift */; }; 0E53249D27D28FC7002565C3 /* Kvitto in Frameworks */ = {isa = PBXBuildFile; productRef = 0E53249C27D28FC7002565C3 /* Kvitto */; }; 0E5324A927D2AC55002565C3 /* LongContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5324A827D2AC55002565C3 /* LongContentView.swift */; }; 0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */; }; @@ -106,9 +105,7 @@ 0E71ACFB27C12E5300F85C4B /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACFA27C12E5300F85C4B /* VersionView.swift */; }; 0E71ACFD27C1321A00F85C4B /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACFC27C1321A00F85C4B /* ActivityView.swift */; }; 0E7577DF2817E22C00081CBE /* VPNToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7577DE2817E22C00081CBE /* VPNToggle.swift */; }; - 0E7A8C0A2A1D410500780F4B /* PersistenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7A8C092A1D410400780F4B /* PersistenceManager.swift */; }; 0E7A8C0C2A1D4A6100780F4B /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0E7A8C0B2A1D4A6100780F4B /* PassepartoutLibrary */; }; - 0E7A8C0E2A1D4A6E00780F4B /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0E7A8C0D2A1D4A6E00780F4B /* PassepartoutLibrary */; }; 0E7A8C0F2A1D54DE00780F4B /* Picker+OpenVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7A8C072A1D40BA00780F4B /* Picker+OpenVPN.swift */; }; 0E7A8C102A1D54DE00780F4B /* Picker+Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7A8C082A1D40BA00780F4B /* Picker+Network.swift */; }; 0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E90DFE527BACC1500EF5078 /* AddHostViewModel.swift */; }; @@ -138,7 +135,6 @@ 0E96D31128721855005EFBCF /* Constants+Mac.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E96D31028721855005EFBCF /* Constants+Mac.swift */; }; 0E96D31428721FC3005EFBCF /* ObservableProcessTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E96D31328721FC3005EFBCF /* ObservableProcessTransformer.swift */; }; 0E9AA978259F756A003FAFF1 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.swift */; }; - 0E9C233027F47032007D5FC7 /* IntentsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9C232F27F47032007D5FC7 /* IntentsManager.swift */; }; 0E9C233327F47E95007D5FC7 /* IntentDispatcher+Activities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9C233227F47E95007D5FC7 /* IntentDispatcher+Activities.swift */; }; 0E9C3B6F27FC573E00D0F02E /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E9C3B6E27FC573E00D0F02E /* CloudKit.framework */; }; 0E9E5AEF27B44CF1008C95DA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E9E5AE227B44CF1008C95DA /* Localizable.strings */; }; @@ -146,11 +142,9 @@ 0EA1D84728805EAE00F3CA48 /* Flags.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0EA1D84628805EAE00F3CA48 /* Flags.xcassets */; }; 0EA591162733DDDA0096F796 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0EA591142733DDDA0096F796 /* Intents.intentdefinition */; }; 0EA9030B287045F70087BC73 /* SystemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9030A287045F70087BC73 /* SystemMenu.swift */; }; - 0EB13BC0290E8C8D003CB654 /* PassepartoutTestsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB13BBF290E8C8D003CB654 /* PassepartoutTestsApp.swift */; }; 0EB17EA727D226B400D473B5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB17EA127D2263700D473B5 /* Constants.swift */; }; 0EB17EA927D226C900D473B5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB17EA127D2263700D473B5 /* Constants.swift */; }; 0EB17EAA27D226C900D473B5 /* Constants+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB17EA527D2263700D473B5 /* Constants+App.swift */; }; - 0EB17EAE27D226CF00D473B5 /* LocalProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB17EA327D2263700D473B5 /* LocalProduct.swift */; }; 0EB17EBA27D2560300D473B5 /* PassepartoutProviders+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB17EB927D2560300D473B5 /* PassepartoutProviders+Extensions.swift */; }; 0EB2B1482733FB6F007705AB /* PassepartoutOpenVPNTunnel.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0EDE8DBF20C86910004C739C /* PassepartoutOpenVPNTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 0EB3413027C7761A00483410 /* Binding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB3412F27C7761A00483410 /* Binding+Extensions.swift */; }; @@ -165,6 +159,8 @@ 0EBC075D27EC529000208AD9 /* DebugLog+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075C27EC529000208AD9 /* DebugLog+Constants.swift */; }; 0EBC076027EC587900208AD9 /* SwiftGen+Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBC075F27EC587900208AD9 /* SwiftGen+Strings.swift */; }; 0EBE880F281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBE880E281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift */; }; + 0EBF254E2B334C980045C547 /* StoreKitReceiptReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBF254D2B334C980045C547 /* StoreKitReceiptReader.swift */; }; + 0EBF25502B334CA00045C547 /* DefaultUpgradeManagerStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBF254F2B334CA00045C547 /* DefaultUpgradeManagerStrategy.swift */; }; 0ECB78E9285F5DE300B0E460 /* PassepartoutMac.bundle in Embed Plugins */ = {isa = PBXBuildFile; fileRef = 0ECB78DA285F52F700B0E460 /* PassepartoutMac.bundle */; platformFilter = maccatalyst; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0ECF71EE27B6A99300CDB528 /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECF71ED27B6A99300CDB528 /* AccountView.swift */; }; 0ED1A5FD2B2B98CC00A0EA90 /* Constants+Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED1A5FC2B2B98CC00A0EA90 /* Constants+Tunnel.swift */; }; @@ -217,13 +213,6 @@ remoteGlobalIDString = 0E41BD96286711C3006346B4; remoteInfo = PassepartoutLauncher; }; - 0EB13BBB290E82F1003CB654 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 0E57F63020C83FC5008323CF /* Project object */; - proxyType = 1; - remoteGlobalIDString = 0ECF71F327B6D9CD00CDB528; - remoteInfo = WireGuardGo; - }; 0EB2B1492733FB6F007705AB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0E57F63020C83FC5008323CF /* Project object */; @@ -301,9 +290,7 @@ /* Begin PBXFileReference section */ 0E021D9A284E68580077EF5D /* CoreContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreContext.swift; sourceTree = ""; }; 0E021D9B284E68580077EF5D /* AppContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; - 0E0392762818732D00827C10 /* BuildProducts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildProducts.swift; sourceTree = ""; }; 0E039278281890B100827C10 /* AddHostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHostView.swift; sourceTree = ""; }; - 0E0480372A1A4AE000462F2F /* Passepartout.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Passepartout.xctestplan; sourceTree = ""; }; 0E04F0052883462E00BFCE1C /* LightUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightUtils.swift; sourceTree = ""; }; 0E04F0082883466500BFCE1C /* DefaultLightUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultLightUtils.swift; sourceTree = ""; }; 0E065F102813269500062CAF /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; @@ -321,14 +308,12 @@ 0E0F4C5B29C76B790022E884 /* SceneDelegate+Shortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SceneDelegate+Shortcuts.swift"; sourceTree = ""; }; 0E0F4C6329C84B5A0022E884 /* LockableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockableView.swift; sourceTree = ""; }; 0E0F4C6529C84CF60022E884 /* LogoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoView.swift; sourceTree = ""; }; - 0E1AD5CB2A2682DA002AE6E6 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; 0E1AD5CD2A268645002AE6E6 /* Errors+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Errors+L10n.swift"; sourceTree = ""; }; 0E1B5F5B29C506AC00FE7D18 /* DiagnosticsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsSection.swift; sourceTree = ""; }; 0E1C0A52238FFF97009FC087 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; 0E1F5627287F0ECB00F8ADD7 /* ProviderProfileItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderProfileItem.swift; sourceTree = ""; }; 0E1F5629287F0EEE00F8ADD7 /* ProviderProfileItem+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProviderProfileItem+ViewModel.swift"; sourceTree = ""; }; 0E23B4A12298559800304C30 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; - 0E293850285A70AC002A6E0E /* AppPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreference.swift; sourceTree = ""; }; 0E293856285A73BC002A6E0E /* AppContext+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+Shared.swift"; sourceTree = ""; }; 0E2A8D4727ADF87F00207D04 /* PassepartoutApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassepartoutApp.swift; sourceTree = ""; }; 0E2A8D4E27B04BB900207D04 /* OrganizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizerView.swift; sourceTree = ""; }; @@ -338,15 +323,18 @@ 0E2DE71B27DCCFE80067B9E1 /* TunnelKit+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelKit+Extensions.swift"; sourceTree = ""; }; 0E2DE71E27DCD0290067B9E1 /* TunnelKit+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelKit+L10n.swift"; sourceTree = ""; }; 0E2DE72427DCDF550067B9E1 /* WireGuard+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WireGuard+L10n.swift"; sourceTree = ""; }; + 0E2E0B6D2B335A8E00E3204A /* AppPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppPreference.swift; sourceTree = ""; }; + 0E2E0B6E2B335A8E00E3204A /* AppError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; + 0E2E0B712B335AAB00E3204A /* IntentsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntentsManager.swift; sourceTree = ""; }; + 0E2E0B722B335AAB00E3204A /* UpgradeManagerStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpgradeManagerStrategy.swift; sourceTree = ""; }; + 0E2E0B732B335AAB00E3204A /* UpgradeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpgradeManager.swift; sourceTree = ""; }; + 0E2E0B742B335AAB00E3204A /* PersistenceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceManager.swift; sourceTree = ""; }; 0E34A2AF27CAA84500C73B67 /* OpenVPN+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenVPN+L10n.swift"; sourceTree = ""; }; 0E34A2B527CAA8CC00C73B67 /* Core+L10n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Core+L10n.swift"; sourceTree = ""; }; 0E34A2CE27CADA6300C73B67 /* GenericVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericVersionView.swift; sourceTree = ""; }; 0E34AC7727F840890042F2AB /* OrganizerView+Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+Scene.swift"; sourceTree = ""; }; 0E34AC8127F892C40042F2AB /* OnDemandView+SSID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnDemandView+SSID.swift"; sourceTree = ""; }; 0E35C099280E95BB0071FA35 /* ProviderProfileAvailability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderProfileAvailability.swift; sourceTree = ""; }; - 0E3A3C112AAB7C470003A5F6 /* UpgradeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeManager.swift; sourceTree = ""; }; - 0E3A3C122AAB7C480003A5F6 /* DefaultUpgradeManagerStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultUpgradeManagerStrategy.swift; sourceTree = ""; }; - 0E3A3C152AAB8AB80003A5F6 /* UpgradeManagerStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeManagerStrategy.swift; sourceTree = ""; }; 0E3A593B2A50975700B3FE40 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; 0E3B7FCC27E47B3700C66F13 /* AddHostView+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AddHostView+Name.swift"; sourceTree = ""; }; 0E3B7FD527E5173A00C66F13 /* ProfileView+VPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+VPN.swift"; sourceTree = ""; }; @@ -365,7 +353,6 @@ 0E49F6BA27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointAdvancedView+OpenVPN.swift"; sourceTree = ""; }; 0E49F6BC27D7639000385834 /* EndpointAdvancedView+WireGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointAdvancedView+WireGuard.swift"; sourceTree = ""; }; 0E49F6BE27D764AF00385834 /* EndpointAdvancedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndpointAdvancedView.swift; sourceTree = ""; }; - 0E53249627D26B51002565C3 /* ProductManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductManager.swift; sourceTree = ""; }; 0E5324A827D2AC55002565C3 /* LongContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongContentView.swift; sourceTree = ""; }; 0E5349BD27C16A4500C71BB3 /* StyledPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyledPicker.swift; sourceTree = ""; }; 0E5349C527C176C200C71BB3 /* EndpointView+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EndpointView+OpenVPN.swift"; sourceTree = ""; }; @@ -404,7 +391,6 @@ 0E7577DE2817E22C00081CBE /* VPNToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNToggle.swift; sourceTree = ""; }; 0E7A8C072A1D40BA00780F4B /* Picker+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Picker+OpenVPN.swift"; sourceTree = ""; }; 0E7A8C082A1D40BA00780F4B /* Picker+Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Picker+Network.swift"; sourceTree = ""; }; - 0E7A8C092A1D410400780F4B /* PersistenceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceManager.swift; sourceTree = ""; }; 0E90DFE527BACC1500EF5078 /* AddHostViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHostViewModel.swift; sourceTree = ""; }; 0E92D7C527F103300033CB7B /* ProfileView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Configuration.swift"; sourceTree = ""; }; 0E92D7C827F1042A0033CB7B /* ProfileView+Extra.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileView+Extra.swift"; sourceTree = ""; }; @@ -428,7 +414,6 @@ 0E96D31028721855005EFBCF /* Constants+Mac.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+Mac.swift"; sourceTree = ""; }; 0E96D31328721FC3005EFBCF /* ObservableProcessTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableProcessTransformer.swift; sourceTree = ""; }; 0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; - 0E9C232F27F47032007D5FC7 /* IntentsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentsManager.swift; sourceTree = ""; }; 0E9C233227F47E95007D5FC7 /* IntentDispatcher+Activities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentDispatcher+Activities.swift"; sourceTree = ""; }; 0E9C3B6E27FC573E00D0F02E /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 0E9E5AE327B44CF1008C95DA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; @@ -460,10 +445,7 @@ 0EA5912C2733DDFC0096F796 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Intents.strings; sourceTree = ""; }; 0EA5912E2733DDFD0096F796 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Intents.strings; sourceTree = ""; }; 0EA9030A287045F70087BC73 /* SystemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemMenu.swift; sourceTree = ""; }; - 0EB13BAC290E825E003CB654 /* PassepartoutTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PassepartoutTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 0EB13BBF290E8C8D003CB654 /* PassepartoutTestsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassepartoutTestsApp.swift; sourceTree = ""; }; 0EB17EA127D2263700D473B5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; - 0EB17EA327D2263700D473B5 /* LocalProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalProduct.swift; sourceTree = ""; }; 0EB17EA527D2263700D473B5 /* Constants+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Constants+App.swift"; sourceTree = ""; }; 0EB17EB927D2560300D473B5 /* PassepartoutProviders+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PassepartoutProviders+Extensions.swift"; sourceTree = ""; }; 0EB3412F27C7761A00483410 /* Binding+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Binding+Extensions.swift"; sourceTree = ""; }; @@ -487,6 +469,8 @@ 0EBE2FD72360F89600F0D5AB /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; 0EBE2FD82360F89600F0D5AB /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = ""; }; 0EBE880E281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrganizerView+ProfileRow.swift"; sourceTree = ""; }; + 0EBF254D2B334C980045C547 /* StoreKitReceiptReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitReceiptReader.swift; sourceTree = ""; }; + 0EBF254F2B334CA00045C547 /* DefaultUpgradeManagerStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultUpgradeManagerStrategy.swift; sourceTree = ""; }; 0ECB78DA285F52F700B0E460 /* PassepartoutMac.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PassepartoutMac.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; 0ECB78E1285F53ED00B0E460 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 0ECB78EA2861D1F300B0E460 /* PassepartoutLibrary */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = PassepartoutLibrary; sourceTree = ""; }; @@ -553,14 +537,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 0EB13BA9290E825E003CB654 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 0E7A8C0E2A1D4A6E00780F4B /* PassepartoutLibrary in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 0ECB78D7285F52F700B0E460 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -624,6 +600,18 @@ path = Reusable; sourceTree = ""; }; + 0E2E0B792B335B3C00E3204A /* Managers */ = { + isa = PBXGroup; + children = ( + 0EBF254F2B334CA00045C547 /* DefaultUpgradeManagerStrategy.swift */, + 0E2E0B712B335AAB00E3204A /* IntentsManager.swift */, + 0E2E0B742B335AAB00E3204A /* PersistenceManager.swift */, + 0E2E0B732B335AAB00E3204A /* UpgradeManager.swift */, + 0E2E0B722B335AAB00E3204A /* UpgradeManagerStrategy.swift */, + ); + path = Managers; + sourceTree = ""; + }; 0E34A2B827CAA8EA00C73B67 /* L10n */ = { isa = PBXGroup; children = ( @@ -701,31 +689,17 @@ path = Views; sourceTree = ""; }; - 0E3A593F2A54ACC900B3FE40 /* Managers */ = { - isa = PBXGroup; - children = ( - 0E3A3C122AAB7C480003A5F6 /* DefaultUpgradeManagerStrategy.swift */, - 0E9C232F27F47032007D5FC7 /* IntentsManager.swift */, - 0E7A8C092A1D410400780F4B /* PersistenceManager.swift */, - 0E53249627D26B51002565C3 /* ProductManager.swift */, - 0E3A3C112AAB7C470003A5F6 /* UpgradeManager.swift */, - 0E3A3C152AAB8AB80003A5F6 /* UpgradeManagerStrategy.swift */, - ); - path = Managers; - sourceTree = ""; - }; 0E3A59402A54AD4300B3FE40 /* Domain */ = { isa = PBXGroup; children = ( - 0E1AD5CB2A2682DA002AE6E6 /* AppError.swift */, - 0E293850285A70AC002A6E0E /* AppPreference.swift */, - 0E0392762818732D00827C10 /* BuildProducts.swift */, - 0EA591142733DDDA0096F796 /* Intents.intentdefinition */, + 0E2E0B6E2B335A8E00E3204A /* AppError.swift */, + 0E2E0B6D2B335A8E00E3204A /* AppPreference.swift */, 0EA591122733DD4E0096F796 /* IntentDispatcher.swift */, 0E9C233227F47E95007D5FC7 /* IntentDispatcher+Activities.swift */, - 0EB17EA327D2263700D473B5 /* LocalProduct.swift */, - 0E7A8C072A1D40BA00780F4B /* Picker+OpenVPN.swift */, + 0EA591142733DDDA0096F796 /* Intents.intentdefinition */, 0E7A8C082A1D40BA00780F4B /* Picker+Network.swift */, + 0E7A8C072A1D40BA00780F4B /* Picker+OpenVPN.swift */, + 0EBF254D2B334C980045C547 /* StoreKitReceiptReader.swift */, ); path = Domain; sourceTree = ""; @@ -830,9 +804,7 @@ children = ( 0EE315DB2733104700F5D461 /* Packages */, 0E23B4A12298559800304C30 /* Config.xcconfig */, - 0E0480372A1A4AE000462F2F /* Passepartout.xctestplan */, 0E9AA982259F7674003FAFF1 /* Passepartout */, - 0EB13BAD290E825E003CB654 /* PassepartoutTests */, 0E57F63920C83FC5008323CF /* Products */, 374B9F085E1148C37CF9117A /* Frameworks */, ); @@ -846,7 +818,6 @@ 0ED2B34A27D3C77800FD8EA9 /* PassepartoutWireGuardTunnel.appex */, 0E41BD97286711C3006346B4 /* PassepartoutLauncher.app */, 0ECB78DA285F52F700B0E460 /* PassepartoutMac.bundle */, - 0EB13BAC290E825E003CB654 /* PassepartoutTests.app */, ); name = Products; sourceTree = ""; @@ -902,9 +873,9 @@ 0E29385A285A749E002A6E0E /* Context */, 0E3A59402A54AD4300B3FE40 /* Domain */, 0E49F6C927DB398100385834 /* Extensions */, - 0E3A593F2A54ACC900B3FE40 /* Managers */, 0E34A2B827CAA8EA00C73B67 /* L10n */, 0E5467F12867A52B00F74D1C /* Mac */, + 0E2E0B792B335B3C00E3204A /* Managers */, 0E2C171C27CB6307007E8488 /* Reusable */, 0E35C0AE280EF8A80071FA35 /* Views */, 0E6059CA27FCC5DE003F4063 /* Assets.xcassets */, @@ -925,14 +896,6 @@ path = App; sourceTree = ""; }; - 0EB13BAD290E825E003CB654 /* PassepartoutTests */ = { - isa = PBXGroup; - children = ( - 0EB13BBF290E8C8D003CB654 /* PassepartoutTestsApp.swift */, - ); - path = PassepartoutTests; - sourceTree = ""; - }; 0ECB78D1285F4F4000B0E460 /* AppShared */ = { isa = PBXGroup; children = ( @@ -1100,27 +1063,6 @@ productReference = 0E57F63820C83FC5008323CF /* Passepartout.app */; productType = "com.apple.product-type.application"; }; - 0EB13BAB290E825E003CB654 /* PassepartoutTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 0EB13BBA290E8260003CB654 /* Build configuration list for PBXNativeTarget "PassepartoutTests" */; - buildPhases = ( - 0EB13BA8290E825E003CB654 /* Sources */, - 0EB13BA9290E825E003CB654 /* Frameworks */, - 0EB13BAA290E825E003CB654 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 0EB13BBC290E82F1003CB654 /* PBXTargetDependency */, - ); - name = PassepartoutTests; - packageProductDependencies = ( - 0E7A8C0D2A1D4A6E00780F4B /* PassepartoutLibrary */, - ); - productName = PassepartoutTests; - productReference = 0EB13BAC290E825E003CB654 /* PassepartoutTests.app */; - productType = "com.apple.product-type.application"; - }; 0ECB78D9285F52F700B0E460 /* PassepartoutMac */ = { isa = PBXNativeTarget; buildConfigurationList = 0ECB78DD285F52F700B0E460 /* Build configuration list for PBXNativeTarget "PassepartoutMac" */; @@ -1188,7 +1130,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1400; + LastSwiftUpdateCheck = 1510; LastUpgradeCheck = 1430; ORGANIZATIONNAME = "Davide De Rosa"; TargetAttributes = { @@ -1213,10 +1155,6 @@ }; }; }; - 0EB13BAB290E825E003CB654 = { - CreatedOnToolsVersion = 14.0.1; - LastSwiftMigration = 1400; - }; 0ECB78D9285F52F700B0E460 = { CreatedOnToolsVersion = 13.4; LastSwiftMigration = 1340; @@ -1269,7 +1207,6 @@ 0E57F63720C83FC5008323CF /* Passepartout */, 0ECB78D9285F52F700B0E460 /* PassepartoutMac */, 0E41BD96286711C3006346B4 /* PassepartoutLauncher */, - 0EB13BAB290E825E003CB654 /* PassepartoutTests */, 0EDE8DBE20C86910004C739C /* OpenVPNTunnel */, 0ECF71F327B6D9CD00CDB528 /* WireGuardGo */, 0ED2B33E27D3C77800FD8EA9 /* WireGuardTunnel */, @@ -1300,13 +1237,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 0EB13BAA290E825E003CB654 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 0ECB78D8285F52F700B0E460 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1439,6 +1369,7 @@ 0EA591162733DDDA0096F796 /* Intents.intentdefinition in Sources */, 0E34AC7827F840890042F2AB /* OrganizerView+Scene.swift in Sources */, 0E0BD27927B2EBE500583AC5 /* ShortcutsView.swift in Sources */, + 0E2E0B762B335AAB00E3204A /* UpgradeManagerStrategy.swift in Sources */, 0E92D7C627F103300033CB7B /* ProfileView+Configuration.swift in Sources */, 0E2DE71C27DCCFE80067B9E1 /* TunnelKit+Extensions.swift in Sources */, 0ED1D6DE27DBA42100983466 /* DiagnosticsView+WireGuard.swift in Sources */, @@ -1448,13 +1379,13 @@ 0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */, 0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */, 0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */, + 0EBF25502B334CA00045C547 /* DefaultUpgradeManagerStrategy.swift in Sources */, 0EB90CC129C25BBD00E64628 /* InteractiveConnectionView.swift in Sources */, 0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */, 0E04F0092883466500BFCE1C /* DefaultLightUtils.swift in Sources */, 0E5349C827C176D100C71BB3 /* EndpointView+WireGuard.swift in Sources */, 0EBC076027EC587900208AD9 /* SwiftGen+Strings.swift in Sources */, 0E7A8C0F2A1D54DE00780F4B /* Picker+OpenVPN.swift in Sources */, - 0E0392772818732D00827C10 /* BuildProducts.swift in Sources */, 0E0F4C5C29C76B790022E884 /* SceneDelegate+Shortcuts.swift in Sources */, 0E5683B927C2825D00EAF1CD /* DiagnosticsView.swift in Sources */, 0E3FC6862867A3F9009B851C /* AppDelegate.swift in Sources */, @@ -1478,7 +1409,6 @@ 0ED89C1E27DE3F8D008B36D6 /* IntentAddView.swift in Sources */, 0E5468002867AC9A00F74D1C /* MacUtils.swift in Sources */, 0E0F4C6429C84B5A0022E884 /* LockableView.swift in Sources */, - 0E3A3C132AAB7C480003A5F6 /* UpgradeManager.swift in Sources */, 0E96D3052872010A005EFBCF /* DefaultLightVPNManager.swift in Sources */, 0EBE880F281B18DE0090D9E6 /* OrganizerView+ProfileRow.swift in Sources */, 0ED30DCF27EA1EF80057D8A3 /* PaywallView+Restricted.swift in Sources */, @@ -1494,12 +1424,15 @@ 0E0838FA2877325A00A34EC0 /* LightProviderManager.swift in Sources */, 0ED7D62F2867328A009F2F8F /* Constants+Library.swift in Sources */, 0E5467FA2867AA0A00F74D1C /* MacBundleDelegate.swift in Sources */, + 0EBF254E2B334C980045C547 /* StoreKitReceiptReader.swift in Sources */, 0E0BD27327B2EA2C00583AC5 /* MainView.swift in Sources */, 0E0F4C5A29C761850022E884 /* SceneDelegate.swift in Sources */, 0EB17EBA27D2560300D473B5 /* PassepartoutProviders+Extensions.swift in Sources */, 0E3B7FDA27E51A0200C66F13 /* ProfileView+Provider.swift in Sources */, + 0E2E0B6F2B335A8E00E3204A /* AppPreference.swift in Sources */, 0E5468062867AEC500F74D1C /* MacMenu.swift in Sources */, 0E71ACE327C0F2E400F85C4B /* Providers+L10n.swift in Sources */, + 0E2E0B752B335AAB00E3204A /* IntentsManager.swift in Sources */, 0E71ACF127C1073800F85C4B /* ProviderLocationView.swift in Sources */, 0E2A8D4F27B04BBA00207D04 /* OrganizerView.swift in Sources */, 0E49F6BB27D7638300385834 /* EndpointAdvancedView+OpenVPN.swift in Sources */, @@ -1509,13 +1442,11 @@ 0E96D2FC2871D94E005EFBCF /* DefaultLightProfileManager.swift in Sources */, 0EF0FAF627DD0211007EB181 /* PaywallView.swift in Sources */, 0E7A8C102A1D54DE00780F4B /* Picker+Network.swift in Sources */, - 0E293851285A70AC002A6E0E /* AppPreference.swift in Sources */, + 0E2E0B702B335A8E00E3204A /* AppError.swift in Sources */, 0E5349BE27C16A4500C71BB3 /* StyledPicker.swift in Sources */, 0E2C172B27CB63F9007E8488 /* Reviewer.swift in Sources */, 0E71ACDD27C0295C00F85C4B /* View+Extensions.swift in Sources */, - 0E3A3C162AAB8AB80003A5F6 /* UpgradeManagerStrategy.swift in Sources */, 0E021D9C284E68580077EF5D /* CoreContext.swift in Sources */, - 0E7A8C0A2A1D410500780F4B /* PersistenceManager.swift in Sources */, A38D607728AFCFD20005C271 /* SettingsView.swift in Sources */, 0E34A2B627CAA8CC00C73B67 /* Core+L10n.swift in Sources */, 0E7577DF2817E22C00081CBE /* VPNToggle.swift in Sources */, @@ -1525,7 +1456,7 @@ 0EBC075527EBC83800208AD9 /* MailComposerView.swift in Sources */, 0EF0FAF727DD159C007EB181 /* IntentDispatcher.swift in Sources */, 0E0838FD2877334300A34EC0 /* DefaultLightProviderManager.swift in Sources */, - 0E3A3C142AAB7C480003A5F6 /* DefaultUpgradeManagerStrategy.swift in Sources */, + 0E2E0B772B335AAB00E3204A /* UpgradeManager.swift in Sources */, 0E0F4C6629C84CF60022E884 /* LogoView.swift in Sources */, 0E039279281890B100827C10 /* AddHostView.swift in Sources */, 0E5467F72867A57000F74D1C /* MacBridge.swift in Sources */, @@ -1536,14 +1467,11 @@ 0E71ACF927C12E4800F85C4B /* CreditsView.swift in Sources */, 0ED89C1527DE0A0C008B36D6 /* Shortcut.swift in Sources */, 0E34A2B927CAA96A00C73B67 /* OpenVPN+L10n.swift in Sources */, + 0E2E0B782B335AAB00E3204A /* PersistenceManager.swift in Sources */, 0EF8C5A828213C510053CE89 /* OrganizerView+Profiles.swift in Sources */, 0E3CD483280DAE92007075C0 /* ProfileView+MainMenu.swift in Sources */, - 0EB17EAE27D226CF00D473B5 /* LocalProduct.swift in Sources */, 0E71ACEB27C1060D00F85C4B /* EndpointView.swift in Sources */, 0E293857285A73BC002A6E0E /* AppContext+Shared.swift in Sources */, - 0E1AD5CC2A2682DA002AE6E6 /* AppError.swift in Sources */, - 0E53249927D26B51002565C3 /* ProductManager.swift in Sources */, - 0E9C233027F47032007D5FC7 /* IntentsManager.swift in Sources */, 0EB4042C27CA0E8C00378B1A /* Unlocalized.swift in Sources */, 0EB4042E27CA136300378B1A /* AddingTextField.swift in Sources */, 0EE8B7E327FF340F00B68621 /* VPNProtocolType+FileExtensions.swift in Sources */, @@ -1566,14 +1494,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 0EB13BA8290E825E003CB654 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 0EB13BC0290E8C8D003CB654 /* PassepartoutTestsApp.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 0ED2B34027D3C77800FD8EA9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1603,11 +1523,6 @@ target = 0E41BD96286711C3006346B4 /* PassepartoutLauncher */; targetProxy = 0E41BDAA286713F6006346B4 /* PBXContainerItemProxy */; }; - 0EB13BBC290E82F1003CB654 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 0ECF71F327B6D9CD00CDB528 /* WireGuardGo */; - targetProxy = 0EB13BBB290E82F1003CB654 /* PBXContainerItemProxy */; - }; 0EB2B14A2733FB6F007705AB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 0EDE8DBE20C86910004C739C /* OpenVPNTunnel */; @@ -1710,7 +1625,6 @@ COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = DTDYD63ZX9; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Passepartout/Launcher/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1735,7 +1649,6 @@ COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = DTDYD63ZX9; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Passepartout/Launcher/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1789,6 +1702,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CURRENT_PROJECT_VERSION = 3548; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = "$(CFG_TEAM_ID)"; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1856,6 +1770,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CURRENT_PROJECT_VERSION = 3548; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = "$(CFG_TEAM_ID)"; ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -1889,7 +1804,6 @@ CODE_SIGN_ENTITLEMENTS = Passepartout/App/App.entitlements; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3548; - DEVELOPMENT_TEAM = DTDYD63ZX9; INFOPLIST_FILE = Passepartout/App/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1916,7 +1830,6 @@ CODE_SIGN_ENTITLEMENTS = Passepartout/App/App.entitlements; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 3548; - DEVELOPMENT_TEAM = DTDYD63ZX9; INFOPLIST_FILE = Passepartout/App/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1932,75 +1845,6 @@ }; name = Release; }; - 0EB13BB8290E8260003CB654 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Manual; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; - "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; - LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; - "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.algoritmico.ios.PassepartoutTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SDKROOT = auto; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 0EB13BB9290E8260003CB654 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Manual; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - ENABLE_HARDENED_RUNTIME = YES; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; - "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; - "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; - "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; - LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; - "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.algoritmico.ios.PassepartoutTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SDKROOT = auto; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; 0ECB78DB285F52F700B0E460 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2012,7 +1856,6 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 3548; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Passepartout/Mac/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; LD_RUNPATH_SEARCH_PATHS = ( @@ -2046,7 +1889,6 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 3548; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Passepartout/Mac/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; LD_RUNPATH_SEARCH_PATHS = ( @@ -2076,7 +1918,6 @@ COPY_PHASE_STRIP = NO; DEBUGGING_SYMBOLS = YES; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = DTDYD63ZX9; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_OPTIMIZATION_LEVEL = 0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -2094,7 +1935,6 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = DTDYD63ZX9; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_CFLAGS = ""; @@ -2108,7 +1948,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = Passepartout/Tunnel/Tunnel.entitlements; CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = DTDYD63ZX9; INFOPLIST_FILE = Passepartout/Tunnel/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -2129,7 +1968,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = Passepartout/Tunnel/Tunnel.entitlements; CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = DTDYD63ZX9; INFOPLIST_FILE = Passepartout/Tunnel/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -2150,7 +1988,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = Passepartout/Tunnel/Tunnel.entitlements; CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = DTDYD63ZX9; INFOPLIST_FILE = Passepartout/Tunnel/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -2171,7 +2008,6 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = Passepartout/Tunnel/Tunnel.entitlements; CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = DTDYD63ZX9; INFOPLIST_FILE = Passepartout/Tunnel/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -2217,15 +2053,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 0EB13BBA290E8260003CB654 /* Build configuration list for PBXNativeTarget "PassepartoutTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 0EB13BB8290E8260003CB654 /* Debug */, - 0EB13BB9290E8260003CB654 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 0ECB78DD285F52F700B0E460 /* Build configuration list for PBXNativeTarget "PassepartoutMac" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -2285,10 +2112,6 @@ isa = XCSwiftPackageProductDependency; productName = PassepartoutLibrary; }; - 0E7A8C0D2A1D4A6E00780F4B /* PassepartoutLibrary */ = { - isa = XCSwiftPackageProductDependency; - productName = PassepartoutLibrary; - }; 0ED2B33827D3C49800FD8EA9 /* OpenVPNAppExtension */ = { isa = XCSwiftPackageProductDependency; productName = OpenVPNAppExtension; diff --git a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme index abb44c3c..f03ed8c6 100644 --- a/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme +++ b/Passepartout.xcodeproj/xcshareddata/xcschemes/Passepartout.xcscheme @@ -68,7 +68,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Passepartout.xctestplan b/Passepartout.xctestplan deleted file mode 100644 index 81bb32e4..00000000 --- a/Passepartout.xctestplan +++ /dev/null @@ -1,73 +0,0 @@ -{ - "configurations" : [ - { - "id" : "38DB0D3B-F3E5-4BD4-B413-B9C92AE7A2F5", - "name" : "Configuration 1", - "options" : { - - } - } - ], - "defaultOptions" : { - "codeCoverage" : false, - "commandLineArgumentEntries" : [ - { - "argument" : "-com.apple.CoreData.SQLDebug 0" - }, - { - "argument" : "-com.apple.CoreData.Logging.stderr 0 " - }, - { - "argument" : "-com.apple.CoreData.CloudKitDebug 0" - }, - { - "argument" : "-com.apple.CoreData.ConcurrencyDebug 0" - }, - { - "argument" : "-com.apple.CoreData.MigrationDebug 0" - } - ], - "environmentVariableEntries" : [ - { - "key" : "APP_TYPE", - "value" : "2" - }, - { - "key" : "LOG_LEVEL", - "value" : "0" - } - ], - "targetForVariableExpansion" : { - "containerPath" : "container:Passepartout.xcodeproj", - "identifier" : "0E57F63720C83FC5008323CF", - "name" : "Passepartout" - } - }, - "testTargets" : [ - { - "enabled" : false, - "target" : { - "containerPath" : "container:PassepartoutLibrary", - "identifier" : "PassepartoutCoreTests", - "name" : "PassepartoutCoreTests" - } - }, - { - "enabled" : false, - "target" : { - "containerPath" : "container:PassepartoutLibrary", - "identifier" : "PassepartoutProvidersTests", - "name" : "PassepartoutProvidersTests" - } - }, - { - "enabled" : false, - "target" : { - "containerPath" : "container:PassepartoutLibrary", - "identifier" : "PassepartoutVPNTests", - "name" : "PassepartoutVPNTests" - } - } - ], - "version" : 1 -} diff --git a/Passepartout/App/Constants/Constants+App.swift b/Passepartout/App/Constants/Constants+App.swift index e8dde14f..b44b10fe 100644 --- a/Passepartout/App/Constants/Constants+App.swift +++ b/Passepartout/App/Constants/Constants+App.swift @@ -52,15 +52,15 @@ extension Constants { } enum InApp { - static var overriddenAppType: ProductManager.AppType? { + static var overriddenAppType: AppType? { if let envString = ProcessInfo.processInfo.environment["APP_TYPE"], let envValue = Int(envString), - let testAppType = ProductManager.AppType(rawValue: envValue) { + let testAppType = AppType(rawValue: envValue) { return testAppType } if let infoValue: Int = bundleConfig("app_type"), - let testAppType = ProductManager.AppType(rawValue: infoValue) { + let testAppType = AppType(rawValue: infoValue) { return testAppType } @@ -228,9 +228,6 @@ extension Constants { // @available(*, deprecated, message: "File importer stops showing again after closing with swipe down") static let xxxPresentFileImporter = 200 - -// @available(*, deprecated, message: "Edited shortcut is outdated in delegate") - static let xxxReloadEditedShortcut = 200 } enum Rating { diff --git a/Passepartout/App/Context/AppContext.swift b/Passepartout/App/Context/AppContext.swift index 6a545a04..214aa2bd 100644 --- a/Passepartout/App/Context/AppContext.swift +++ b/Passepartout/App/Context/AppContext.swift @@ -57,6 +57,8 @@ final class AppContext { upgradeManager.migrate(toVersion: Constants.Global.appVersionNumber) productManager = ProductManager( + inApp: StoreKitInApp(), + receiptReader: StoreKitReceiptReader(), overriddenAppType: Constants.InApp.overriddenAppType, buildProducts: Constants.InApp.buildProducts ) diff --git a/Passepartout/App/Domain/StoreKitReceiptReader.swift b/Passepartout/App/Domain/StoreKitReceiptReader.swift new file mode 100644 index 00000000..471d80ee --- /dev/null +++ b/Passepartout/App/Domain/StoreKitReceiptReader.swift @@ -0,0 +1,79 @@ +// +// StoreKitReceiptReader.swift +// Passepartout +// +// Created by Davide De Rosa on 12/20/23. +// Copyright (c) 2023 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation +import Kvitto +import PassepartoutLibrary + +struct StoreKitReceiptReader: ReceiptReader { + func receipt(for appType: AppType) -> InAppReceipt? { + guard let url = Bundle.main.appStoreReceiptURL else { + pp_log.warning("No App Store receipt found!") + return nil + } + + let receipt = Receipt(contentsOfURL: url) + + let fallbackReceipt: Receipt? = { + + // in TestFlight, attempt fallback to existing release receipt + if appType == .beta { + guard let receipt else { + let releaseUrl = url.deletingLastPathComponent().appendingPathComponent("receipt") + guard releaseUrl != url else { + assertionFailure("How can release URL be equal to sandbox URL in TestFlight?") + return nil + } + pp_log.warning("Sandbox receipt not found, falling back to Release receipt") + return Receipt(contentsOfURL: releaseUrl) + } + return receipt + } + return receipt + }() + + return fallbackReceipt?.asInAppReceipt + } +} + +private extension Receipt { + var asInAppReceipt: InAppReceipt { + var originalBuildNumber: Int? + var purchaseReceipts: [InAppReceipt.PurchaseReceipt]? + if let originalAppVersion, let buildNumber = Int(originalAppVersion) { + originalBuildNumber = buildNumber + } + if let inAppPurchaseReceipts { + purchaseReceipts = inAppPurchaseReceipts + .map { + InAppReceipt.PurchaseReceipt(productIdentifier: $0.productIdentifier, + cancellationDate: $0.cancellationDate, + originalPurchaseDate: $0.originalPurchaseDate) + } + } + return InAppReceipt(originalBuildNumber: originalBuildNumber, + purchaseReceipts: purchaseReceipts) + } +} diff --git a/Passepartout/App/Managers/DefaultUpgradeManagerStrategy.swift b/Passepartout/App/Managers/DefaultUpgradeManagerStrategy.swift index 2892b9c5..5ced2f72 100644 --- a/Passepartout/App/Managers/DefaultUpgradeManagerStrategy.swift +++ b/Passepartout/App/Managers/DefaultUpgradeManagerStrategy.swift @@ -26,11 +26,11 @@ import Foundation import PassepartoutLibrary -public final class DefaultUpgradeManagerStrategy: UpgradeManagerStrategy { - public init() { +final class DefaultUpgradeManagerStrategy: UpgradeManagerStrategy { + init() { } - public func migrate(store: KeyValueStore, lastVersion: String?) { + func migrate(store: KeyValueStore, lastVersion: String?) { // legacy check before lastVersion was even stored let isUpgradeFromBefore_2_2_0: Bool? = store.value(forLocation: UpgradeManager.StoreKey.existingKeyBefore_2_2_0) @@ -47,6 +47,6 @@ public final class DefaultUpgradeManagerStrategy: UpgradeManagerStrategy { pp_log.debug("Upgrade from \(lastVersion)") } - public func migrateData() { + func migrateData() { } } diff --git a/Passepartout/App/Managers/IntentsManager.swift b/Passepartout/App/Managers/IntentsManager.swift index ec29d0b5..c1ded1af 100644 --- a/Passepartout/App/Managers/IntentsManager.swift +++ b/Passepartout/App/Managers/IntentsManager.swift @@ -92,7 +92,7 @@ extension IntentsManager: INUIEditVoiceShortcutViewControllerDelegate { // // so damn it, reload manually after a delay Task { - await Task.maybeWait(forMilliseconds: Constants.Delays.xxxReloadEditedShortcut) + await Task.maybeWait(forMilliseconds: 200) await reloadShortcuts() } } diff --git a/Passepartout/App/Managers/UpgradeManager.swift b/Passepartout/App/Managers/UpgradeManager.swift index 0d7de479..d4a5066d 100644 --- a/Passepartout/App/Managers/UpgradeManager.swift +++ b/Passepartout/App/Managers/UpgradeManager.swift @@ -27,7 +27,7 @@ import Foundation import PassepartoutLibrary @MainActor -public final class UpgradeManager: ObservableObject { +final class UpgradeManager: ObservableObject { // MARK: Initialization @@ -37,9 +37,9 @@ public final class UpgradeManager: ObservableObject { // MARK: State - @Published public private(set) var isDoingMigrations = true + @Published private(set) var isDoingMigrations = true - public init( + init( store: KeyValueStore, strategy: UpgradeManagerStrategy ) { @@ -47,14 +47,14 @@ public final class UpgradeManager: ObservableObject { self.strategy = strategy } - public func migrate(toVersion currentVersion: String) { + func migrate(toVersion currentVersion: String) { if let lastVersion, currentVersion > lastVersion { strategy.migrate(store: store, lastVersion: lastVersion) } lastVersion = currentVersion } - public func migrateData(profileManager: ProfileManager) { + func migrateData(profileManager: ProfileManager) { isDoingMigrations = false } } diff --git a/Passepartout/App/Managers/UpgradeManagerStrategy.swift b/Passepartout/App/Managers/UpgradeManagerStrategy.swift index 87b42f38..2c1ae67f 100644 --- a/Passepartout/App/Managers/UpgradeManagerStrategy.swift +++ b/Passepartout/App/Managers/UpgradeManagerStrategy.swift @@ -24,7 +24,7 @@ // import Foundation -import PassepartoutCore +import PassepartoutLibrary public protocol UpgradeManagerStrategy { func migrate(store: KeyValueStore, lastVersion: String?) diff --git a/Passepartout/App/Views/DonateView.swift b/Passepartout/App/Views/DonateView.swift index 5b6640d5..5fcb5e75 100644 --- a/Passepartout/App/Views/DonateView.swift +++ b/Passepartout/App/Views/DonateView.swift @@ -24,7 +24,6 @@ // import PassepartoutLibrary -import StoreKit import SwiftUI struct DonateView: View { @@ -113,7 +112,7 @@ private extension DonateView { } @ViewBuilder - func productRow(_ product: SKProduct) -> some View { + func productRow(_ product: InAppProduct) -> some View { HStack { Button(product.localizedTitle) { purchaseProduct(product) @@ -132,10 +131,10 @@ private extension DonateView { } private extension ProductManager { - var donations: [SKProduct] { + var donations: [InAppProduct] { products.filter { product in LocalProduct.allDonations.contains { - $0.matchesStoreKitProduct(product) + $0.matchesInAppProduct(product) } }.sorted { $0.price.decimalValue < $1.price.decimalValue @@ -146,7 +145,7 @@ private extension ProductManager { // MARK: - private extension DonateView { - func purchaseProduct(_ product: SKProduct) { + func purchaseProduct(_ product: InAppProduct) { pendingDonationIdentifier = product.productIdentifier productManager.purchase(product, completionHandler: handlePurchaseResult) } diff --git a/Passepartout/App/Views/PaywallView+Purchase.swift b/Passepartout/App/Views/PaywallView+Purchase.swift index 675dc6f0..1eb89b0b 100644 --- a/Passepartout/App/Views/PaywallView+Purchase.swift +++ b/Passepartout/App/Views/PaywallView+Purchase.swift @@ -24,13 +24,12 @@ // import PassepartoutLibrary -import StoreKit import SwiftUI extension PaywallView { struct PurchaseView: View { fileprivate enum PurchaseState { - case purchasing(SKProduct) + case purchasing(InAppProduct) case restoring } @@ -70,7 +69,7 @@ extension PaywallView { } private struct PurchaseRow: View { - var product: SKProduct? + var product: InAppProduct? let title: String @@ -93,7 +92,7 @@ private struct PurchaseRow: View { } } -private typealias RowModel = (product: SKProduct, extra: String?) +private typealias RowModel = (product: InAppProduct, extra: String?) // MARK: - @@ -136,14 +135,14 @@ private extension PaywallView.PurchaseView { } private extension PaywallView.PurchaseView { - var skFeature: SKProduct? { + var skFeature: InAppProduct? { guard let feature = feature else { return nil } return productManager.product(withIdentifier: feature) } - var skPlatformVersion: SKProduct? { + var skPlatformVersion: InAppProduct? { #if targetEnvironment(macCatalyst) productManager.product(withIdentifier: .fullVersion_macOS) #else @@ -152,7 +151,7 @@ private extension PaywallView.PurchaseView { } // hide full version if already bought the other platform version - var skFullVersion: SKProduct? { + var skFullVersion: InAppProduct? { #if targetEnvironment(macCatalyst) guard !productManager.hasPurchased(.fullVersion_iOS) else { return nil @@ -209,14 +208,14 @@ private extension PurchaseRow { @ViewBuilder var actionButton: some View { - if let product = product { + if let product { purchaseButton(product) } else { restoreButton } } - func purchaseButton(_ product: SKProduct) -> some View { + func purchaseButton(_ product: InAppProduct) -> some View { HStack { Button(title, action: action) Spacer() @@ -245,7 +244,7 @@ private extension PurchaseRow { // MARK: - private extension PaywallView.PurchaseView { - func purchaseProduct(_ product: SKProduct) { + func purchaseProduct(_ product: InAppProduct) { purchaseState = .purchasing(product) productManager.purchase(product) { diff --git a/Passepartout/App/Views/PaywallView.swift b/Passepartout/App/Views/PaywallView.swift index a779dd4b..f0553420 100644 --- a/Passepartout/App/Views/PaywallView.swift +++ b/Passepartout/App/Views/PaywallView.swift @@ -24,6 +24,7 @@ // import SwiftUI +import PassepartoutLibrary struct PaywallView: View { @ObservedObject private var productManager: ProductManager diff --git a/PassepartoutLibrary/.swiftpm/PassepartoutLibrary.xctestplan b/PassepartoutLibrary/.swiftpm/PassepartoutLibrary.xctestplan deleted file mode 100644 index f95d91d0..00000000 --- a/PassepartoutLibrary/.swiftpm/PassepartoutLibrary.xctestplan +++ /dev/null @@ -1,45 +0,0 @@ -{ - "configurations" : [ - { - "id" : "2A4B150B-39A2-417B-98FD-1C5C74C26E3E", - "name" : "Test Scheme Action", - "options" : { - - } - } - ], - "defaultOptions" : { - "codeCoverage" : false - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:", - "identifier" : "PassepartoutCoreTests", - "name" : "PassepartoutCoreTests" - } - }, - { - "target" : { - "containerPath" : "container:", - "identifier" : "PassepartoutProvidersTests", - "name" : "PassepartoutProvidersTests" - } - }, - { - "target" : { - "containerPath" : "container:", - "identifier" : "PassepartoutServicesTests", - "name" : "PassepartoutServicesTests" - } - }, - { - "target" : { - "containerPath" : "container:", - "identifier" : "PassepartoutVPNTests", - "name" : "PassepartoutVPNTests" - } - } - ], - "version" : 1 -} diff --git a/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutCoreTests.xcscheme b/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutCoreTests.xcscheme deleted file mode 100644 index b1df5d8d..00000000 --- a/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutCoreTests.xcscheme +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutLibrary-Package.xcscheme b/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutLibrary-Package.xcscheme index abc851a7..17f71550 100644 --- a/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutLibrary-Package.xcscheme +++ b/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutLibrary-Package.xcscheme @@ -1,39 +1,11 @@ - - - - - - - - @@ -76,62 +48,6 @@ ReferencedContainer = "container:"> - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - diff --git a/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutLibrary.xcscheme b/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutLibrary.xcscheme index 2836c84b..ef527420 100644 --- a/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutLibrary.xcscheme +++ b/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutLibrary.xcscheme @@ -1,6 +1,6 @@ - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> - - - - - - - - - - - - - - - - - - - - diff --git a/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutVPNTests.xcscheme b/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutVPNTests.xcscheme deleted file mode 100644 index 2560f992..00000000 --- a/PassepartoutLibrary/.swiftpm/xcode/xcshareddata/xcschemes/PassepartoutVPNTests.xcscheme +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/PassepartoutLibrary/Package.swift b/PassepartoutLibrary/Package.swift index 4243ec81..d382c18f 100644 --- a/PassepartoutLibrary/Package.swift +++ b/PassepartoutLibrary/Package.swift @@ -13,6 +13,14 @@ let package = Package( .library( name: "PassepartoutLibrary", targets: ["PassepartoutLibrary"]), + .library( + name: "PassepartoutInterfaces", + targets: [ + "PassepartoutCore", + "PassepartoutFrontend", + "PassepartoutProviders", + "PassepartoutServices" + ]), .library( name: "OpenVPNAppExtension", targets: ["OpenVPNAppExtension"]), @@ -72,6 +80,11 @@ let package = Package( .product(name: "TunnelKitOpenVPN", package: "TunnelKit"), .product(name: "TunnelKitWireGuard", package: "TunnelKit"), ]), + .target( + name: "PassepartoutFrontend", + dependencies: [ + "PassepartoutProviders" + ]), .target( name: "PassepartoutProviders", dependencies: [ @@ -104,17 +117,20 @@ let package = Package( // MARK: Tests - .testTarget( - name: "PassepartoutVPNTests", - dependencies: ["PassepartoutVPNImpl"]), - .testTarget( - name: "PassepartoutProvidersTests", - dependencies: ["PassepartoutProvidersImpl"]), .testTarget( name: "PassepartoutCoreTests", dependencies: ["PassepartoutCore"], resources: [ .process("Resources") - ]) + ]), + .testTarget( + name: "PassepartoutFrontendTests", + dependencies: ["PassepartoutFrontend"]), + .testTarget( + name: "PassepartoutProvidersTests", + dependencies: ["PassepartoutProviders"]), + .testTarget( + name: "PassepartoutServicesTests", + dependencies: ["PassepartoutServices"]) ] ) diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/InApp.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/InApp.swift index 36b7eef9..a1e37c2f 100644 --- a/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/InApp.swift +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/InApp.swift @@ -24,9 +24,8 @@ // import Foundation -import StoreKit -public enum InAppPurchaseResult { +public enum InAppPurchaseResult: Sendable { case done case cancelled @@ -36,188 +35,57 @@ public enum InAppError: Error { case unknown } -public final class InApp: NSObject, - SKProductsRequestDelegate, SKPaymentTransactionObserver - where PID.RawValue == String { +public struct InAppProduct: Sendable { + public let productIdentifier: String - public typealias ProductObserver = ([PID: SKProduct]) -> Void + public let localizedTitle: String - public typealias TransactionObserver = (Result) -> Void + public let localizedDescription: String - public typealias RestoreObserver = (Bool, PID?, Error?) -> Void + public let price: NSDecimalNumber - public typealias FailureObserver = (Error) -> Void - - private var productsMap: [PID: SKProduct] - - public var products: [SKProduct] { - [SKProduct](productsMap.values) - } - - private var productObservers: [ProductObserver] - - private var productFailureObserver: FailureObserver? - - private var transactionObservers: [String: TransactionObserver] - - private var restoreObservers: [RestoreObserver] - - public override init() { - productsMap = [:] - productObservers = [] - productFailureObserver = nil - transactionObservers = [:] - restoreObservers = [] - super.init() - - SKPaymentQueue.default().add(self) - } - - deinit { - SKPaymentQueue.default().remove(self) - } - - public func requestProducts(withIdentifiers identifiers: [PID], completionHandler: ProductObserver?, failureHandler: FailureObserver?) { - let req = SKProductsRequest(productIdentifiers: Set(identifiers.map { $0.rawValue })) - req.delegate = self - if let observer = completionHandler { - productObservers.append(observer) - } - productFailureObserver = failureHandler - req.start() - } - - @discardableResult - public func purchase(productWithIdentifier productIdentifier: PID, completionHandler: @escaping TransactionObserver) -> Bool { - guard let product = productsMap[productIdentifier] else { - return false - } - purchase(product: product, completionHandler: completionHandler) - return true - } - - public func purchase(product: SKProduct, completionHandler: @escaping TransactionObserver) { - let queue = SKPaymentQueue.default() - let payment = SKPayment(product: product) - transactionObservers[product.productIdentifier] = completionHandler - queue.add(payment) - } - - public func restorePurchases(completionHandler: @escaping RestoreObserver) { - let queue = SKPaymentQueue.default() - restoreObservers.append(completionHandler) - queue.restoreCompletedTransactions() - } - - public func product(withIdentifier productIdentifier: PID) -> SKProduct? { - productsMap[productIdentifier] - } - - // MARK: SKProductsRequestDelegate - - public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { - DispatchQueue.main.async { - self.receiveProducts(response.products) - } - } - - public func request(_ request: SKRequest, didFailWithError error: Error) { - if request as? SKProductsRequest != nil { - DispatchQueue.main.async { - self.productFailureObserver?(error) - } - } - DispatchQueue.main.async { - self.transactionObservers.removeAll() - } - } - - private func receiveProducts(_ products: [SKProduct]) { - productsMap.removeAll() - for p in products { - guard let pid = PID(rawValue: p.productIdentifier) else { - continue - } - productsMap[pid] = p - } - productObservers.forEach { $0(productsMap) } - productObservers.removeAll() - } - - // MARK: SKPaymentTransactionObserver - - public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { - DispatchQueue.main.async { - let currentRestoreObservers = self.restoreObservers - - for tx in transactions { - let productIdentifier = tx.payment.productIdentifier - let observer = self.transactionObservers[productIdentifier] - - switch tx.transactionState { - case .purchased: - queue.finishTransaction(tx) - observer?(.success(.done)) - - case .restored: - queue.finishTransaction(tx) - observer?(.success(.done)) - for r in currentRestoreObservers { - guard let pid = PID(rawValue: productIdentifier) else { - continue - } - r(false, pid, nil) - } - - case .failed: - queue.finishTransaction(tx) - if let skError = tx.error as? SKError, skError.code == .paymentCancelled { - observer?(.success(.cancelled)) - } else { - observer?(.failure(tx.error ?? InAppError.unknown)) - for r in currentRestoreObservers { - guard let pid = PID(rawValue: productIdentifier) else { - continue - } - r(false, pid, tx.error) - } - } - - default: - break - } - } - } - } - - public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { - DispatchQueue.main.async { - for r in self.restoreObservers { - r(true, nil, nil) - } - self.restoreObservers.removeAll() - } - } - - public func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { - DispatchQueue.main.async { - for r in self.restoreObservers { - r(true, nil, error) - } - self.restoreObservers.removeAll() - } - } + public let localizedPrice: String? } -extension SKProduct { - private var localizedCurrencyFormatter: NumberFormatter { - let fmt = NumberFormatter() - fmt.locale = priceLocale - fmt.numberStyle = .currency - return fmt +public protocol InAppProtocol { + associatedtype ProductIdentifier: Hashable + + func canMakePurchases() -> Bool + + func requestProducts(withIdentifiers identifiers: [ProductIdentifier]) async throws -> [ProductIdentifier: InAppProduct] + + func purchase(productWithIdentifier productIdentifier: ProductIdentifier) async throws -> InAppPurchaseResult + + func restorePurchases() async throws + + func products() -> [InAppProduct] + + func product(withIdentifier productIdentifier: ProductIdentifier) -> InAppProduct? + + func setTransactionsObserver(_ block: @escaping () -> Void) +} + +public struct InAppReceipt: Sendable { + public struct PurchaseReceipt: Sendable { + public let productIdentifier: String? + + public let cancellationDate: Date? + + public let originalPurchaseDate: Date? + + public init(productIdentifier: String?, cancellationDate: Date?, originalPurchaseDate: Date?) { + self.productIdentifier = productIdentifier + self.cancellationDate = cancellationDate + self.originalPurchaseDate = originalPurchaseDate + } } - public var localizedPrice: String? { - localizedCurrencyFormatter.string(from: price) + public let originalBuildNumber: Int? + + public let purchaseReceipts: [PurchaseReceipt]? + + public init(originalBuildNumber: Int?, purchaseReceipts: [PurchaseReceipt]?) { + self.originalBuildNumber = originalBuildNumber + self.purchaseReceipts = purchaseReceipts } } diff --git a/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/StoreKitInApp.swift b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/StoreKitInApp.swift new file mode 100644 index 00000000..881be032 --- /dev/null +++ b/PassepartoutLibrary/Sources/PassepartoutCore/Reusable/StoreKitInApp.swift @@ -0,0 +1,297 @@ +// +// StoreKitInApp.swift +// Passepartout +// +// Created by Davide De Rosa on 12/19/23. +// Copyright (c) 2023 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation +import StoreKit + +@MainActor +public final class StoreKitInApp: InAppProtocol where PID: Hashable & RawRepresentable, + PID.RawValue == String { + public typealias ProductIdentifier = PID + + private let impl: StoreKitInAppImpl + + public init() { + impl = StoreKitInAppImpl() + } + + public nonisolated func canMakePurchases() -> Bool { + SKPaymentQueue.canMakePayments() + } + + public func requestProducts(withIdentifiers identifiers: [PID]) async throws -> [PID: InAppProduct] { + try await withCheckedThrowingContinuation { continuation in + impl.requestProducts(withIdentifiers: identifiers) { products in + continuation.resume(returning: products.reduce(into: [:]) { map, pidProduct in + map[pidProduct.key] = pidProduct.value.asInAppProduct + }) + } failureHandler: { error in + continuation.resume(throwing: error) + } + } + } + + public func purchase(productWithIdentifier productIdentifier: PID) async throws -> InAppPurchaseResult { + try await withCheckedThrowingContinuation { continuation in + impl.purchase(productWithIdentifier: productIdentifier) { result in + do { + let value = try result.get() + continuation.resume(returning: value) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + public func restorePurchases() async throws { + try await withCheckedThrowingContinuation { continuation in + impl.restorePurchases { finished, _, _ in + if finished { + continuation.resume() + } + } + } + } + + public nonisolated func products() -> [InAppProduct] { + impl.products.map(\.asInAppProduct) + } + + public nonisolated func product(withIdentifier productIdentifier: PID) -> InAppProduct? { + guard let skProduct = impl.product(withIdentifier: productIdentifier) else { + return nil + } + return skProduct.asInAppProduct + } + + public nonisolated func setTransactionsObserver(_ block: @escaping () -> Void) { + impl.onTransactionsUpdated = block + } +} + +private final class StoreKitInAppImpl: NSObject, + SKProductsRequestDelegate, SKPaymentTransactionObserver + where PID.RawValue == String { + + typealias ProductObserver = ([PID: SKProduct]) -> Void + + typealias TransactionObserver = (Result) -> Void + + typealias RestoreObserver = (Bool, PID?, Error?) -> Void + + typealias FailureObserver = (Error) -> Void + + private var productsMap: [PID: SKProduct] + + var products: [SKProduct] { + Array(productsMap.values) + } + + private var productObservers: [ProductObserver] + + private var productFailureObserver: FailureObserver? + + private var transactionObservers: [String: TransactionObserver] + + private var restoreObservers: [RestoreObserver] + + var onTransactionsUpdated: (() -> Void)? + + override init() { + productsMap = [:] + productObservers = [] + productFailureObserver = nil + transactionObservers = [:] + restoreObservers = [] + super.init() + + SKPaymentQueue.default().add(self) + } + + deinit { + SKPaymentQueue.default().remove(self) + } + + func requestProducts(withIdentifiers identifiers: [PID], completionHandler: ProductObserver?, failureHandler: FailureObserver?) { + let req = SKProductsRequest(productIdentifiers: Set(identifiers.map { $0.rawValue })) + req.delegate = self + if let observer = completionHandler { + productObservers.append(observer) + } + productFailureObserver = failureHandler + req.start() + } + + @discardableResult + func purchase(productWithIdentifier productIdentifier: PID, completionHandler: @escaping TransactionObserver) -> Bool { + guard let product = productsMap[productIdentifier] else { + return false + } + purchase(product: product, completionHandler: completionHandler) + return true + } + + func purchase(product: SKProduct, completionHandler: @escaping TransactionObserver) { + let queue = SKPaymentQueue.default() + let payment = SKPayment(product: product) + transactionObservers[product.productIdentifier] = completionHandler + queue.add(payment) + } + + func restorePurchases(completionHandler: @escaping RestoreObserver) { + let queue = SKPaymentQueue.default() + restoreObservers.append(completionHandler) + queue.restoreCompletedTransactions() + } + + func product(withIdentifier productIdentifier: PID) -> SKProduct? { + productsMap[productIdentifier] + } + + // MARK: SKProductsRequestDelegate + + func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { + DispatchQueue.main.async { + self.receiveProducts(response.products) + } + } + + func request(_ request: SKRequest, didFailWithError error: Error) { + if request as? SKProductsRequest != nil { + DispatchQueue.main.async { + self.productFailureObserver?(error) + } + } + DispatchQueue.main.async { + self.transactionObservers.removeAll() + } + } + + private func receiveProducts(_ products: [SKProduct]) { + productsMap.removeAll() + for p in products { + guard let pid = PID(rawValue: p.productIdentifier) else { + continue + } + productsMap[pid] = p + } + productObservers.forEach { $0(productsMap) } + productObservers.removeAll() + } + + // MARK: SKPaymentTransactionObserver + + func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + + let currentRestoreObservers = self.restoreObservers + + for tx in transactions { + let productIdentifier = tx.payment.productIdentifier + let observer = self.transactionObservers[productIdentifier] + + switch tx.transactionState { + case .purchased: + queue.finishTransaction(tx) + observer?(.success(.done)) + + case .restored: + queue.finishTransaction(tx) + observer?(.success(.done)) + for r in currentRestoreObservers { + guard let pid = PID(rawValue: productIdentifier) else { + continue + } + r(false, pid, nil) + } + + case .failed: + queue.finishTransaction(tx) + if let skError = tx.error as? SKError, skError.code == .paymentCancelled { + observer?(.success(.cancelled)) + } else { + observer?(.failure(tx.error ?? InAppError.unknown)) + for r in currentRestoreObservers { + guard let pid = PID(rawValue: productIdentifier) else { + continue + } + r(false, pid, tx.error) + } + } + + default: + break + } + } + + self.onTransactionsUpdated?() + } + } + + func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { + DispatchQueue.main.async { + for r in self.restoreObservers { + r(true, nil, nil) + } + self.restoreObservers.removeAll() + } + } + + func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) { + DispatchQueue.main.async { + for r in self.restoreObservers { + r(true, nil, error) + } + self.restoreObservers.removeAll() + } + } +} + +extension SKProduct { + public var asInAppProduct: InAppProduct { + InAppProduct(productIdentifier: productIdentifier, + localizedTitle: localizedTitle, + localizedDescription: localizedDescription, + price: price, + localizedPrice: localizedPrice) + } +} + +private extension SKProduct { + var localizedCurrencyFormatter: NumberFormatter { + let fmt = NumberFormatter() + fmt.locale = priceLocale + fmt.numberStyle = .currency + return fmt + } + + var localizedPrice: String? { + localizedCurrencyFormatter.string(from: price) + } +} diff --git a/PassepartoutLibrary/Tests/PassepartoutVPNTests/ProfilesTests.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/AppType.swift similarity index 69% rename from PassepartoutLibrary/Tests/PassepartoutVPNTests/ProfilesTests.swift rename to PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/AppType.swift index 51dda764..452cd716 100644 --- a/PassepartoutLibrary/Tests/PassepartoutVPNTests/ProfilesTests.swift +++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/AppType.swift @@ -1,8 +1,8 @@ // -// ProfilesTests.swift +// AppType.swift // Passepartout // -// Created by Davide De Rosa on 4/7/22. +// Created by Davide De Rosa on 12/19/23. // Copyright (c) 2023 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn @@ -23,13 +23,24 @@ // along with Passepartout. If not, see . // -@testable import PassepartoutVPN -import XCTest +import Foundation -final class ProfilesTests: XCTestCase { - override func setUp() { - } +public enum AppType: Int { + case undefined = -1 - override func tearDown() { + case freemium = 0 + + case beta = 1 + + case fullVersion = 2 + + public var isRestricted: Bool { + switch self { + case .undefined, .beta: + return true + + default: + return false + } } } diff --git a/Passepartout/App/Domain/BuildProducts.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/BuildProducts.swift similarity index 81% rename from Passepartout/App/Domain/BuildProducts.swift rename to PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/BuildProducts.swift index 75871244..259fa458 100644 --- a/Passepartout/App/Domain/BuildProducts.swift +++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/BuildProducts.swift @@ -25,18 +25,18 @@ import Foundation -struct BuildProducts { +public struct BuildProducts { private let productsAtBuild: (Int) -> [LocalProduct] - init(productsAtBuild: @escaping (Int) -> [LocalProduct]) { + public init(productsAtBuild: @escaping (Int) -> [LocalProduct]) { self.productsAtBuild = productsAtBuild } - func products(atBuild build: Int) -> [LocalProduct] { + public func products(atBuild build: Int) -> [LocalProduct] { productsAtBuild(build) } - func hasProduct(_ product: LocalProduct, atBuild build: Int) -> Bool { + public func hasProduct(_ product: LocalProduct, atBuild build: Int) -> Bool { productsAtBuild(build).contains(product) } } diff --git a/Passepartout/App/Domain/LocalProduct.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/LocalProduct.swift similarity index 63% rename from Passepartout/App/Domain/LocalProduct.swift rename to PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/LocalProduct.swift index 751bd2b0..d3173375 100644 --- a/Passepartout/App/Domain/LocalProduct.swift +++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/LocalProduct.swift @@ -24,10 +24,10 @@ // import Foundation -import PassepartoutLibrary -import StoreKit +import PassepartoutCore +import PassepartoutProviders -struct LocalProduct: RawRepresentable, Equatable, Hashable { +public struct LocalProduct: RawRepresentable, Hashable, Sendable { private static let bundleSubdomain = "ios" private static let bundle = "com.algoritmico.\(bundleSubdomain).Passepartout" @@ -40,19 +40,19 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable { // MARK: Donations - static let tinyDonation = LocalProduct(donationDescription: "Tiny") + public static let tinyDonation = LocalProduct(donationDescription: "Tiny") - static let smallDonation = LocalProduct(donationDescription: "Small") + public static let smallDonation = LocalProduct(donationDescription: "Small") - static let mediumDonation = LocalProduct(donationDescription: "Medium") + public static let mediumDonation = LocalProduct(donationDescription: "Medium") - static let bigDonation = LocalProduct(donationDescription: "Big") + public static let bigDonation = LocalProduct(donationDescription: "Big") - static let hugeDonation = LocalProduct(donationDescription: "Huge") + public static let hugeDonation = LocalProduct(donationDescription: "Huge") - static let maxiDonation = LocalProduct(donationDescription: "Maxi") + public static let maxiDonation = LocalProduct(donationDescription: "Maxi") - static let allDonations: [LocalProduct] = [ + public static let allDonations: [LocalProduct] = [ .tinyDonation, .smallDonation, .mediumDonation, @@ -67,19 +67,19 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable { // MARK: Features - static let allProviders = LocalProduct(featureId: "all_providers") + public static let allProviders = LocalProduct(featureId: "all_providers") - static let networkSettings = LocalProduct(featureId: "network_settings") + public static let networkSettings = LocalProduct(featureId: "network_settings") - static let trustedNetworks = LocalProduct(featureId: "trusted_networks") + public static let trustedNetworks = LocalProduct(featureId: "trusted_networks") - static let siriShortcuts = LocalProduct(featureId: "siri") + public static let siriShortcuts = LocalProduct(featureId: "siri") - static let fullVersion_iOS = LocalProduct(featureId: "full_version") + public static let fullVersion_iOS = LocalProduct(featureId: "full_version") - static let fullVersion_macOS = LocalProduct(featureId: "full_mac_version") + public static let fullVersion_macOS = LocalProduct(featureId: "full_mac_version") - static let fullVersion = LocalProduct(featureId: "full_multi_version") + public static let fullVersion = LocalProduct(featureId: "full_multi_version") static let allFeatures: [LocalProduct] = [ .allProviders, @@ -101,35 +101,45 @@ struct LocalProduct: RawRepresentable, Equatable, Hashable { allDonations + allFeatures// + allProviders } - var isDonation: Bool { + public var isDonation: Bool { rawValue.hasPrefix(LocalProduct.donationsBundle) } - var isFeature: Bool { + public var isFeature: Bool { rawValue.hasPrefix(LocalProduct.featuresBundle) } - var isProvider: Bool { + public var isProvider: Bool { rawValue.hasPrefix(LocalProduct.providersBundle) } + public var isPlatformVersion: Bool { + switch self { + case .fullVersion_iOS, .fullVersion_macOS: + return true + + default: + return false + } + } + // MARK: RawRepresentable - let rawValue: String + public let rawValue: String - init?(rawValue: String) { + public init?(rawValue: String) { self.rawValue = rawValue } } extension LocalProduct { - func matchesStoreKitProduct(_ skProduct: SKProduct) -> Bool { - skProduct.productIdentifier == rawValue + public func matchesInAppProduct(_ iaProduct: InAppProduct) -> Bool { + iaProduct.productIdentifier == rawValue } } extension ProviderName { - var product: LocalProduct { + public var product: LocalProduct { .init(rawValue: "\(LocalProduct.providersBundle).\(inApp)")! } } diff --git a/PassepartoutTests/PassepartoutTestsApp.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/ReceiptReader.swift similarity index 79% rename from PassepartoutTests/PassepartoutTestsApp.swift rename to PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/ReceiptReader.swift index fdec4020..e00fccb7 100644 --- a/PassepartoutTests/PassepartoutTestsApp.swift +++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Domain/ReceiptReader.swift @@ -1,8 +1,8 @@ // -// PassepartoutTestsApp.swift +// ReceiptReader.swift // Passepartout // -// Created by Davide De Rosa on 10/30/22. +// Created by Davide De Rosa on 12/19/23. // Copyright (c) 2023 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn @@ -23,13 +23,9 @@ // along with Passepartout. If not, see . // -import SwiftUI +import Foundation +import PassepartoutCore -@main -struct PassepartoutTestsApp: App { - var body: some Scene { - WindowGroup { - EmptyView() - } - } +public protocol ReceiptReader { + func receipt(for appType: AppType) -> InAppReceipt? } diff --git a/Passepartout/App/Managers/ProductManager.swift b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift similarity index 62% rename from Passepartout/App/Managers/ProductManager.swift rename to PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift index 769bd04f..8c9818d3 100644 --- a/Passepartout/App/Managers/ProductManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutFrontend/Managers/ProductManager.swift @@ -25,48 +25,35 @@ import Combine import Foundation -import Kvitto -import PassepartoutLibrary -import StoreKit +import PassepartoutCore +import PassepartoutProviders + +public protocol LocalInApp: InAppProtocol where ProductIdentifier == LocalProduct { +} + +extension StoreKitInApp: LocalInApp where ProductIdentifier == LocalProduct { +} @MainActor -final class ProductManager: NSObject, ObservableObject { - enum AppType: Int { - case undefined = -1 +public final class ProductManager: NSObject, ObservableObject { + private let inApp: any LocalInApp - case freemium = 0 - - case beta = 1 - - case fullVersion = 2 - - var isRestricted: Bool { - switch self { - case .undefined, .beta: - return true - - default: - return false - } - } - } + private let receiptReader: ReceiptReader private let overriddenAppType: AppType? - let buildProducts: BuildProducts + public let buildProducts: BuildProducts - let didRefundProducts = PassthroughSubject() + public let didRefundProducts = PassthroughSubject() - @Published private(set) var appType: AppType + @Published public private(set) var appType: AppType - @Published private(set) var isRefreshingProducts = false + @Published public private(set) var isRefreshingProducts = false - @Published private(set) var products: [SKProduct] + @Published public private(set) var products: [InAppProduct] // - private let inApp: InApp - private var purchasedAppBuild: Int? private var purchasedFeatures: Set @@ -86,15 +73,17 @@ final class ProductManager: NSObject, ObservableObject { } } - private var refreshRequest: SKReceiptRefreshRequest? - - init(overriddenAppType: AppType?, buildProducts: BuildProducts) { + public init(inApp: any LocalInApp, + receiptReader: ReceiptReader, + overriddenAppType: AppType? = nil, + buildProducts: BuildProducts) { self.overriddenAppType = overriddenAppType + self.receiptReader = receiptReader self.buildProducts = buildProducts appType = .undefined products = [] - inApp = InApp() + self.inApp = inApp purchasedAppBuild = nil purchasedFeatures = [] purchaseDates = [:] @@ -102,8 +91,10 @@ final class ProductManager: NSObject, ObservableObject { super.init() + inApp.setTransactionsObserver { [weak self] in + self?.reloadReceipt() + } reloadReceipt() - SKPaymentQueue.default().add(self) refreshProducts() Task { @@ -114,15 +105,11 @@ final class ProductManager: NSObject, ObservableObject { } } - deinit { - SKPaymentQueue.default().remove(self) + public func canMakePayments() -> Bool { + inApp.canMakePurchases() } - func canMakePayments() -> Bool { - SKPaymentQueue.canMakePayments() - } - - func refreshProducts() { + public func refreshProducts() { let ids = LocalProduct.all guard !ids.isEmpty else { return @@ -132,23 +119,26 @@ final class ProductManager: NSObject, ObservableObject { return } isRefreshingProducts = true - inApp.requestProducts(withIdentifiers: ids, completionHandler: { _ in - pp_log.debug("In-app products: \(self.inApp.products.map { $0.productIdentifier })") + Task { + do { + let productsMap = try await inApp.requestProducts(withIdentifiers: ids) + pp_log.debug("In-app products: \(productsMap.keys.map(\.rawValue))") - self.products = self.inApp.products - self.isRefreshingProducts = false - }, failureHandler: { - pp_log.warning("Unable to list products: \($0)") - self.isRefreshingProducts = false - }) + products = Array(productsMap.values) + isRefreshingProducts = false + } catch { + pp_log.warning("Unable to list products: \(error)") + isRefreshingProducts = false + } + } } - func product(withIdentifier identifier: LocalProduct) -> SKProduct? { + public func product(withIdentifier identifier: LocalProduct) -> InAppProduct? { inApp.product(withIdentifier: identifier) } - func featureProducts(including: [LocalProduct]) -> [SKProduct] { - inApp.products.filter { + public func featureProducts(including: [LocalProduct]) -> [InAppProduct] { + inApp.products().filter { guard let p = LocalProduct(rawValue: $0.productIdentifier) else { return false } @@ -162,8 +152,8 @@ final class ProductManager: NSObject, ObservableObject { } } - func featureProducts(excluding: [LocalProduct]) -> [SKProduct] { - inApp.products.filter { + public func featureProducts(excluding: [LocalProduct]) -> [InAppProduct] { + inApp.products().filter { guard let p = LocalProduct(rawValue: $0.productIdentifier) else { return false } @@ -177,35 +167,42 @@ final class ProductManager: NSObject, ObservableObject { } } - func purchase(_ product: SKProduct, completionHandler: @escaping (Result) -> Void) { - inApp.purchase(product: product) { result in - if case .success = result { - self.reloadReceipt() - } - DispatchQueue.main.async { - completionHandler(result) + public func purchase(_ product: InAppProduct, completionHandler: @escaping (Result) -> Void) { + guard let pid = LocalProduct(rawValue: product.productIdentifier) else { + pp_log.warning("Unrecognized product: \(product)") + return + } + Task { + do { + let result = try await inApp.purchase(productWithIdentifier: pid) + reloadReceipt() + completionHandler(.success(result)) + } catch { + completionHandler(.failure(error)) } } } - func restorePurchases(completionHandler: @escaping (Error?) -> Void) { - inApp.restorePurchases { (finished, _, error) in - guard finished else { - return - } - DispatchQueue.main.async { + public func restorePurchases(completionHandler: @escaping (Error?) -> Void) { + Task { + do { + try await inApp.restorePurchases() + completionHandler(nil) + } catch { completionHandler(error) } } } +} - // MARK: In-app eligibility +// MARK: In-app eligibility - private func isCurrentPlatformVersion() -> Bool { +extension ProductManager { + public func isCurrentPlatformVersion() -> Bool { purchasedFeatures.contains(isMac ? .fullVersion_macOS : .fullVersion_iOS) } - private func isFullVersion() -> Bool { + public func isFullVersion() -> Bool { if appType == .fullVersion { return true } @@ -215,42 +212,47 @@ final class ProductManager: NSObject, ObservableObject { return purchasedFeatures.contains(.fullVersion) } - func isEligible(forFeature feature: LocalProduct) -> Bool { + public func isEligible(forFeature feature: LocalProduct) -> Bool { if let purchasedAppBuild = purchasedAppBuild { if feature == .networkSettings && buildProducts.hasProduct(.networkSettings, atBuild: purchasedAppBuild) { return true } } + if feature.isPlatformVersion { + return purchasedFeatures.contains(feature) + } return isFullVersion() || purchasedFeatures.contains(feature) } - func isEligible(forProvider providerName: ProviderName) -> Bool { + public func isEligible(forProvider providerName: ProviderName) -> Bool { guard providerName != .oeck else { return true } return isEligible(forFeature: providerName.product) } - func isEligibleForFeedback() -> Bool { + public func isEligibleForFeedback() -> Bool { appType == .beta || !purchasedFeatures.isEmpty } - func hasPurchased(_ product: LocalProduct) -> Bool { + public func hasPurchased(_ product: LocalProduct) -> Bool { purchasedFeatures.contains(product) } - func purchaseDate(forProduct product: LocalProduct) -> Date? { + public func purchaseDate(forProduct product: LocalProduct) -> Date? { purchaseDates[product] } +} - func reloadReceipt(andNotify: Bool = true) { - guard let receipt else { +extension ProductManager { + public func reloadReceipt(andNotify: Bool = true) { + guard let receipt = receiptReader.receipt(for: appType) else { pp_log.error("Could not parse App Store receipt!") return } - if let originalAppVersion = receipt.originalAppVersion, let buildNumber = Int(originalAppVersion) { - purchasedAppBuild = buildNumber + if let originalBuildNumber = receipt.originalBuildNumber { + purchasedAppBuild = originalBuildNumber } purchasedFeatures.removeAll() var newCancelledPurchases: Set = [] @@ -263,7 +265,7 @@ final class ProductManager: NSObject, ObservableObject { purchasedFeatures.insert($0) } } - if let iapReceipts = receipt.inAppPurchaseReceipts { + if let iapReceipts = receipt.purchaseReceipts { purchaseDates.removeAll() pp_log.debug("In-app receipts:") @@ -291,14 +293,6 @@ final class ProductManager: NSObject, ObservableObject { } } -extension ProductManager: SKPaymentTransactionObserver { - func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { - DispatchQueue.main.async { [weak self] in - self?.reloadReceipt() - } - } -} - private extension ProductManager { var isMac: Bool { #if targetEnvironment(macCatalyst) @@ -308,30 +302,6 @@ private extension ProductManager { #endif } - var receipt: Receipt? { - guard let url = Bundle.main.appStoreReceiptURL else { - pp_log.warning("No App Store receipt found!") - return nil - } - let receipt = Receipt(contentsOfURL: url) - - // in TestFlight, attempt fallback to existing release receipt - if appType == .beta { - guard let receipt else { - let releaseUrl = url.deletingLastPathComponent().appendingPathComponent("receipt") - guard releaseUrl != url else { - assertionFailure("How can release URL be equal to sandbox URL in TestFlight?") - return nil - } - pp_log.warning("Sandbox receipt not found, falling back to Release receipt") - return Receipt(contentsOfURL: releaseUrl) - } - return receipt - } - - return receipt - } - func detectRefunds(_ refunds: Set) { let isEligibleForFullVersion = isFullVersion() let hasCancelledFullVersion: Bool diff --git a/PassepartoutLibrary/Sources/PassepartoutLibrary/Exports.swift b/PassepartoutLibrary/Sources/PassepartoutLibrary/Exports.swift index af3adf00..4dec4d1d 100644 --- a/PassepartoutLibrary/Sources/PassepartoutLibrary/Exports.swift +++ b/PassepartoutLibrary/Sources/PassepartoutLibrary/Exports.swift @@ -1,4 +1,5 @@ @_exported import PassepartoutCore +@_exported import PassepartoutFrontend @_exported import PassepartoutProviders @_exported import PassepartoutProvidersImpl @_exported import PassepartoutVPN diff --git a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/OnDemand+Rules.swift b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/OnDemand+Rules.swift index a1467d32..ab1dd00d 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/OnDemand+Rules.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPNImpl/Extensions/OnDemand+Rules.swift @@ -58,9 +58,12 @@ private extension Profile.OnDemand { // apply exceptions (unless .any) if withCustomRules && policy != .any { + #if os(iOS) if Utils.hasCellularData() && withMobileNetwork { rules.append(cellularRule()) } + #endif + #if os(macOS) if Utils.hasEthernet() && withEthernetNetwork { if let rule = ethernetRule() { rules.append(rule) @@ -68,6 +71,7 @@ private extension Profile.OnDemand { pp_log.warning("Unable to add rule for NEOnDemandRuleInterfaceType.ethernet (not compatible)") } } + #endif let SSIDs = Array(withSSIDs.filter { $1 }.keys) if !SSIDs.isEmpty { rules.append(wifiRule(SSIDs: SSIDs)) @@ -108,16 +112,20 @@ private extension Profile.OnDemand { return rule } + #if os(iOS) func cellularRule() -> NEOnDemandRule { networkRule(matchingInterface: .cellular) } + #endif + #if os(macOS) func ethernetRule() -> NEOnDemandRule? { guard let compatibleEthernet = NEOnDemandRuleInterfaceType.compatibleEthernet else { return nil } return networkRule(matchingInterface: compatibleEthernet) } + #endif func wifiRule(SSIDs: [String]) -> NEOnDemandRule { let rule = networkRule(matchingInterface: .wiFi) diff --git a/PassepartoutLibrary/Tests/PassepartoutFrontendTests/Mock/MockInApp.swift b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/Mock/MockInApp.swift new file mode 100644 index 00000000..55700c7b --- /dev/null +++ b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/Mock/MockInApp.swift @@ -0,0 +1,71 @@ +// +// MockInApp.swift +// Passepartout +// +// Created by Davide De Rosa on 12/19/23. +// Copyright (c) 2023 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation +@testable import PassepartoutCore +@testable import PassepartoutFrontend + +final class MockInApp: LocalInApp { + var allProducts: [LocalProduct: InAppProduct] + + init() { + allProducts = [:] + } + + func canMakePurchases() -> Bool { + true + } + + func requestProducts(withIdentifiers identifiers: [LocalProduct]) async throws -> [LocalProduct: InAppProduct] { + allProducts = LocalProduct.all.reduce(into: [:]) { + $0[$1] = InAppProduct(productIdentifier: $1.rawValue, + localizedTitle: $1.rawValue, + localizedDescription: $1.rawValue, + price: 10.0, + localizedPrice: "10.0") + } + return allProducts + } + + func purchase(productWithIdentifier productIdentifier: LocalProduct) async throws -> InAppPurchaseResult { + .done + } + + func restorePurchases() async throws { + // + } + + func products() -> [InAppProduct] { + Array(allProducts.values) + } + + func product(withIdentifier productIdentifier: LocalProduct) -> InAppProduct? { + allProducts[productIdentifier] + } + + func setTransactionsObserver(_ block: @escaping () -> Void) { + // + } +} diff --git a/PassepartoutLibrary/Tests/PassepartoutVPNTests/VPNTests.swift b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/Mock/MockReceiptReader.swift similarity index 52% rename from PassepartoutLibrary/Tests/PassepartoutVPNTests/VPNTests.swift rename to PassepartoutLibrary/Tests/PassepartoutFrontendTests/Mock/MockReceiptReader.swift index a4b9b801..42571b41 100644 --- a/PassepartoutLibrary/Tests/PassepartoutVPNTests/VPNTests.swift +++ b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/Mock/MockReceiptReader.swift @@ -1,8 +1,8 @@ // -// VPNTests.swift +// MockReceiptReader.swift // Passepartout // -// Created by Davide De Rosa on 10/29/22. +// Created by Davide De Rosa on 12/19/23. // Copyright (c) 2023 Davide De Rosa. All rights reserved. // // https://github.com/passepartoutvpn @@ -23,14 +23,26 @@ // along with Passepartout. If not, see . // -@testable import PassepartoutVPN -import XCTest +import Foundation +import PassepartoutCore +@testable import PassepartoutFrontend -final class VPNTests: XCTestCase { - override func setUp() { +final class MockReceiptReader: ReceiptReader { + var receipt: InAppReceipt? + + init(receipt: InAppReceipt? = nil) { + self.receipt = receipt } - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. + func setReceipt(withBuild build: Int, products: [LocalProduct], cancelledProducts: Set = []) { + receipt = InAppReceipt(originalBuildNumber: build, purchaseReceipts: products.map { + .init(productIdentifier: $0.rawValue, + cancellationDate: cancelledProducts.contains($0) ? Date() : nil, + originalPurchaseDate: nil) + }) } - } + + func receipt(for appType: AppType) -> InAppReceipt? { + receipt + } +} diff --git a/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift new file mode 100644 index 00000000..2c0eab8b --- /dev/null +++ b/PassepartoutLibrary/Tests/PassepartoutFrontendTests/ProductManagerTests.swift @@ -0,0 +1,140 @@ +// +// ProductManagerTests.swift +// Passepartout +// +// Created by Davide De Rosa on 12/19/23. +// Copyright (c) 2023 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +import Foundation +@testable import PassepartoutFrontend +import XCTest + +@MainActor +final class ProductManagerTests: XCTestCase { + private let inApp = MockInApp() + + private let noBuildProducts = BuildProducts { _ in [] } + + func test_givenBuildProducts_whenOlder_thenFullVersion() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 500, products: []) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: BuildProducts { build in + if build <= 1000 { + return [.fullVersion] + } + return [] + }) + XCTAssertTrue(sut.isEligible(forFeature: .fullVersion)) + } + + func test_givenBuildProducts_whenNewer_thenFreeVersion() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 1500, products: []) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: BuildProducts { build in + if build <= 1000 { + return [.fullVersion] + } + return [] + }) + XCTAssertFalse(sut.isEligible(forFeature: .fullVersion)) + } + + func test_givenPurchase_whenReload_thenCredited() { + let reader = MockReceiptReader() + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + XCTAssertFalse(sut.isEligible(forFeature: .fullVersion)) + + reader.setReceipt(withBuild: 1500, products: [.fullVersion]) + XCTAssertFalse(sut.isEligible(forFeature: .fullVersion)) + + sut.reloadReceipt() + XCTAssertTrue(sut.isEligible(forFeature: .fullVersion)) + } + + func test_givenPurchase_whenCancelled_thenRevoke() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 1500, products: [.fullVersion]) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + XCTAssertTrue(sut.isEligible(forFeature: .fullVersion)) + + reader.setReceipt(withBuild: 1500, products: [.fullVersion], cancelledProducts: [.fullVersion]) + XCTAssertTrue(sut.isEligible(forFeature: .fullVersion)) + + sut.reloadReceipt() + XCTAssertFalse(sut.isEligible(forFeature: .fullVersion)) + } + + func test_givenFeature_thenIsOnlyEligibleForFeature() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 1500, products: [.siriShortcuts, .networkSettings]) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + + XCTAssertTrue(sut.isEligible(forFeature: .siriShortcuts)) + XCTAssertTrue(sut.isEligible(forFeature: .networkSettings)) + XCTAssertFalse(sut.isEligible(forFeature: .trustedNetworks)) + XCTAssertFalse(sut.isEligible(forFeature: .fullVersion)) + XCTAssertFalse(sut.isFullVersion()) + } + + func test_givenPlatformVersion_thenIsFullVersionForPlatform() { + let reader = MockReceiptReader() + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + + #if targetEnvironment(macCatalyst) + reader.setReceipt(withBuild: 1500, products: [.fullVersion_macOS, .networkSettings]) + sut.reloadReceipt() + XCTAssertFalse(sut.isEligible(forFeature: .fullVersion_iOS)) + XCTAssertTrue(sut.isEligible(forFeature: .fullVersion_macOS)) + #else + reader.setReceipt(withBuild: 1500, products: [.fullVersion_iOS, .networkSettings]) + sut.reloadReceipt() + XCTAssertTrue(sut.isEligible(forFeature: .fullVersion_iOS)) + XCTAssertFalse(sut.isEligible(forFeature: .fullVersion_macOS)) + #endif + + XCTAssertTrue(sut.isCurrentPlatformVersion()) + XCTAssertTrue(sut.isFullVersion()) + XCTAssertTrue(sut.isEligible(forFeature: .fullVersion)) + } + + func test_givenFullVersion_thenIsEligibleForAnyFeature() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 1500, products: [.fullVersion]) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + + XCTAssertTrue(LocalProduct + .allFeatures + .filter { !$0.isPlatformVersion } + .allSatisfy(sut.isEligible(forFeature:)) + ) + } + + func test_givenFreeVersion_thenIsNotEligibleForAnyFeature() { + let reader = MockReceiptReader() + reader.setReceipt(withBuild: 1500, products: []) + let sut = ProductManager(inApp: inApp, receiptReader: reader, buildProducts: noBuildProducts) + + XCTAssertFalse(LocalProduct + .allFeatures + .allSatisfy(sut.isEligible(forFeature:)) + ) + } +} diff --git a/PassepartoutLibrary/Tests/PassepartoutProvidersTests/ProviderManagerTests.swift b/PassepartoutLibrary/Tests/PassepartoutProvidersTests/ProviderManagerTests.swift new file mode 100644 index 00000000..5fa313b7 --- /dev/null +++ b/PassepartoutLibrary/Tests/PassepartoutProvidersTests/ProviderManagerTests.swift @@ -0,0 +1,173 @@ +// +// ProviderManagerTests.swift +// Passepartout +// +// Created by Davide De Rosa on 3/13/22. +// Copyright (c) 2023 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +//import Combine +//import CoreData +//import PassepartoutCore +//import PassepartoutProviders +//@testable import PassepartoutProvidersImpl +//import XCTest +// +//@MainActor +//final class ProvidersTests: XCTestCase { +// private var persistence: ProvidersPersistence! +// +// private var manager: ProviderManager! +// +// private var cancellables: Set = [] +// +// override func setUp() { +// persistence = ProvidersPersistence(withName: "ProvidersTests", cloudKit: false, author: nil) +// +// let remoteStrategy = APIRemoteProvidersStrategy( +// appBuild: 10000, +// bundleServices: APIWebServices.bundledServices(withVersion: "v5"), +// remoteServices: APIWebServices("v5", URL(string: "https://passepartoutvpn.app/api/")!, timeout: nil), +// webServicesRepository: persistence.webServicesRepository() +// ) +// manager = ProviderManager( +// localProvidersRepository: persistence.localProvidersRepository(), +// remoteProvidersStrategy: remoteStrategy +// ) +//// persistence.truncate() +// } +// +// override func tearDown() { +//// persistence.truncate() +// } +// +// func testFetchLocalIndex() throws { +// let exp = expectation(description: "Local index") +// +// manager.fetchProvidersIndexPublisher(priority: .bundle) +// .sink { +// switch $0 { +// case .finished: +// exp.fulfill() +// +// case .failure(let error): +// pp_log.error("Unable to load remote provider: \(error)") +// exp.fulfill() +// } +// } receiveValue: { +// pp_log.debug("Loaded index") +// }.store(in: &cancellables) +// +// waitForExpectations(timeout: 10.0, handler: nil) +// } +// +// func testFetchRemoteIndex() throws { +// let exp = expectation(description: "Remote index") +// +// manager.fetchProvidersIndexPublisher(priority: .remote) +// .sink { +// switch $0 { +// case .finished: +// exp.fulfill() +// +// case .failure(let error): +// pp_log.error("Unable to load remote provider: \(error)") +// exp.fulfill() +// } +// } receiveValue: { +// pp_log.debug("Loaded index") +// }.store(in: &cancellables) +// +// waitForExpectations(timeout: 10.0, handler: nil) +// } +// +// func testFetchRemoteProvider() async { +// do { +// try await manager.fetchProviderPublisher(withName: .hideme, vpnProtocol: .openVPN, priority: .remote).async() +// pp_log.debug("Loaded provider") +// } catch { +// XCTFail("Unable to load remote provider: \(error)") +// } +// } +// +// func testListProviders() { +// let providers = manager.allProviders() +// providers.forEach { +// pp_log.debug("\($0.name) -> \($0.fullName)") +// } +// } +// +// func testListCategories() async { +// await fetchProvider(.surfshark) +// let categories = manager.categories(.surfshark, vpnProtocol: .openVPN) +// categories.forEach { +// pp_log.debug("Category: \($0.name)") +// $0.locations.forEach { +// pp_log.debug("\t\($0)") +// } +// } +// } +// +// func testListServers() async { +// await fetchProvider(.nordvpn) +// manager.allProviders().filter({ $0.name == .nordvpn }).forEach { +// let location = ProviderLocation( +// providerMetadata: $0, +// vpnProtocol: .openVPN, +// categoryName: "", +// countryCode: "ES", +// servers: nil +// ) +// +// let servers = manager.servers(forLocation: location) +// pp_log.debug("\($0.fullName): Servers [\(location.countryCode)] (\(servers.count)): \(servers)") +// } +// } +// +// func testServerId() async { +// await fetchProvider(.nordvpn) +// guard let server = manager.server(.nordvpn, vpnProtocol: .openVPN, apiId: "es143") else { +// return +// } +// pp_log.debug(server) +// } +// +// func testDefaultServer() async { +// await fetchProvider(.protonvpn) +// guard let server = manager.anyDefaultServer(.protonvpn, vpnProtocol: .openVPN) else { +// return +// } +// pp_log.debug(server) +// } +// +// func testServerUniqueId() async { +// await fetchProvider(.nordvpn) +// guard let server = manager.server(withId: "BEA03D24A5854DD17395057DEFBE7D6BEEA981227ACF8949E487443E6B5EF9C7") else { +// return +// } +// pp_log.debug(server) +// XCTAssertEqual(server.apiId, "es143") +// } +// +// private func fetchProvider(_ name: ProviderName) async { +// try? await manager.fetchProvidersIndexPublisher(priority: .bundle).async() +// try? await manager.fetchProviderPublisher(withName: name, vpnProtocol: .openVPN, priority: .bundle).async() +// } +//} diff --git a/PassepartoutLibrary/Tests/PassepartoutProvidersTests/ProvidersTests.swift b/PassepartoutLibrary/Tests/PassepartoutProvidersTests/ProvidersTests.swift deleted file mode 100644 index 35f47b85..00000000 --- a/PassepartoutLibrary/Tests/PassepartoutProvidersTests/ProvidersTests.swift +++ /dev/null @@ -1,173 +0,0 @@ -// -// ProvidersTests.swift -// Passepartout -// -// Created by Davide De Rosa on 3/13/22. -// Copyright (c) 2023 Davide De Rosa. All rights reserved. -// -// https://github.com/passepartoutvpn -// -// This file is part of Passepartout. -// -// Passepartout is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Passepartout is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Passepartout. If not, see . -// - -import Combine -import CoreData -import PassepartoutCore -import PassepartoutProviders -@testable import PassepartoutProvidersImpl -import XCTest - -@MainActor -final class ProvidersTests: XCTestCase { - private var persistence: ProvidersPersistence! - - private var manager: ProviderManager! - - private var cancellables: Set = [] - - override func setUp() { - persistence = ProvidersPersistence(withName: "ProvidersTests", cloudKit: false, author: nil) - - let remoteStrategy = APIRemoteProvidersStrategy( - appBuild: 10000, - bundleServices: APIWebServices.bundledServices(withVersion: "v5"), - remoteServices: APIWebServices("v5", URL(string: "https://passepartoutvpn.app/api/")!, timeout: nil), - webServicesRepository: persistence.webServicesRepository() - ) - manager = ProviderManager( - localProvidersRepository: persistence.localProvidersRepository(), - remoteProvidersStrategy: remoteStrategy - ) -// persistence.truncate() - } - - override func tearDown() { -// persistence.truncate() - } - - func testFetchLocalIndex() throws { - let exp = expectation(description: "Local index") - - manager.fetchProvidersIndexPublisher(priority: .bundle) - .sink { - switch $0 { - case .finished: - exp.fulfill() - - case .failure(let error): - pp_log.error("Unable to load remote provider: \(error)") - exp.fulfill() - } - } receiveValue: { - pp_log.debug("Loaded index") - }.store(in: &cancellables) - - waitForExpectations(timeout: 10.0, handler: nil) - } - - func testFetchRemoteIndex() throws { - let exp = expectation(description: "Remote index") - - manager.fetchProvidersIndexPublisher(priority: .remote) - .sink { - switch $0 { - case .finished: - exp.fulfill() - - case .failure(let error): - pp_log.error("Unable to load remote provider: \(error)") - exp.fulfill() - } - } receiveValue: { - pp_log.debug("Loaded index") - }.store(in: &cancellables) - - waitForExpectations(timeout: 10.0, handler: nil) - } - - func testFetchRemoteProvider() async { - do { - try await manager.fetchProviderPublisher(withName: .hideme, vpnProtocol: .openVPN, priority: .remote).async() - pp_log.debug("Loaded provider") - } catch { - XCTFail("Unable to load remote provider: \(error)") - } - } - - func testListProviders() { - let providers = manager.allProviders() - providers.forEach { - pp_log.debug("\($0.name) -> \($0.fullName)") - } - } - - func testListCategories() async { - await fetchProvider(.surfshark) - let categories = manager.categories(.surfshark, vpnProtocol: .openVPN) - categories.forEach { - pp_log.debug("Category: \($0.name)") - $0.locations.forEach { - pp_log.debug("\t\($0)") - } - } - } - - func testListServers() async { - await fetchProvider(.nordvpn) - manager.allProviders().filter({ $0.name == .nordvpn }).forEach { - let location = ProviderLocation( - providerMetadata: $0, - vpnProtocol: .openVPN, - categoryName: "", - countryCode: "ES", - servers: nil - ) - - let servers = manager.servers(forLocation: location) - pp_log.debug("\($0.fullName): Servers [\(location.countryCode)] (\(servers.count)): \(servers)") - } - } - - func testServerId() async { - await fetchProvider(.nordvpn) - guard let server = manager.server(.nordvpn, vpnProtocol: .openVPN, apiId: "es143") else { - return - } - pp_log.debug(server) - } - - func testDefaultServer() async { - await fetchProvider(.protonvpn) - guard let server = manager.anyDefaultServer(.protonvpn, vpnProtocol: .openVPN) else { - return - } - pp_log.debug(server) - } - - func testServerUniqueId() async { - await fetchProvider(.nordvpn) - guard let server = manager.server(withId: "BEA03D24A5854DD17395057DEFBE7D6BEEA981227ACF8949E487443E6B5EF9C7") else { - return - } - pp_log.debug(server) - XCTAssertEqual(server.apiId, "es143") - } - - private func fetchProvider(_ name: ProviderName) async { - try? await manager.fetchProvidersIndexPublisher(priority: .bundle).async() - try? await manager.fetchProviderPublisher(withName: name, vpnProtocol: .openVPN, priority: .bundle).async() - } -} diff --git a/PassepartoutLibrary/Tests/PassepartoutProvidersTests/ServicesTests.swift b/PassepartoutLibrary/Tests/PassepartoutProvidersTests/ServicesTests.swift deleted file mode 100644 index 6ae08dc6..00000000 --- a/PassepartoutLibrary/Tests/PassepartoutProvidersTests/ServicesTests.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// ServicesTests.swift -// Passepartout -// -// Created by Davide De Rosa on 6/11/18. -// Copyright (c) 2023 Davide De Rosa. All rights reserved. -// -// https://github.com/passepartoutvpn -// -// This file is part of Passepartout. -// -// Passepartout is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Passepartout is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Passepartout. If not, see . -// - -import Combine -import PassepartoutCore -@testable import PassepartoutProvidersImpl -import XCTest - -final class ServicesTests: XCTestCase { - let wsLocal = APIWebServices.bundledServices(withVersion: "v5") - - let wsRemote = APIWebServices("v5", URL(string: "https://passepartoutvpn.app/api/")!, timeout: nil) - - private var cancellables: Set = [] - - override func setUp() { - } - - override func tearDown() { - } - - func testLastModified() { - let fmt = DateFormatter() - fmt.timeZone = TimeZone(abbreviation: "GMT") - fmt.dateFormat = "EEE, dd LLL yyyy HH:mm:ss zzz" - - let lmString = "Wed, 23 Oct 2019 17:06:54 GMT" - - fmt.locale = Locale(identifier: "en") - XCTAssertNotNil(fmt.date(from: lmString)) - fmt.locale = Locale(identifier: "fr-FR") - XCTAssertNil(fmt.date(from: lmString)) - } - - func testLocalIndex() { - let exp = expectation(description: "") - wsLocal.providersIndex() - .sink { - switch $0 { - case .finished: - break - - case .failure(let error): - pp_log.debug(error) - exp.fulfill() - } - } receiveValue: { - pp_log.debug($0) - exp.fulfill() - }.store(in: &cancellables) - - waitForExpectations(timeout: 10.0, handler: nil) - } - - func testRemoteIndex() { - let exp = expectation(description: "") - wsRemote.providersIndex() - .sink { - switch $0 { - case .finished: - break - - case .failure(let error): - pp_log.debug(error) - exp.fulfill() - } - } receiveValue: { - pp_log.debug($0) - exp.fulfill() - }.store(in: &cancellables) - - waitForExpectations(timeout: 10.0, handler: nil) - } -} diff --git a/PassepartoutLibrary/Tests/PassepartoutServicesTests/WebServicesTests.swift b/PassepartoutLibrary/Tests/PassepartoutServicesTests/WebServicesTests.swift new file mode 100644 index 00000000..780f7eb2 --- /dev/null +++ b/PassepartoutLibrary/Tests/PassepartoutServicesTests/WebServicesTests.swift @@ -0,0 +1,96 @@ +// +// WebServicesTests.swift +// Passepartout +// +// Created by Davide De Rosa on 6/11/18. +// Copyright (c) 2023 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Passepartout is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Passepartout. If not, see . +// + +//import Combine +//import PassepartoutCore +//@testable import PassepartoutProvidersImpl +//import XCTest +// +//final class ServicesTests: XCTestCase { +// let wsLocal = APIWebServices.bundledServices(withVersion: "v5") +// +// let wsRemote = APIWebServices("v5", URL(string: "https://passepartoutvpn.app/api/")!, timeout: nil) +// +// private var cancellables: Set = [] +// +// override func setUp() { +// } +// +// override func tearDown() { +// } +// +// func testLastModified() { +// let fmt = DateFormatter() +// fmt.timeZone = TimeZone(abbreviation: "GMT") +// fmt.dateFormat = "EEE, dd LLL yyyy HH:mm:ss zzz" +// +// let lmString = "Wed, 23 Oct 2019 17:06:54 GMT" +// +// fmt.locale = Locale(identifier: "en") +// XCTAssertNotNil(fmt.date(from: lmString)) +// fmt.locale = Locale(identifier: "fr-FR") +// XCTAssertNil(fmt.date(from: lmString)) +// } +// +// func testLocalIndex() { +// let exp = expectation(description: "") +// wsLocal.providersIndex() +// .sink { +// switch $0 { +// case .finished: +// break +// +// case .failure(let error): +// pp_log.debug(error) +// exp.fulfill() +// } +// } receiveValue: { +// pp_log.debug($0) +// exp.fulfill() +// }.store(in: &cancellables) +// +// waitForExpectations(timeout: 10.0, handler: nil) +// } +// +// func testRemoteIndex() { +// let exp = expectation(description: "") +// wsRemote.providersIndex() +// .sink { +// switch $0 { +// case .finished: +// break +// +// case .failure(let error): +// pp_log.debug(error) +// exp.fulfill() +// } +// } receiveValue: { +// pp_log.debug($0) +// exp.fulfill() +// }.store(in: &cancellables) +// +// waitForExpectations(timeout: 10.0, handler: nil) +// } +//} diff --git a/fastlane/Scanfile b/fastlane/Scanfile deleted file mode 100644 index cb22a493..00000000 --- a/fastlane/Scanfile +++ /dev/null @@ -1,2 +0,0 @@ -scheme "PassepartoutTests" -device "iPhone 14"