diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6caadc88f1..f4c1f200d8 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1568,8 +1568,6 @@ 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; 7BAF9E4D2A8A3CCB002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */; }; 7BB108592A43375D000AB95F /* PFMoveApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BB108582A43375D000AB95F /* PFMoveApplication.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; - 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; - 7BBA7CE72BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; 7BBD45B12A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBD45B22A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */; }; 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */; }; @@ -1763,10 +1761,6 @@ 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */; }; 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; 9F6434622BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; - 9F6434682BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */; }; - 9F6434692BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */; }; - 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */; }; - 9F64346C2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */; }; 9F6434702BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */; }; 9F6434712BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */; }; 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */; }; @@ -2589,6 +2583,8 @@ F116A7C32BD1924B00F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */; }; F116A7C72BD1925500F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */; }; F116A7C92BD1929000F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C82BD1929000F3FCF7 /* PixelKitTestingUtilities */; }; + F118EA7D2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; + F118EA7E2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */; }; F118EA852BEACC7000F77634 /* NonStandardPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA842BEACC7000F77634 /* NonStandardPixel.swift */; }; F118EA862BEACC7000F77634 /* NonStandardPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA842BEACC7000F77634 /* NonStandardPixel.swift */; }; F188267C2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */; }; @@ -2615,14 +2611,62 @@ F1B33DF32BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */; }; F1B33DF62BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; F1B33DF72BAD970E001128B3 /* SubscriptionErrorReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */; }; + F1D0428E2BFB9F9C00A31506 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1D0428D2BFB9F9C00A31506 /* Subscription */; }; + F1D042902BFB9FA300A31506 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1D0428F2BFB9FA300A31506 /* Subscription */; }; + F1D042912BFB9FD700A31506 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1D042922BFB9FD800A31506 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1D042942BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; + F1D042952BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; + F1D042992BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429A2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429B2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429C2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429D2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429E2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D0429F2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D042A02BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */; }; + F1D042A12BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; + F1D042A22BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; F1D43AEE2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */; }; F1D43AEF2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */; }; F1D43AF32B98E47800BAB743 /* BareBonesBrowserKit in Frameworks */ = {isa = PBXBuildFile; productRef = F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */; }; F1D43AF52B98E48900BAB743 /* BareBonesBrowserKit in Frameworks */ = {isa = PBXBuildFile; productRef = F1D43AF42B98E48900BAB743 /* BareBonesBrowserKit */; }; + F1DA51862BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA51872BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA51882BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA51892BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA518A2BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA518B2BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */; }; + F1DA518C2BF607D200CF29FA /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */; }; + F1DA518D2BF607D200CF29FA /* SubscriptionRedirectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */; }; + F1DA51922BF6081C00CF29FA /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + F1DA51932BF6081D00CF29FA /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + F1DA51942BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + F1DA51952BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */; }; + F1DA51962BF6083700CF29FA /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; + F1DA51972BF6083A00CF29FA /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; + F1DA51982BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; + F1DA51992BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; + F1DA51A32BF6114200CF29FA /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A22BF6114200CF29FA /* Subscription */; }; + F1DA51A52BF6114200CF29FA /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A42BF6114200CF29FA /* SubscriptionTestingUtilities */; }; + F1DA51A72BF6114B00CF29FA /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A62BF6114B00CF29FA /* Subscription */; }; + F1DA51A92BF6114C00CF29FA /* SubscriptionTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F1DA51A82BF6114C00CF29FA /* SubscriptionTestingUtilities */; }; F1DF95E32BD1807C0045E591 /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 08D4923DC968236E22E373E2 /* Crashes */; }; F1DF95E42BD1807C0045E591 /* Crashes in Frameworks */ = {isa = PBXBuildFile; productRef = 537FC71EA5115A983FAF3170 /* Crashes */; }; F1DF95E52BD1807C0045E591 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = DC3F73D49B2D44464AFEFCD8 /* Subscription */; }; F1DF95E72BD188B60045E591 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = F1DF95E62BD188B60045E591 /* LoginItems */; }; + F1FDC9292BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC92A2BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC92B2BF4DFEC006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC92C2BF4DFED006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC92D2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC92E2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */; }; + F1FDC9382BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; + F1FDC9392BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; + F1FDC93A2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; + F1FDC93B2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; + F1FDC93C2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; + F1FDC93D2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */; }; F41D174125CB131900472416 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; }; F44C130225C2DA0400426E3E /* NSAppearanceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44C130125C2DA0400426E3E /* NSAppearanceExtension.swift */; }; F4A6198C283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A6198B283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift */; }; @@ -3372,7 +3416,6 @@ 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionIPCTunnelController.swift; sourceTree = ""; }; 7BB108572A43375D000AB95F /* PFMoveApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFMoveApplication.h; sourceTree = ""; }; 7BB108582A43375D000AB95F /* PFMoveApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFMoveApplication.m; sourceTree = ""; }; - 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = ""; }; 7BBD45B02A691AB500C83CA9 /* NetworkProtectionDebugUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionDebugUtilities.swift; sourceTree = ""; }; 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkExtensionController.swift; sourceTree = ""; }; 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeychainType+ClientDefault.swift"; sourceTree = ""; }; @@ -3519,8 +3562,6 @@ 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModel.swift; sourceTree = ""; }; 9F56CFB02B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewFactory.swift; sourceTree = ""; }; 9F6434602BEC82B700D2D8A0 /* AttributionPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionPixelHandler.swift; sourceTree = ""; }; - 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManager.swift; sourceTree = ""; }; - 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandler.swift; sourceTree = ""; }; 9F64346F2BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManagerTests.swift; sourceTree = ""; }; 9F872D972B8DA9F800138637 /* Bookmarks+Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+Tab.swift"; sourceTree = ""; }; 9F872D9C2B9058D000138637 /* Bookmarks+TabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmarks+TabTests.swift"; sourceTree = ""; }; @@ -4090,13 +4131,20 @@ EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtection+VPNAgentConvenienceInitializers.swift"; sourceTree = ""; }; EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacPacketTunnelProvider.swift; sourceTree = ""; }; EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAlertViewModelTests.swift; sourceTree = ""; }; + F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = ""; }; F118EA842BEACC7000F77634 /* NonStandardPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonStandardPixel.swift; sourceTree = ""; }; F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPixel.swift; sourceTree = ""; }; F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyProPixel.swift; sourceTree = ""; }; F18826832BBEE31700D9AC4F /* PixelKit+Assertion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PixelKit+Assertion.swift"; sourceTree = ""; }; F1B33DF12BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionAppStoreRestorer.swift; sourceTree = ""; }; F1B33DF52BAD970E001128B3 /* SubscriptionErrorReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionErrorReporter.swift; sourceTree = ""; }; + F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataBrokerProtectionSettings+Environment.swift"; sourceTree = ""; }; + F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SubscriptionManager+StandardConfiguration.swift"; sourceTree = ""; }; F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MainMenuActions+VanillaBrowser.swift"; sourceTree = ""; }; + F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionAttributionPixelHandler.swift; sourceTree = ""; }; + F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionRedirectManager.swift; sourceTree = ""; }; + F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SubscriptionEnvironment+Default.swift"; sourceTree = ""; }; + F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VPNSettings+Environment.swift"; sourceTree = ""; }; F41D174025CB131900472416 /* NSColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSColorExtension.swift; sourceTree = ""; }; F44C130125C2DA0400426E3E /* NSAppearanceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAppearanceExtension.swift; sourceTree = ""; }; F4A6198B283CFFBB007F2080 /* ContentScopeFeatureFlagging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentScopeFeatureFlagging.swift; sourceTree = ""; }; @@ -4150,6 +4198,8 @@ 3706FE88293F661700E42796 /* OHHTTPStubs in Frameworks */, F116A7C72BD1925500F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CF2B316E0200A595BB /* SnapshotTesting in Frameworks */, + F1DA51A52BF6114200CF29FA /* SubscriptionTestingUtilities in Frameworks */, + F1DA51A32BF6114200CF29FA /* Subscription in Frameworks */, 3706FE89293F661700E42796 /* OHHTTPStubsSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4284,6 +4334,7 @@ buildActionMask = 2147483647; files = ( 9DEF97E12B06C4EE00764F03 /* Networking in Frameworks */, + F1D0428E2BFB9F9C00A31506 /* Subscription in Frameworks */, 9D9AE8F92AAA3AD00026E7DC /* DataBrokerProtection in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4293,6 +4344,7 @@ buildActionMask = 2147483647; files = ( 315A023F2B6421AE00BFA577 /* Networking in Frameworks */, + F1D042902BFB9FA300A31506 /* Subscription in Frameworks */, 9D9AE8FB2AAA3AD90026E7DC /* DataBrokerProtection in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4344,6 +4396,8 @@ B6DA44172616C13800DD1EC2 /* OHHTTPStubs in Frameworks */, F116A7C32BD1924B00F3FCF7 /* PixelKitTestingUtilities in Frameworks */, B65CD8CB2B316DF100A595BB /* SnapshotTesting in Frameworks */, + F1DA51A92BF6114C00CF29FA /* SubscriptionTestingUtilities in Frameworks */, + F1DA51A72BF6114B00CF29FA /* Subscription in Frameworks */, B6DA44192616C13800DD1EC2 /* OHHTTPStubsSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4541,16 +4595,6 @@ path = Services; sourceTree = ""; }; - 1EA7B8D62B7E124E000330A4 /* Subscription */ = { - isa = PBXGroup; - children = ( - 7BBA7CE52BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */, - 9F6434672BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift */, - 9F64346A2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift */, - ); - path = Subscription; - sourceTree = ""; - }; 3169132B2BD2C7960051B46D /* ErrorView */ = { isa = PBXGroup; children = ( @@ -6634,7 +6678,7 @@ 4B677422255DBEB800025BD8 /* SmarterEncryption */, B68458AE25C7E75100DC17B6 /* StateRestoration */, B6A9E44E26142AF90067D1B9 /* Statistics */, - 1EA7B8D62B7E124E000330A4 /* Subscription */, + F118EA7B2BEA2B8700F77634 /* Subscription */, AACB8E7224A4C8BC005F2218 /* Suggestions */, 3775913429AB99DA00E26367 /* Sync */, AA86491B24D837DE001BABEE /* Tab */, @@ -8245,6 +8289,20 @@ path = JSAlert; sourceTree = ""; }; + F118EA7B2BEA2B8700F77634 /* Subscription */ = { + isa = PBXGroup; + children = ( + F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */, + F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */, + F1DA51842BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift */, + F1FDC9282BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift */, + F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */, + F1DA51852BF607D200CF29FA /* SubscriptionRedirectManager.swift */, + F1FDC9372BF51F41006B1435 /* VPNSettings+Environment.swift */, + ); + path = Subscription; + sourceTree = ""; + }; F1B33DF92BAD9C83001128B3 /* Subscription */ = { isa = PBXGroup; children = ( @@ -8335,6 +8393,8 @@ 3706FDD8293F661700E42796 /* OHHTTPStubsSwift */, B65CD8CE2B316E0200A595BB /* SnapshotTesting */, F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */, + F1DA51A22BF6114200CF29FA /* Subscription */, + F1DA51A42BF6114200CF29FA /* SubscriptionTestingUtilities */, ); productName = DuckDuckGoTests; productReference = 3706FE99293F661700E42796 /* Unit Tests App Store.xctest */; @@ -8624,6 +8684,7 @@ packageProductDependencies = ( 9D9AE8F82AAA3AD00026E7DC /* DataBrokerProtection */, 9DEF97E02B06C4EE00764F03 /* Networking */, + F1D0428D2BFB9F9C00A31506 /* Subscription */, ); productName = DuckDuckGoAgent; productReference = 9D9AE8D12AAA39A70026E7DC /* DuckDuckGo Personal Information Removal.app */; @@ -8646,6 +8707,7 @@ packageProductDependencies = ( 9D9AE8FA2AAA3AD90026E7DC /* DataBrokerProtection */, 315A023E2B6421AE00BFA577 /* Networking */, + F1D0428F2BFB9FA300A31506 /* Subscription */, ); productName = DuckDuckGoAgent; productReference = 9D9AE8F22AAA39D30026E7DC /* DuckDuckGo Personal Information Removal App Store.app */; @@ -8730,6 +8792,8 @@ B6DA44182616C13800DD1EC2 /* OHHTTPStubsSwift */, B65CD8CA2B316DF100A595BB /* SnapshotTesting */, F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */, + F1DA51A62BF6114B00CF29FA /* Subscription */, + F1DA51A82BF6114C00CF29FA /* SubscriptionTestingUtilities */, ); productName = DuckDuckGoTests; productReference = AA585D90248FD31400E9A3E2 /* Unit Tests.xctest */; @@ -9509,6 +9573,7 @@ buildActionMask = 2147483647; files = ( 4B41EDA82B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, + F1DA518D2BF607D200CF29FA /* SubscriptionRedirectManager.swift in Sources */, 3706FA7B293F65D500E42796 /* FaviconUserScript.swift in Sources */, 3706FA7E293F65D500E42796 /* LottieAnimationCache.swift in Sources */, 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */, @@ -9555,6 +9620,7 @@ 3706FAA2293F65D500E42796 /* MainWindow.swift in Sources */, 3707C727294B5D2900682A9F /* WKWebView+SessionState.swift in Sources */, 4B44FEF42B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */, + F1D042A22BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, 3706FAA3293F65D500E42796 /* CrashReportPromptViewController.swift in Sources */, 3706FAA4293F65D500E42796 /* ContextMenuManager.swift in Sources */, 31AA6B982B960BA50025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */, @@ -9582,7 +9648,6 @@ 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, - 9F6434692BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */, 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 3706FABD293F65D500E42796 /* NSNotificationName+PasswordManager.swift in Sources */, 3706FABE293F65D500E42796 /* RulesCompilationMonitor.swift in Sources */, @@ -9714,6 +9779,7 @@ 85774B042A71CDD000DE0561 /* BlockMenuItem.swift in Sources */, 3706FB19293F65D500E42796 /* FireViewController.swift in Sources */, B6E3E55C2BC0041A00A41922 /* DownloadListStoreMock.swift in Sources */, + F1D0429A2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */, 3707C71F294B5D2900682A9F /* WKUserContentControllerExtension.swift in Sources */, 3706FB1A293F65D500E42796 /* OutlineSeparatorViewCell.swift in Sources */, @@ -9761,6 +9827,7 @@ 3706FB3D293F65D500E42796 /* FocusRingView.swift in Sources */, 3706FB3E293F65D500E42796 /* BookmarksBarViewModel.swift in Sources */, 3706FB3F293F65D500E42796 /* NSPopUpButtonView.swift in Sources */, + F1FDC9392BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */, 3706FB40293F65D500E42796 /* ContextualMenu.swift in Sources */, 3706FB41293F65D500E42796 /* NavigationBarViewController.swift in Sources */, @@ -9769,6 +9836,7 @@ 3706FB43293F65D500E42796 /* DuckPlayer.swift in Sources */, 3706FB44293F65D500E42796 /* Favicon.swift in Sources */, 3706FB45293F65D500E42796 /* SuggestionContainerViewModel.swift in Sources */, + F1FDC92A2BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 3706FB46293F65D500E42796 /* FirePopoverWrapperViewController.swift in Sources */, 3706FB47293F65D500E42796 /* NSPasteboardItemExtension.swift in Sources */, 3706FB48293F65D500E42796 /* AutofillPreferencesModel.swift in Sources */, @@ -9803,6 +9871,7 @@ 3706FB5C293F65D500E42796 /* AVCaptureDevice+SwizzledAuthState.swift in Sources */, 3706FB5D293F65D500E42796 /* VisitMenuItem.swift in Sources */, 3706FB5E293F65D500E42796 /* EncryptionKeyStore.swift in Sources */, + F1DA51872BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, 3706FB60293F65D500E42796 /* PasswordManagementIdentityItemView.swift in Sources */, 3706FB61293F65D500E42796 /* ProgressExtension.swift in Sources */, 3706FB62293F65D500E42796 /* CSVParser.swift in Sources */, @@ -10019,7 +10088,6 @@ B68D21D02ACBC9FD002DA3C2 /* ContentBlockerRulesManagerMock.swift in Sources */, 3706FEC6293F6F0600E42796 /* BWKeyStorage.swift in Sources */, 4B6785482AA8DE69008A5004 /* VPNUninstaller.swift in Sources */, - 9F64346C2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, 3706FBEC293F65D500E42796 /* EditableTextView.swift in Sources */, 3706FBED293F65D500E42796 /* TabCollection.swift in Sources */, B6C0BB6B29AF1C7000AE8E3C /* BrowserTabView.swift in Sources */, @@ -10093,7 +10161,6 @@ 3706FC19293F65D500E42796 /* NSNotificationName+Favicons.swift in Sources */, 3706FC1A293F65D500E42796 /* PinningManager.swift in Sources */, 4B37EE782B4CFF3900A89A61 /* DataBrokerProtectionRemoteMessage.swift in Sources */, - 7BBA7CE72BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, 3706FC1B293F65D500E42796 /* TabCollectionViewModel+NSSecureCoding.swift in Sources */, 3706FC1D293F65D500E42796 /* EmailManagerRequestDelegate.swift in Sources */, 3706FC1E293F65D500E42796 /* ApplicationVersionReader.swift in Sources */, @@ -10102,6 +10169,7 @@ 1DDC85042B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, 3706FC20293F65D500E42796 /* PreferencesAutofillView.swift in Sources */, 3706FC21293F65D500E42796 /* UserText+PasswordManager.swift in Sources */, + F118EA7E2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, 3706FC22293F65D500E42796 /* LoadingProgressView.swift in Sources */, 3706FC23293F65D500E42796 /* StatisticsStore.swift in Sources */, 1DDD3EBD2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */, @@ -10666,11 +10734,15 @@ B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, EEBCA0C72BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */, + F1DA51932BF6081D00CF29FA /* AttributionPixelHandler.swift in Sources */, + F1FDC92C2BF4DFED006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, + F1DA51892BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8192A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, 4B2D06322A11C1D300DE1F49 /* NSApplicationExtension.swift in Sources */, + F1FDC93B2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 4B2D06332A11C1E300DE1F49 /* OptionalExtension.swift in Sources */, 4BF0E50B2AD2552200FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B41EDA12B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, @@ -10679,6 +10751,8 @@ 4B2537722A11BF8B00610219 /* main.swift in Sources */, EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */, + F1D0429C2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, + F1DA51972BF6083A00CF29FA /* PrivacyProPixel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10692,9 +10766,11 @@ 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, 7BA7CC592AD1203B0042E5CE /* UserText+NetworkProtection.swift in Sources */, EEDE50112BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, + F1DA51942BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */, 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, 7B2DDCFA2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */, 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, + F1D0429D2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */, 7B8DB31A2B504D7500EC16DA /* VPNAppEventsHandler.swift in Sources */, 7BA7CC532AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, @@ -10705,6 +10781,8 @@ 7B4D8A232BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, + F1DA51982BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */, + F1FDC92D2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */, BDA764842BC49E3F00D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, @@ -10712,8 +10790,10 @@ B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, BDA7647F2BC4998900D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 4BF0E5072AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, + F1DA518A2BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, 7BA7CC442AD11E490042E5CE /* UserText.swift in Sources */, 4BF0E5142AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, + F1FDC93C2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 7BA7CC422AD11E420042E5CE /* NetworkProtectionBouncer.swift in Sources */, B65DA5F12A77D2BC00CBEE8D /* BundleExtension.swift in Sources */, ); @@ -10729,9 +10809,11 @@ 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, + F1DA51952BF6081E00CF29FA /* AttributionPixelHandler.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */, + F1D0429E2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, 7BA7CC412AD11E420042E5CE /* NetworkProtectionBouncer.swift in Sources */, @@ -10742,6 +10824,8 @@ 7B4D8A242BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 4BF0E5152AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, + F1DA51992BF6083B00CF29FA /* PrivacyProPixel.swift in Sources */, + F1FDC92E2BF4E001006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, B65DA5F02A77CC3C00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, BDA764852BC49E4000D0400C /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, @@ -10749,8 +10833,10 @@ 7BA7CC392AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */, BDA764802BC4998A00D0400C /* DefaultVPNLocationFormatter.swift in Sources */, 7BA7CC552AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, + F1DA518B2BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, 7BA7CC3D2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 4BA7C4DA2B3F639800AFE511 /* NetworkProtectionTunnelController.swift in Sources */, + F1FDC93D2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 7BA7CC432AD11E480042E5CE /* UserText.swift in Sources */, 7BA7CC542AD11FCE0042E5CE /* Bundle+VPN.swift in Sources */, ); @@ -10785,17 +10871,23 @@ 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, + F1DA51962BF6083700CF29FA /* PrivacyProPixel.swift in Sources */, EEBCA0C62BD7CE2C004DF19C /* VPNFailureRecoveryPixel.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, + F1D0429B2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, B65DA5F52A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, 4B4D60892A0B2A1C00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */, 4B4D60AD2A0C807300BCD287 /* NSApplicationExtension.swift in Sources */, + F1DA51922BF6081C00CF29FA /* AttributionPixelHandler.swift in Sources */, 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */, 4BF0E50C2AD2552300FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */, B65DA5F22A77D3C600CBEE8D /* UserDefaultsWrapper.swift in Sources */, + F1FDC92B2BF4DFEC006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, + F1DA51882BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, + F1FDC93A2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10846,9 +10938,15 @@ buildActionMask = 2147483647; files = ( 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, + F1D042942BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, + 9D9AE92C2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, + F1D042912BFB9FD700A31506 /* SubscriptionEnvironment+Default.swift in Sources */, + 7B0099792B65013800FE7C31 /* BrowserWindowManager.swift in Sources */, + 9D9AE9292AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, 31ECDA132BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, + F1D0429F2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 31ECDA0E2BED317300AE679F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -10858,9 +10956,15 @@ buildActionMask = 2147483647; files = ( 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, + F1D042952BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, + 7B1459542B7D437200047F2C /* BrowserWindowManager.swift in Sources */, + F1D042922BFB9FD800A31506 /* SubscriptionEnvironment+Default.swift in Sources */, + 9D9AE92D2AAB84FF0026E7DC /* DBPMocks.swift in Sources */, + 9D9AE92A2AAA43EB0026E7DC /* DataBrokerProtectionBackgroundManager.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */, 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, + F1D042A02BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 31ECDA0F2BED317300AE679F /* BundleExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -10876,6 +10980,7 @@ 4B9DB0412A983B24000927DB /* WaitlistDialogView.swift in Sources */, 37D2377A287EB8CA00BCE03B /* TabIndex.swift in Sources */, B60C6F8D29B200AB007BFAA8 /* SavePanelAccessoryView.swift in Sources */, + F1D042992BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 37534CA3281132CB002621E7 /* TabLazyLoaderDataSource.swift in Sources */, 3158B15C2B0BF76D00AF130C /* DataBrokerProtectionAppEvents.swift in Sources */, 4B723E0E26B0006300E14D75 /* LoginImport.swift in Sources */, @@ -11026,6 +11131,7 @@ 1DFAB51D2A8982A600A0F7F6 /* SetExtension.swift in Sources */, 315AA07028CA5CC800200030 /* YoutubePlayerNavigationHandler.swift in Sources */, 37AFCE9227DB8CAD00471A10 /* PreferencesAboutView.swift in Sources */, + F1D042A12BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, 4B2F565C2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, 9826B0A02747DF3D0092F683 /* ContentBlocking.swift in Sources */, 4B379C2227BDBA29008A968E /* LocalAuthenticationService.swift in Sources */, @@ -11056,6 +11162,7 @@ AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemInterface.swift in Sources */, 9F6434612BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, + 3158B1502B0BF75200AF130C /* DataBrokerProtectionLoginItemScheduler.swift in Sources */, 7BBA7CE62BAB03C1007579A3 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, 4B9DB01D2A983B24000927DB /* Waitlist.swift in Sources */, @@ -11124,6 +11231,7 @@ B626A7602992407D00053070 /* CancellableExtension.swift in Sources */, 37D23785287F4E6500BCE03B /* PinnedTabsHostingView.swift in Sources */, 4BB99CFE26FE191E001E4761 /* FirefoxBookmarksReader.swift in Sources */, + F1DA518C2BF607D200CF29FA /* SubscriptionRedirectManager.swift in Sources */, 4BBC16A227C485BC00E00A38 /* DeviceIdleStateDetector.swift in Sources */, 4B379C2427BDE1B0008A968E /* FlatButton.swift in Sources */, 37054FC92873301700033B6F /* PinnedTabView.swift in Sources */, @@ -11131,6 +11239,7 @@ 4BA1A6A0258B079600F6F690 /* DataEncryption.swift in Sources */, B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */, B626A76D29928B1600053070 /* TestsClosureNavigationResponder.swift in Sources */, + F1DA51862BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, 85B7184E27677CBB00B4277F /* RootView.swift in Sources */, AABEE6AF24AD22B90043105B /* AddressBarTextField.swift in Sources */, B693954C26F04BEB0015B914 /* FocusRingView.swift in Sources */, @@ -11165,7 +11274,6 @@ 31ECDA112BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 4B4D60E02A0C875F00BCD287 /* Bundle+VPN.swift in Sources */, AA6820E425502F19005ED0D5 /* WebsiteDataStore.swift in Sources */, - 9F64346B2BECA38B00D2D8A0 /* SubscriptionAttributionPixelHandler.swift in Sources */, B6F9BDDC2B45B7EE00677B33 /* WebsiteInfo.swift in Sources */, 4B67854A2AA8DE75008A5004 /* NetworkProtectionFeatureVisibility.swift in Sources */, B64C852A26942AC90048FEBE /* PermissionContextMenu.swift in Sources */, @@ -11223,7 +11331,6 @@ B6BCC54F2AFE4F7D002C5499 /* DataImportTypePicker.swift in Sources */, AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */, 4B37EE752B4CFF3300A89A61 /* DataBrokerProtectionRemoteMessaging.swift in Sources */, - 9F6434682BEC9A5F00D2D8A0 /* SubscriptionRedirectManager.swift in Sources */, B655369B268442EE00085A79 /* GeolocationProvider.swift in Sources */, B6C0B23C26E87D900031CB7F /* NSAlert+ActiveDownloadsTermination.swift in Sources */, AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */, @@ -11468,6 +11575,7 @@ 858A797F26A79EAA00A75A42 /* UserText+PasswordManager.swift in Sources */, B693954E26F04BEB0015B914 /* LoadingProgressView.swift in Sources */, B69B503C2726A12500758A2B /* StatisticsStore.swift in Sources */, + F1FDC9382BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 3158B14A2B0BF74300AF130C /* DataBrokerProtectionDebugMenu.swift in Sources */, 4BBDEE9128FC14760092FAA6 /* BWInstallationService.swift in Sources */, 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, @@ -11537,6 +11645,7 @@ AAC82C60258B6CB5009B6B42 /* TabPreviewWindowController.swift in Sources */, AAC5E4E425D6BA9C007F5990 /* NSSizeExtension.swift in Sources */, AA6820EB25503D6A005ED0D5 /* Fire.swift in Sources */, + F1FDC9292BF4DF48006B1435 /* SubscriptionEnvironment+Default.swift in Sources */, 3158B1492B0BF73000AF130C /* DBPHomeViewController.swift in Sources */, 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */, @@ -11577,6 +11686,7 @@ B6C00ECB292F839D009C73A6 /* AutofillTabExtension.swift in Sources */, B6E319382953446000DD3BCF /* Assertions.swift in Sources */, AAB549DF25DAB8F80058460B /* BookmarkViewModel.swift in Sources */, + F118EA7D2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift in Sources */, 85707F28276A34D900DC0649 /* DaxSpeech.swift in Sources */, 31F28C5328C8EECA00119F70 /* DuckURLSchemeHandler.swift in Sources */, AA13DCB4271480B0006D48D3 /* FirePopoverViewModel.swift in Sources */, @@ -12924,8 +13034,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 145.3.3; + branch = fcappelli/subscription_refactoring_2; + kind = branch; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -13574,6 +13684,16 @@ package = B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */; productName = SwiftLintTool; }; + F1D0428D2BFB9F9C00A31506 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; + F1D0428F2BFB9FA300A31506 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; F1D43AF22B98E47800BAB743 /* BareBonesBrowserKit */ = { isa = XCSwiftPackageProductDependency; package = F1D43AF12B98E47800BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */; @@ -13584,6 +13704,26 @@ package = F1D43AF12B98E47800BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */; productName = BareBonesBrowserKit; }; + F1DA51A22BF6114200CF29FA /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; + F1DA51A42BF6114200CF29FA /* SubscriptionTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = SubscriptionTestingUtilities; + }; + F1DA51A62BF6114B00CF29FA /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Subscription; + }; + F1DA51A82BF6114C00CF29FA /* SubscriptionTestingUtilities */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = SubscriptionTestingUtilities; + }; F1DF95E62BD188B60045E591 /* LoginItems */ = { isa = XCSwiftPackageProductDependency; productName = LoginItems; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a0e5fe45de..981f226c05 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "a49bbac8aa58033981a5a946d220886366dd471b", - "version" : "145.3.3" + "branch" : "fcappelli/subscription_refactoring_2", + "revision" : "874ae4269db821797742655e134e72199c2813c8" } }, { diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme index 7198d6f122..3afb1cb16d 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme @@ -154,6 +154,9 @@ + + diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index afbeaf6d23..f0811eca7f 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -34,12 +34,12 @@ import ServiceManagement import SyncDataProviders import UserNotifications import Lottie - import NetworkProtection import Subscription import NetworkProtectionIPC +import DataBrokerProtection -@MainActor +// @MainActor final class AppDelegate: NSObject, NSApplicationDelegate { #if DEBUG @@ -86,20 +86,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? - // Needs to be lazy as indirectly depends on AppDelegate - private lazy var networkProtectionSubscriptionEventHandler: NetworkProtectionSubscriptionEventHandler = { - - let ipcClient = TunnelControllerIPCClient() - let tunnelController = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) - let vpnUninstaller = VPNUninstaller(ipcClient: ipcClient) - - return NetworkProtectionSubscriptionEventHandler( - tunnelController: tunnelController, - vpnUninstaller: vpnUninstaller) - }() + private var accountManager: AccountManaging { + subscriptionManager.accountManager + } + public let subscriptionManager: SubscriptionManaging + public let vpnSettings = VPNSettings(defaults: .netP) + private var networkProtectionSubscriptionEventHandler: NetworkProtectionSubscriptionEventHandler? #if DBP - private let dataBrokerProtectionSubscriptionEventHandler = DataBrokerProtectionSubscriptionEventHandler() + private var dataBrokerProtectionSubscriptionEventHandler: DataBrokerProtectionSubscriptionEventHandler? #endif private var didFinishLaunching = false @@ -188,19 +183,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate { privacyConfigManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager ) - #if APPSTORE || !STRIPE - SubscriptionPurchaseEnvironment.current = .appStore - #else - SubscriptionPurchaseEnvironment.current = .stripe - #endif - } + // Configure Subscription + subscriptionManager = SubscriptionManager() - static func configurePixelKit() { -#if DEBUG - Self.setUpPixelKit(dryRun: true) -#else - Self.setUpPixelKit(dryRun: false) -#endif + // Update VPN environment and match the Subscription environment + vpnSettings.alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment) + + // Update DBP environment and match the Subscription environment + DataBrokerProtectionSettings().alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment) } func applicationWillFinishLaunching(_ notification: Notification) { @@ -217,6 +207,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate { #endif appIconChanger = AppIconChanger(internalUserDecider: internalUserDecider) + + // Configure Event handlers + let ipcClient = TunnelControllerIPCClient() + let tunnelController = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) + let vpnUninstaller = VPNUninstaller(ipcClient: ipcClient) + + networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler(subscriptionManager: subscriptionManager, + tunnelController: tunnelController, + vpnUninstaller: vpnUninstaller) +#if DBP + dataBrokerProtectionSubscriptionEventHandler = DataBrokerProtectionSubscriptionEventHandler(subscriptionManager: subscriptionManager) +#endif } // swiftlint:disable:next function_body_length @@ -264,19 +266,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { startupSync() - let defaultEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment.default - - let currentEnvironment = UserDefaultsWrapper(key: .subscriptionEnvironment, - defaultValue: defaultEnvironment).wrappedValue - SubscriptionPurchaseEnvironment.currentServiceEnvironment = currentEnvironment - - Task { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - if let token = accountManager.accessToken { - _ = await SubscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) - _ = await accountManager.fetchEntitlements(cachePolicy: .reloadIgnoringLocalCacheData) - } - } + subscriptionManager.loadInitialData() if [.normal, .uiTests].contains(NSApp.runType) { stateRestorationManager.applicationDidFinishLaunching() @@ -314,13 +304,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { UserDefaultsWrapper.clearRemovedKeys() - networkProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents() + networkProtectionSubscriptionEventHandler?.registerForSubscriptionAccountManagerEvents() - NetworkProtectionAppEvents().applicationDidFinishLaunching() + NetworkProtectionAppEvents(featureVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager)).applicationDidFinishLaunching() UNUserNotificationCenter.current().delegate = self #if DBP - dataBrokerProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents() + dataBrokerProtectionSubscriptionEventHandler?.registerForSubscriptionAccountManagerEvents() #endif #if DBP @@ -336,15 +326,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate { syncService?.initializeIfNeeded() syncService?.scheduler.notifyAppLifecycleEvent() - NetworkProtectionAppEvents().applicationDidBecomeActive() - + NetworkProtectionAppEvents(featureVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager)).applicationDidBecomeActive() #if DBP DataBrokerProtectionAppEvents().applicationDidBecomeActive() #endif AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager.toggleProtectionsCounter.sendEventsIfNeeded() - updateSubscriptionStatus() + subscriptionManager.updateSubscriptionStatus { isActive in + if isActive { + PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActive, frequency: .daily) + } + } } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { @@ -386,9 +379,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate { urlEventHandler.handleFiles(files) } - private func applyPreferredTheme() { - let appearancePreferences = AppearancePreferences() - appearancePreferences.updateUserInterfaceStyle() + // MARK: - PixelKit + + static func configurePixelKit() { +#if DEBUG + Self.setUpPixelKit(dryRun: true) +#else + Self.setUpPixelKit(dryRun: false) +#endif } private static func setUpPixelKit(dryRun: Bool) { @@ -415,6 +413,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } } + // MARK: - Theme + + private func applyPreferredTheme() { + let appearancePreferences = AppearancePreferences() + appearancePreferences.updateUserInterfaceStyle() + } + // MARK: - Sync private func startupSync() { @@ -490,7 +495,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { switch response { case .alertSecondButtonReturn: alert.window.sheetParent?.endSheet(alert.window) - WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .sync) + DispatchQueue.main.async { + WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .sync) + } default: break } @@ -567,30 +574,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } private func setUpAutoClearHandler() { - autoClearHandler = AutoClearHandler(preferences: .shared, - fireViewModel: FireCoordinator.fireViewModel, - stateRestorationManager: stateRestorationManager) - autoClearHandler.handleAppLaunch() - autoClearHandler.onAutoClearCompleted = { - NSApplication.shared.reply(toApplicationShouldTerminate: true) - } - } - -} - -func updateSubscriptionStatus() { - Task { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - - guard let token = accountManager.accessToken else { return } - - if case .success(let subscription) = await SubscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) { - if subscription.isActive { - PixelKit.fire(PrivacyProPixel.privacyProSubscriptionActive, frequency: .daily) + DispatchQueue.main.async { + self.autoClearHandler = AutoClearHandler(preferences: .shared, + fireViewModel: FireCoordinator.fireViewModel, + stateRestorationManager: self.stateRestorationManager) + self.autoClearHandler.handleAppLaunch() + self.autoClearHandler.onAutoClearCompleted = { + NSApplication.shared.reply(toApplicationShouldTerminate: true) } } - - _ = await accountManager.fetchEntitlements(cachePolicy: .reloadIgnoringLocalCacheData) } } @@ -609,7 +601,9 @@ extension AppDelegate: UNUserNotificationCenterDelegate { #if DBP if response.notification.request.identifier == DataBrokerProtectionWaitlist.notificationIdentifier { - DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .localPush) + DispatchQueue.main.async { + DataBrokerProtectionAppEvents().handleWaitlistInvitedNotification(source: .localPush) + } } #endif } diff --git a/DuckDuckGo/Application/Application.swift b/DuckDuckGo/Application/Application.swift index 2099e4aef8..73045c9ca9 100644 --- a/DuckDuckGo/Application/Application.swift +++ b/DuckDuckGo/Application/Application.swift @@ -23,16 +23,18 @@ import Foundation final class Application: NSApplication { private let copyHandler = CopyHandler() - private var _delegate: AppDelegate! +// private var _delegate: AppDelegate! + public static var appDelegate: AppDelegate! override init() { super.init() - _delegate = AppDelegate() - self.delegate = _delegate + let delegate = AppDelegate() + self.delegate = delegate + Application.appDelegate = delegate - let mainMenu = MainMenu(featureFlagger: _delegate.featureFlagger, - bookmarkManager: _delegate.bookmarksManager, + let mainMenu = MainMenu(featureFlagger: delegate.featureFlagger, + bookmarkManager: delegate.bookmarksManager, faviconManager: FaviconManager.shared, copyHandler: copyHandler) self.mainMenu = mainMenu diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index 003cf5500b..723fbc2123 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -20,6 +20,7 @@ import Common import Foundation import AppKit import PixelKit +import Subscription import NetworkProtectionUI @@ -27,10 +28,10 @@ import NetworkProtectionUI import DataBrokerProtection #endif -@MainActor +// @MainActor final class URLEventHandler { - private let handler: @MainActor (URL) -> Void + private let handler: (URL) -> Void private var didFinishLaunching = false private var urlsToOpen = [URL]() @@ -50,7 +51,9 @@ final class URLEventHandler { if !urlsToOpen.isEmpty { for url in urlsToOpen { - self.handler(url) + DispatchQueue.main.async { + self.handler(url) + } } self.urlsToOpen = [] @@ -96,7 +99,9 @@ final class URLEventHandler { private func handleURLs(_ urls: [URL]) { if didFinishLaunching { - urls.forEach { self.handler($0) } + urls.forEach { + self.handler($0) + } } else { self.urlsToOpen.append(contentsOf: urls) } @@ -113,54 +118,58 @@ final class URLEventHandler { } #endif - if url.isFileURL && url.pathExtension == WebKitDownloadTask.downloadExtension { - guard let mainViewController = { - if let mainWindowController = WindowControllersManager.shared.lastKeyMainWindowController { - return mainWindowController.mainViewController + DispatchQueue.main.async { + if url.isFileURL && url.pathExtension == WebKitDownloadTask.downloadExtension { + guard let mainViewController = { + if let mainWindowController = WindowControllersManager.shared.lastKeyMainWindowController { + return mainWindowController.mainViewController + } + return WindowsManager.openNewWindow(with: .newtab, source: .ui, isBurner: false)?.contentViewController as? MainViewController + }() else { return } + + if !mainViewController.navigationBarViewController.isDownloadsPopoverShown { + mainViewController.navigationBarViewController.toggleDownloadsPopover(keepButtonVisible: false) } - return WindowsManager.openNewWindow(with: .newtab, source: .ui, isBurner: false)?.contentViewController as? MainViewController - }() else { return } - if !mainViewController.navigationBarViewController.isDownloadsPopoverShown { - mainViewController.navigationBarViewController.toggleDownloadsPopover(keepButtonVisible: false) + return } - return - } - - if url.scheme?.isNetworkProtectionScheme == false && url.scheme?.isDataBrokerProtectionScheme == false { - WaitlistModalDismisser.dismissWaitlistModalViewControllerIfNecessary(url) - WindowControllersManager.shared.show(url: url, source: .appOpenUrl, newTab: true) + if url.scheme?.isNetworkProtectionScheme == false && url.scheme?.isDataBrokerProtectionScheme == false { + WaitlistModalDismisser.dismissWaitlistModalViewControllerIfNecessary(url) + WindowControllersManager.shared.show(url: url, source: .appOpenUrl, newTab: true) + } } } /// Handles NetP URLs - /// private static func handleNetworkProtectionURL(_ url: URL) { - switch url { - case AppLaunchCommand.showStatus.launchURL: - Task { - await WindowControllersManager.shared.showNetworkProtectionStatus() - } - case AppLaunchCommand.showSettings.launchURL: - WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) - case AppLaunchCommand.shareFeedback.launchURL: - WindowControllersManager.shared.showShareFeedbackModal() - case AppLaunchCommand.justOpen.launchURL: - WindowControllersManager.shared.showMainWindow() - case AppLaunchCommand.showVPNLocations.launchURL: - WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) - WindowControllersManager.shared.showLocationPickerSheet() - case AppLaunchCommand.showPrivacyPro.launchURL: - WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) - PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) + DispatchQueue.main.async { + switch url { + case AppLaunchCommand.showStatus.launchURL: + Task { + await WindowControllersManager.shared.showNetworkProtectionStatus() + } + case AppLaunchCommand.showSettings.launchURL: + WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) + case AppLaunchCommand.shareFeedback.launchURL: + WindowControllersManager.shared.showShareFeedbackModal() + case AppLaunchCommand.justOpen.launchURL: + WindowControllersManager.shared.showMainWindow() + case AppLaunchCommand.showVPNLocations.launchURL: + WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) + WindowControllersManager.shared.showLocationPickerSheet() + case AppLaunchCommand.showPrivacyPro.launchURL: + let url = Application.appDelegate.subscriptionManager.url(for: .purchase) + WindowControllersManager.shared.showTab(with: .subscription(url)) + PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) #if !APPSTORE && !DEBUG - case AppLaunchCommand.moveAppToApplications.launchURL: - // this should be run after NSApplication.shared is set - PFMoveToApplicationsFolderIfNecessary(false) + case AppLaunchCommand.moveAppToApplications.launchURL: + // this should be run after NSApplication.shared is set + PFMoveToApplicationsFolderIfNecessary(false) #endif - default: - return + default: + return + } } } @@ -171,15 +180,14 @@ final class URLEventHandler { switch url { case DataBrokerProtectionNotificationCommand.showDashboard.url: NotificationCenter.default.post(name: DataBrokerProtectionNotifications.shouldReloadUI, object: nil) - - WindowControllersManager.shared.showTab(with: .dataBrokerProtection) + DispatchQueue.main.async { + WindowControllersManager.shared.showTab(with: .dataBrokerProtection) + } default: return } } - #endif - } private extension String { diff --git a/DuckDuckGo/Common/Logging/Logging.swift b/DuckDuckGo/Common/Logging/Logging.swift index 72a121cd48..3e05109032 100644 --- a/DuckDuckGo/Common/Logging/Logging.swift +++ b/DuckDuckGo/Common/Logging/Logging.swift @@ -52,26 +52,26 @@ extension OSLog { } } - @OSLogWrapper(.atb) static var atb - @OSLogWrapper(.config) static var config - @OSLogWrapper(.downloads) static var downloads - @OSLogWrapper(.fire) static var fire - @OSLogWrapper(.dataImportExport) static var dataImportExport - @OSLogWrapper(.pixel) static var pixel - @OSLogWrapper(.httpsUpgrade) static var httpsUpgrade - @OSLogWrapper(.favicons) static var favicons - @OSLogWrapper(.autoLock) static var autoLock - @OSLogWrapper(.tabLazyLoading) static var tabLazyLoading - @OSLogWrapper(.autoconsent) static var autoconsent - @OSLogWrapper(.bookmarks) static var bookmarks - @OSLogWrapper(.attribution) static var attribution - @OSLogWrapper(.bitwarden) static var bitwarden - @OSLogWrapper(.navigation) static var navigation - @OSLogWrapper(.duckPlayer) static var duckPlayer - @OSLogWrapper(.tabSnapshots) static var tabSnapshots - @OSLogWrapper(.sync) static var sync - @OSLogWrapper(.networkProtection) static var networkProtection - @OSLogWrapper(.dbp) static var dbp + @OSLogWrapper(AppCategories.atb) static var atb + @OSLogWrapper(AppCategories.config) static var config + @OSLogWrapper(AppCategories.downloads) static var downloads + @OSLogWrapper(AppCategories.fire) static var fire + @OSLogWrapper(AppCategories.dataImportExport) static var dataImportExport + @OSLogWrapper(AppCategories.pixel) static var pixel + @OSLogWrapper(AppCategories.httpsUpgrade) static var httpsUpgrade + @OSLogWrapper(AppCategories.favicons) static var favicons + @OSLogWrapper(AppCategories.autoLock) static var autoLock + @OSLogWrapper(AppCategories.tabLazyLoading) static var tabLazyLoading + @OSLogWrapper(AppCategories.autoconsent) static var autoconsent + @OSLogWrapper(AppCategories.bookmarks) static var bookmarks + @OSLogWrapper(AppCategories.attribution) static var attribution + @OSLogWrapper(AppCategories.bitwarden) static var bitwarden + @OSLogWrapper(AppCategories.navigation) static var navigation + @OSLogWrapper(AppCategories.duckPlayer) static var duckPlayer + @OSLogWrapper(AppCategories.tabSnapshots) static var tabSnapshots + @OSLogWrapper(AppCategories.sync) static var sync + @OSLogWrapper(AppCategories.networkProtection) static var networkProtection + @OSLogWrapper(AppCategories.dbp) static var dbp // Debug->Logging categories will only be enabled for one day @UserDefaultsWrapper(key: .loggingEnabledDate, defaultValue: .distantPast) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 35104d98e3..39eb3062a3 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -280,7 +280,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } @objc private func runCustomJSON() { - let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager() + let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(subscriptionManager: Application.appDelegate.subscriptionManager) let viewController = DataBrokerRunCustomJSONViewController(authenticationManager: authenticationManager) let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 500, height: 400), styleMask: [.titled, .closable, .miniaturizable, .resizable], @@ -339,25 +339,13 @@ final class DataBrokerProtectionDebugMenu: NSMenu { settings.showInMenuBar.toggle() } - @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { - let title = menuItem.title - let selectedEnvironment: DataBrokerProtectionSettings.SelectedEnvironment - - if title == EnvironmentTitle.staging.rawValue { - selectedEnvironment = .staging - } else { - selectedEnvironment = .production - } - - settings.selectedEnvironment = selectedEnvironment - } - // MARK: - Utility Functions private func populateDataBrokerProtectionEnvironmentListMenuItems() { environmentMenu.items = [ - NSMenuItem(title: EnvironmentTitle.production.rawValue, action: #selector(setSelectedEnvironment(_:)), target: self, keyEquivalent: ""), - NSMenuItem(title: EnvironmentTitle.staging.rawValue, action: #selector(setSelectedEnvironment(_:)), target: self, keyEquivalent: ""), + NSMenuItem(title: "⚠️ The environment can be set in the Subscription > Environment menu", action: nil, target: nil), + NSMenuItem(title: EnvironmentTitle.production.rawValue, action: nil, target: nil, keyEquivalent: ""), + NSMenuItem(title: EnvironmentTitle.staging.rawValue, action: nil, target: nil, keyEquivalent: ""), ] } @@ -417,9 +405,10 @@ final class DataBrokerProtectionDebugMenu: NSMenu { private func updateEnvironmentMenu() { let selectedEnvironment = settings.selectedEnvironment + guard environmentMenu.items.count == 3 else { return } - environmentMenu.items.first?.state = selectedEnvironment == .production ? .on: .off - environmentMenu.items.last?.state = selectedEnvironment == .staging ? .on: .off + environmentMenu.items[1].state = selectedEnvironment == .production ? .on: .off + environmentMenu.items[2].state = selectedEnvironment == .staging ? .on: .off } private func updateShowStatusMenuIconMenu() { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift index 111892a5fe..3a181f4594 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionManager.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionManager.swift @@ -51,7 +51,7 @@ public final class DataBrokerProtectionManager { }() private init() { - self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager() + self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(subscriptionManager: Application.appDelegate.subscriptionManager) } public func isUserAuthenticated() -> Bool { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift b/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift index 50f8a36d4b..34142380b3 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift @@ -23,9 +23,16 @@ import DataBrokerProtection import PixelKit final class DataBrokerProtectionSubscriptionEventHandler { + + private let subscriptionManager: SubscriptionManaging + private let authRepository: AuthenticationRepository private let featureDisabler: DataBrokerProtectionFeatureDisabling - init(featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler()) { + init(subscriptionManager: SubscriptionManaging, + authRepository: AuthenticationRepository = KeychainAuthenticationData(), + featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler()) { + self.subscriptionManager = subscriptionManager + self.authRepository = authRepository self.featureDisabler = featureDisabler } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 1b24150862..3a41e65525 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -540,7 +540,7 @@ import SubscriptionUI toggleBookmarksShortcutMenuItem.title = LocalPinningManager.shared.shortcutTitle(for: .bookmarks) toggleDownloadsShortcutMenuItem.title = LocalPinningManager.shared.shortcutTitle(for: .downloads) - if DefaultNetworkProtectionVisibility().isVPNVisible() { + if DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager).isVPNVisible() { toggleNetworkProtectionShortcutMenuItem.isHidden = false toggleNetworkProtectionShortcutMenuItem.title = LocalPinningManager.shared.shortcutTitle(for: .networkProtection) } else { @@ -620,24 +620,27 @@ import SubscriptionUI NSMenuItem(title: "Trigger Fatal Error", action: #selector(MainViewController.triggerFatalError)) - let currentEnvironmentWrapper = UserDefaultsWrapper(key: .subscriptionEnvironment, defaultValue: SubscriptionPurchaseEnvironment.ServiceEnvironment.default) let isInternalTestingWrapper = UserDefaultsWrapper(key: .subscriptionInternalTesting, defaultValue: false) + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! - SubscriptionDebugMenu( - currentEnvironment: { currentEnvironmentWrapper.wrappedValue.rawValue }, - updateEnvironment: { - guard let newEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment(rawValue: $0) else { return } - currentEnvironmentWrapper.wrappedValue = newEnvironment - SubscriptionPurchaseEnvironment.currentServiceEnvironment = newEnvironment - VPNSettings(defaults: .netP).selectedEnvironment = newEnvironment == .staging ? .staging : .production - }, - isInternalTestingEnabled: { isInternalTestingWrapper.wrappedValue }, - updateInternalTestingFlag: { isInternalTestingWrapper.wrappedValue = $0 }, - currentViewController: { - WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController - }, - subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs) - ) + var currentEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + let updateServiceEnvironment: (SubscriptionEnvironment.ServiceEnvironment) -> Void = { env in + currentEnvironment.serviceEnvironment = env + SubscriptionManager.save(subscriptionEnvironment: currentEnvironment, userDefaults: subscriptionUserDefaults) + } + let updatePurchasingPlatform: (SubscriptionEnvironment.PurchasePlatform) -> Void = { platform in + currentEnvironment.purchasePlatform = platform + SubscriptionManager.save(subscriptionEnvironment: currentEnvironment, userDefaults: subscriptionUserDefaults) + } + + SubscriptionDebugMenu(currentEnvironment: currentEnvironment, + updateServiceEnvironment: updateServiceEnvironment, + updatePurchasingPlatform: updatePurchasingPlatform, + isInternalTestingEnabled: { isInternalTestingWrapper.wrappedValue }, + updateInternalTestingFlag: { isInternalTestingWrapper.wrappedValue = $0 }, + currentViewController: { WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController }, + subscriptionManager: Application.appDelegate.subscriptionManager) NSMenuItem(title: "Logging").submenu(setupLoggingMenu()) } diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 12094bbd16..103b90bc81 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -42,29 +42,41 @@ extension AppDelegate { // MARK: - File @objc func newWindow(_ sender: Any?) { - WindowsManager.openNewWindow() + DispatchQueue.main.async { + WindowsManager.openNewWindow() + } } @objc func newBurnerWindow(_ sender: Any?) { - WindowsManager.openNewWindow(burnerMode: BurnerMode(isBurner: true)) + DispatchQueue.main.async { + WindowsManager.openNewWindow(burnerMode: BurnerMode(isBurner: true)) + } } @objc func newTab(_ sender: Any?) { - WindowsManager.openNewWindow() + DispatchQueue.main.async { + WindowsManager.openNewWindow() + } } @objc func openLocation(_ sender: Any?) { - WindowsManager.openNewWindow() + DispatchQueue.main.async { + WindowsManager.openNewWindow() + } } @objc func closeAllWindows(_ sender: Any?) { - WindowsManager.closeWindows() + DispatchQueue.main.async { + WindowsManager.closeWindows() + } } // MARK: - History @objc func reopenLastClosedTab(_ sender: Any?) { - RecentlyClosedCoordinator.shared.reopenItem() + DispatchQueue.main.async { + RecentlyClosedCoordinator.shared.reopenItem() + } } @objc func recentlyClosedAction(_ sender: Any?) { @@ -73,8 +85,9 @@ extension AppDelegate { assertionFailure("Wrong represented object for recentlyClosedAction()") return } - - RecentlyClosedCoordinator.shared.reopenItem(cacheItem) + DispatchQueue.main.async { + RecentlyClosedCoordinator.shared.reopenItem(cacheItem) + } } @objc func openVisit(_ sender: NSMenuItem) { @@ -83,34 +96,41 @@ extension AppDelegate { assertionFailure("Wrong represented object") return } - - WindowsManager.openNewWindow(with: Tab(content: .contentFromURL(url, source: .historyEntry), shouldLoadInBackground: true)) + DispatchQueue.main.async { + WindowsManager.openNewWindow(with: Tab(content: .contentFromURL(url, source: .historyEntry), shouldLoadInBackground: true)) + } } @objc func clearAllHistory(_ sender: NSMenuItem) { - guard let window = WindowsManager.openNewWindow(with: Tab(content: .newtab)), - let windowController = window.windowController as? MainWindowController else { - assertionFailure("No reference to main window controller") - return - } + DispatchQueue.main.async { + guard let window = WindowsManager.openNewWindow(with: Tab(content: .newtab)), + let windowController = window.windowController as? MainWindowController else { + assertionFailure("No reference to main window controller") + return + } - windowController.mainViewController.clearAllHistory(sender) + windowController.mainViewController.clearAllHistory(sender) + } } @objc func clearThisHistory(_ sender: ClearThisHistoryMenuItem) { - guard let window = WindowsManager.openNewWindow(with: Tab(content: .newtab)), - let windowController = window.windowController as? MainWindowController else { - assertionFailure("No reference to main window controller") - return - } + DispatchQueue.main.async { + guard let window = WindowsManager.openNewWindow(with: Tab(content: .newtab)), + let windowController = window.windowController as? MainWindowController else { + assertionFailure("No reference to main window controller") + return + } - windowController.mainViewController.clearThisHistory(sender) + windowController.mainViewController.clearThisHistory(sender) + } } // MARK: - Window @objc func reopenAllWindowsFromLastSession(_ sender: Any?) { - _=stateRestorationManager.restoreLastSessionState(interactive: true) + DispatchQueue.main.async { + self.stateRestorationManager.restoreLastSessionState(interactive: true) + } } // MARK: - Help @@ -118,7 +138,9 @@ extension AppDelegate { #if FEEDBACK @objc func openFeedback(_ sender: Any?) { - FeedbackPresenter.presentFeedbackForm() + DispatchQueue.main.async { + FeedbackPresenter.presentFeedbackForm() + } } @objc func openReportBrokenSite(_ sender: Any?) { @@ -132,13 +154,15 @@ extension AppDelegate { display: true) privacyDashboardWindow = window - guard let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController, - let tabModel = parentWindowController.mainViewController.tabCollectionViewModel.selectedTabViewModel else { - assertionFailure("AppDelegate: Failed to present PrivacyDashboard") - return + DispatchQueue.main.async { + guard let parentWindowController = WindowControllersManager.shared.lastKeyMainWindowController, + let tabModel = parentWindowController.mainViewController.tabCollectionViewModel.selectedTabViewModel else { + assertionFailure("AppDelegate: Failed to present PrivacyDashboard") + return + } + privacyDashboardViewController.updateTabViewModel(tabModel) + parentWindowController.window?.beginSheet(window) { _ in } } - privacyDashboardViewController.updateTabViewModel(tabModel) - parentWindowController.window?.beginSheet(window) { _ in } } #endif @@ -154,22 +178,27 @@ extension AppDelegate { assertionFailure("Unexpected type of menuItem.representedObject: \(type(of: menuItem.representedObject))") return } - - let tab = Tab(content: .url(url, source: .bookmark), shouldLoadInBackground: true) - WindowsManager.openNewWindow(with: tab) + DispatchQueue.main.async { + let tab = Tab(content: .url(url, source: .bookmark), shouldLoadInBackground: true) + WindowsManager.openNewWindow(with: tab) + } } @objc func showManageBookmarks(_ sender: Any?) { - let tabCollection = TabCollection(tabs: [Tab(content: .bookmarks)]) - let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + DispatchQueue.main.async { + let tabCollection = TabCollection(tabs: [Tab(content: .bookmarks)]) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) - WindowsManager.openNewWindow(with: tabCollectionViewModel) + WindowsManager.openNewWindow(with: tabCollectionViewModel) + } } @objc func openPreferences(_ sender: Any?) { - let tabCollection = TabCollection(tabs: [Tab(content: .anySettingsPane)]) - let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) - WindowsManager.openNewWindow(with: tabCollectionViewModel) + DispatchQueue.main.async { + let tabCollection = TabCollection(tabs: [Tab(content: .anySettingsPane)]) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + WindowsManager.openNewWindow(with: tabCollectionViewModel) + } } @objc func openAbout(_ sender: Any?) { @@ -182,9 +211,12 @@ extension AppDelegate { } @objc func openImportBrowserDataWindow(_ sender: Any?) { - DataImportView().show() + DispatchQueue.main.async { + DataImportView().show() + } } + @MainActor @objc func openExportLogins(_ sender: Any?) { guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController, let window = windowController.window else { return } @@ -223,6 +255,7 @@ extension AppDelegate { } } + @MainActor @objc func openExportBookmarks(_ sender: Any?) { guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController, let window = windowController.window, @@ -246,16 +279,20 @@ extension AppDelegate { } @objc func fireButtonAction(_ sender: NSButton) { - FireCoordinator.fireButtonAction() + DispatchQueue.main.async { + FireCoordinator.fireButtonAction() + } } @objc func navigateToPrivateEmail(_ sender: Any?) { - guard let window = NSApplication.shared.keyWindow, - let windowController = window.windowController as? MainWindowController else { - assertionFailure("No reference to main window controller") - return + DispatchQueue.main.async { + guard let window = NSApplication.shared.keyWindow, + let windowController = window.windowController as? MainWindowController else { + assertionFailure("No reference to main window controller") + return + } + windowController.mainViewController.browserTabViewController.openNewTab(with: .url(URL.duckDuckGoEmailLogin, source: .ui)) } - windowController.mainViewController.browserTabViewController.openNewTab(with: .url(URL.duckDuckGoEmailLogin, source: .ui)) } } @@ -751,7 +788,7 @@ extension MainViewController { /// Clears the PrivacyPro state to make testing easier. /// private func clearPrivacyProState() { - AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).signOut() + Application.appDelegate.subscriptionManager.accountManager.signOut() resetThankYouModalChecks(nil) UserDefaults.netP.networkProtectionEntitlementsExpired = false diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 943f6a8e66..6acef850b7 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -67,6 +67,10 @@ final class AddressBarTextField: NSTextField { // flag when updating the Value from `handleTextDidChange()` private var currentTextDidChangeEvent: TextDidChangeEventType = .none +// var subscriptionEnvironment: SubscriptionEnvironment { +// Application.appDelegate.subscriptionManager.currentEnvironment +// } + // MARK: - Lifecycle override func awakeFromNib() { @@ -368,7 +372,9 @@ final class AddressBarTextField: NSTextField { #endif if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { - if providedUrl.isChild(of: URL.subscriptionBaseURL) || providedUrl.isChild(of: URL.identityTheftRestoration) { + let baseURL = Application.appDelegate.subscriptionManager.url(for: .baseURL) + let identityTheftRestorationURL = Application.appDelegate.subscriptionManager.url(for: .identityTheftRestoration) + if providedUrl.isChild(of: baseURL) || providedUrl.isChild(of: identityTheftRestorationURL) { self.updateValue(selectedTabViewModel: nil, addressBarString: nil) // reset self.window?.makeFirstResponder(nil) return diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index f2092a4641..b4a1e23483 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -57,7 +57,7 @@ final class MoreOptionsMenu: NSMenu { private let passwordManagerCoordinator: PasswordManagerCoordinating private let internalUserDecider: InternalUserDecider private lazy var sharingMenu: NSMenu = SharingMenu(title: UserText.shareMenuItem) - private lazy var accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + private let accountManager: AccountManaging private let networkProtectionFeatureVisibility: NetworkProtectionFeatureVisibility @@ -68,15 +68,17 @@ final class MoreOptionsMenu: NSMenu { init(tabCollectionViewModel: TabCollectionViewModel, emailManager: EmailManager = EmailManager(), passwordManagerCoordinator: PasswordManagerCoordinator, - networkProtectionFeatureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + networkProtectionFeatureVisibility: NetworkProtectionFeatureVisibility, sharingMenu: NSMenu? = nil, - internalUserDecider: InternalUserDecider) { + internalUserDecider: InternalUserDecider, + accountManager: AccountManaging) { self.tabCollectionViewModel = tabCollectionViewModel self.emailManager = emailManager self.passwordManagerCoordinator = passwordManagerCoordinator self.networkProtectionFeatureVisibility = networkProtectionFeatureVisibility self.internalUserDecider = internalUserDecider + self.accountManager = accountManager super.init(title: "") @@ -391,7 +393,9 @@ final class MoreOptionsMenu: NSMenu { } private func makeInactiveSubscriptionItems() -> [NSMenuItem] { - let shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false + let subscriptionManager = Application.appDelegate.subscriptionManager + let platform = subscriptionManager.currentEnvironment.purchasePlatform + let shouldHidePrivacyProDueToNoProducts = platform == .appStore && subscriptionManager.canPurchase == false if shouldHidePrivacyProDueToNoProducts { return [] } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index cab1a8fa70..02368d3191 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -71,6 +71,10 @@ final class NavigationBarViewController: NSViewController { return progressView }() + private var subscriptionManager: SubscriptionManaging { + Application.appDelegate.subscriptionManager + } + var addressBarViewController: AddressBarViewController? private var tabCollectionViewModel: TabCollectionViewModel @@ -269,7 +273,9 @@ final class NavigationBarViewController: NSViewController { let internalUserDecider = NSApp.delegateTyped.internalUserDecider let menu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: PasswordManagerCoordinator.shared, - internalUserDecider: internalUserDecider) + networkProtectionFeatureVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager), + internalUserDecider: internalUserDecider, + accountManager: subscriptionManager.accountManager) menu.actionDelegate = self let location = NSPoint(x: -menu.size.width + sender.bounds.width, y: sender.bounds.height + 4) menu.popUp(positioning: nil, at: location, in: sender) @@ -894,7 +900,7 @@ extension NavigationBarViewController: NSMenuDelegate { let isPopUpWindow = view.window?.isPopUpWindow ?? false - if !isPopUpWindow && DefaultNetworkProtectionVisibility().isVPNVisible() { + if !isPopUpWindow && DefaultNetworkProtectionVisibility(subscriptionManager: subscriptionManager).isVPNVisible() { let networkProtectionTitle = LocalPinningManager.shared.shortcutTitle(for: .networkProtection) menu.addItem(withTitle: networkProtectionTitle, action: #selector(toggleNetworkProtectionPanelPinning), keyEquivalent: "N") } @@ -1032,12 +1038,14 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { } func optionsButtonMenuRequestedSubscriptionPurchasePage(_ menu: NSMenu) { - WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) + let url = subscriptionManager.url(for: .purchase) + WindowControllersManager.shared.showTab(with: .subscription(url)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) } func optionsButtonMenuRequestedIdentityTheftRestoration(_ menu: NSMenu) { - WindowControllersManager.shared.showTab(with: .identityTheftRestoration(.identityTheftRestoration)) + let url = subscriptionManager.url(for: .identityTheftRestoration) + WindowControllersManager.shared.showTab(with: .identityTheftRestoration(url)) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index fe1993cb94..f48a2c97fb 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -27,7 +27,7 @@ extension NetworkProtectionDeviceManager { @MainActor static func create() -> NetworkProtectionDeviceManager { - let settings = VPNSettings(defaults: .netP) + let settings = Application.appDelegate.vpnSettings let keyStore = NetworkProtectionKeychainKeyStore() let tokenStore = NetworkProtectionKeychainTokenStore() return NetworkProtectionDeviceManager(environment: settings.selectedEnvironment, @@ -40,7 +40,7 @@ extension NetworkProtectionDeviceManager { extension NetworkProtectionCodeRedemptionCoordinator { convenience init() { - let settings = VPNSettings(defaults: .netP) + let settings = Application.appDelegate.vpnSettings self.init(environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), errorEvents: .networkProtectionAppDebugEvents, @@ -54,7 +54,7 @@ extension NetworkProtectionKeychainTokenStore { } convenience init(isSubscriptionEnabled: Bool) { - let accessTokenProvider: () -> String? = { AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).accessToken } + let accessTokenProvider: () -> String? = { Application.appDelegate.subscriptionManager.accountManager.accessToken } self.init(keychainType: .default, errorEvents: .networkProtectionAppDebugEvents, isSubscriptionEnabled: isSubscriptionEnabled, @@ -71,7 +71,7 @@ extension NetworkProtectionKeychainKeyStore { extension NetworkProtectionLocationListCompositeRepository { convenience init() { - let settings = VPNSettings(defaults: .netP) + let settings = Application.appDelegate.vpnSettings self.init( environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index aaf0b2de0f..bfe46a5e55 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -50,7 +50,7 @@ final class NetworkProtectionAppEvents { private let uninstaller: VPNUninstalling private let defaults: UserDefaults - init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + init(featureVisibility: NetworkProtectionFeatureVisibility, uninstaller: VPNUninstalling = VPNUninstaller(), defaults: UserDefaults = .netP) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 0323060f84..3cd6f11e63 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -168,7 +168,9 @@ final class NetworkProtectionDebugMenu: NSMenu { // MARK: - Tunnel Settings - private let settings = VPNSettings(defaults: .netP) + private var settings: VPNSettings { + Application.appDelegate.vpnSettings + } // MARK: - Debug Logic @@ -237,7 +239,7 @@ final class NetworkProtectionDebugMenu: NSMenu { /// @objc func logFeedbackMetadataToConsole(_ sender: Any?) { Task { @MainActor in - let collector = DefaultVPNMetadataCollector() + let collector = DefaultVPNMetadataCollector(accountManager: Application.appDelegate.subscriptionManager.accountManager) let metadata = await collector.collectMetadata() print(metadata.toPrettyPrintedJSON()!) @@ -319,8 +321,9 @@ final class NetworkProtectionDebugMenu: NSMenu { private func populateNetworkProtectionEnvironmentListMenuItems() { environmentMenu.items = [ - NSMenuItem(title: "Production", action: #selector(setSelectedEnvironment(_:)), target: self, keyEquivalent: ""), - NSMenuItem(title: "Staging", action: #selector(setSelectedEnvironment(_:)), target: self, keyEquivalent: ""), + NSMenuItem(title: "⚠️ The environment can be set in the Subscription > Environment menu", action: nil, target: nil), + NSMenuItem(title: "Production", action: nil, target: nil, keyEquivalent: ""), + NSMenuItem(title: "Staging", action: nil, target: nil, keyEquivalent: ""), ] } @@ -417,15 +420,10 @@ final class NetworkProtectionDebugMenu: NSMenu { private func updateEnvironmentMenu() { let selectedEnvironment = settings.selectedEnvironment + guard environmentMenu.items.count == 3 else { return } - switch selectedEnvironment { - case .production: - environmentMenu.items.first?.state = .on - environmentMenu.items.last?.state = .off - case .staging: - environmentMenu.items.first?.state = .off - environmentMenu.items.last?.state = .on - } + environmentMenu.items[1].state = selectedEnvironment == .production ? .on: .off + environmentMenu.items[2].state = selectedEnvironment == .staging ? .on: .off } private func updatePreferredServerMenu() { @@ -512,28 +510,6 @@ final class NetworkProtectionDebugMenu: NSMenu { } } - // MARK: Environment - - @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { - let title = menuItem.title - let selectedEnvironment: VPNSettings.SelectedEnvironment - - if title == "Staging" { - selectedEnvironment = .staging - } else { - selectedEnvironment = .production - } - - settings.selectedEnvironment = selectedEnvironment - - Task { - _ = try await NetworkProtectionDeviceManager.create().refreshServerList() - try? await populateNetworkProtectionServerListMenuItems() - - settings.selectedServer = .automatic - } - } - // MARK: - Exclusions private let dbpBackgroundAppIdentifier = Bundle.main.dbpBackgroundAgentBundleId diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index e4742d3521..de2dd586dd 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -71,7 +71,7 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { init(popoverManager: NetPPopoverManager, pinningManager: PinningManager = LocalPinningManager.shared, - vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), statusReporter: NetworkProtectionStatusReporter, iconProvider: IconProvider = NavigationBarIconProvider()) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 9bf6853f9a..44394aa36f 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -74,7 +74,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr // MARK: - Subscriptions - private let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + private let accessTokenStorage: SubscriptionTokenKeychainStorage // MARK: - Debug Options Support @@ -164,7 +164,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr defaults: UserDefaults, tokenStore: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), notificationCenter: NotificationCenter = .default, - logger: NetworkProtectionLogger = DefaultNetworkProtectionLogger()) { + logger: NetworkProtectionLogger = DefaultNetworkProtectionLogger(), + accessTokenStorage: SubscriptionTokenKeychainStorage) { self.logger = logger self.networkExtensionBundleID = networkExtensionBundleID @@ -173,6 +174,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr self.settings = settings self.defaults = defaults self.tokenStore = tokenStore + self.accessTokenStorage = accessTokenStorage subscribeToSettingsChanges() subscribeToStatusChanges() @@ -793,7 +795,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr } private func fetchAuthToken() throws -> NSString? { - if let accessToken = accountManager.accessToken { + + if let accessToken = try? accessTokenStorage.getAccessToken() { os_log(.error, log: .networkProtection, "🟢 TunnelController found token: %{public}d", accessToken) return Self.adaptAccessTokenForVPN(accessToken) as NSString? } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift index e2e26d0e67..3f9f0f86e4 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift @@ -190,7 +190,7 @@ extension VPNLocationViewModel { let locationListRepository = NetworkProtectionLocationListCompositeRepository() self.init( locationListRepository: locationListRepository, - settings: VPNSettings(defaults: .netP) + settings: Application.appDelegate.vpnSettings ) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift index a39fcb6de4..028b3ba4cc 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift @@ -53,7 +53,7 @@ final class NetworkProtectionIPCTunnelController { private let pixelKit: PixelFiring? private let errorRecorder: VPNOperationErrorRecorder - init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + init(featureVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), loginItemsManager: LoginItemsManaging = LoginItemsManager(), ipcClient: NetworkProtectionIPCClient, pixelKit: PixelFiring? = PixelKit.shared, diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift index 6daeb90569..aef4e46516 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionRemoteMessaging/NetworkProtectionRemoteMessaging.swift @@ -55,7 +55,7 @@ final class DefaultNetworkProtectionRemoteMessaging: NetworkProtectionRemoteMess messageStorage: HomePageRemoteMessagingStorage = DefaultHomePageRemoteMessagingStorage.networkProtection(), waitlistStorage: WaitlistStorage = WaitlistKeychainStore(waitlistIdentifier: "networkprotection", keychainAppGroup: Bundle.main.appGroup(bundle: .netP)), waitlistActivationDateStore: WaitlistActivationDateStore = DefaultWaitlistActivationDateStore(source: .netP), - networkProtectionVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + networkProtectionVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), minimumRefreshInterval: TimeInterval, userDefaults: UserDefaults = .standard ) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index 39752c05bd..6e3fa7ea0b 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -25,20 +25,19 @@ import NetworkProtectionUI final class NetworkProtectionSubscriptionEventHandler { - private let accountManager: AccountManager + private let subscriptionManager: SubscriptionManaging private let tunnelController: TunnelController private let networkProtectionTokenStorage: NetworkProtectionTokenStore private let vpnUninstaller: VPNUninstalling private let userDefaults: UserDefaults private var cancellables = Set() - init(accountManager: AccountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)), + init(subscriptionManager: SubscriptionManaging, tunnelController: TunnelController, networkProtectionTokenStorage: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), vpnUninstaller: VPNUninstalling, userDefaults: UserDefaults = .netP) { - - self.accountManager = accountManager + self.subscriptionManager = subscriptionManager self.tunnelController = tunnelController self.networkProtectionTokenStorage = networkProtectionTokenStorage self.vpnUninstaller = vpnUninstaller @@ -49,7 +48,7 @@ final class NetworkProtectionSubscriptionEventHandler { private func subscribeToEntitlementChanges() { Task { - switch await accountManager.hasEntitlement(for: .networkProtection) { + switch await subscriptionManager.accountManager.hasEntitlement(for: .networkProtection) { case .success(let hasEntitlements): Task { await handleEntitlementsChange(hasEntitlements: hasEntitlements) @@ -99,7 +98,7 @@ final class NetworkProtectionSubscriptionEventHandler { } @objc private func handleAccountDidSignIn() { - guard accountManager.accessToken != nil else { + guard subscriptionManager.accountManager.accessToken != nil else { assertionFailure("[NetP Subscription] AccountManager signed in but token could not be retrieved") return } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 5623af1f0c..8b4fd7288d 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -141,8 +141,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { #else let defaults = UserDefaults.netP #endif - let settings = VPNSettings(defaults: defaults) - switch event { case .userBecameActive: PixelKit.fire( @@ -327,8 +325,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - Initialization - @MainActor - @objc public init() { + // swiftlint:disable:next function_body_length + @MainActor @objc public init() { let isSubscriptionEnabled = false #if NETP_SYSTEM_EXTENSION @@ -338,26 +336,45 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { #endif NetworkProtectionLastVersionRunStore(userDefaults: defaults).lastExtensionVersionRun = AppVersion.shared.versionAndBuildNumber - let settings = VPNSettings(defaults: defaults) - let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) + + // MARK: - Configure Subscription + let subscriptionUserDefaults = UserDefaults(suiteName: MacPacketTunnelProvider.subscriptionsAppGroup)! + let notificationCenter: NetworkProtectionNotificationCenter = DistributedNotificationCenter.default() let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) - let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings, defaults: defaults) let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, serviceName: Self.tokenServiceName, errorEvents: debugEvents, isSubscriptionEnabled: isSubscriptionEnabled, accessTokenProvider: { nil } ) + let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, + key: UserDefaultsCacheKey.subscriptionEntitlements, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + // Align Subscription environment to the VPN environment + var subscriptionEnvironment = SubscriptionEnvironment.default + switch settings.selectedEnvironment { + case .production: + subscriptionEnvironment.serviceEnvironment = .production + case .staging: + subscriptionEnvironment.serviceEnvironment = .staging + } - let accountManager = AccountManager(subscriptionAppGroup: Self.subscriptionsAppGroup, accessTokenStorage: tokenStore) + let subscriptionService = SubscriptionService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let authService = AuthService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let accountManager = AccountManager(accessTokenStorage: tokenStore, + entitlementsCache: entitlementsCache, + subscriptionService: subscriptionService, + authService: authService) - SubscriptionPurchaseEnvironment.currentServiceEnvironment = settings.selectedEnvironment == .production ? .production : .staging let entitlementsCheck = { await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) } + let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) + let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings, defaults: defaults) + super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: tunnelHealthStore, controllerErrorStore: controllerErrorStore, diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift index 497ce84c7b..adc069cb3d 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift @@ -21,7 +21,8 @@ import Subscription import NetworkProtection import Common -extension NetworkProtectionKeychainTokenStore: SubscriptionTokenStorage { +extension NetworkProtectionKeychainTokenStore: SubscriptionTokenStoring { + public func store(accessToken: String) throws { try store(accessToken) } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index 29e8333811..7a69e10868 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -61,10 +61,11 @@ struct PreferencesSection: Hashable, Identifiable { ] if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { + let subscriptionManager = Application.appDelegate.subscriptionManager + let platform = subscriptionManager.currentEnvironment.purchasePlatform + var shouldHidePrivacyProDueToNoProducts = platform == .appStore && subscriptionManager.canPurchase == false - var shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false - - if AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).isUserAuthenticated { + if subscriptionManager.accountManager.isUserAuthenticated { shouldHidePrivacyProDueToNoProducts = false } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index 1bcd647e37..db5a483437 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -42,7 +42,7 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent], privacyConfigurationManager: PrivacyConfigurationManaging, syncService: DDGSyncing, - vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility() + vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager) ) { self.loadSections = loadSections self.tabSwitcherTabs = tabSwitcherTabs @@ -77,7 +77,7 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, syncService: DDGSyncing, - vpnVisibility: NetworkProtectionFeatureVisibility = DefaultNetworkProtectionVisibility(), + vpnVisibility: NetworkProtectionFeatureVisibility, includeDuckPlayer: Bool, userDefaults: UserDefaults = .netP ) { diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift index 1316d6cf85..b5e8273c17 100644 --- a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -59,7 +59,7 @@ final class VPNPreferencesModel: ObservableObject { private var onboardingStatus: OnboardingStatus { didSet { - showUninstallVPN = DefaultNetworkProtectionVisibility().isInstalled + showUninstallVPN = DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager).isInstalled } } diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 350eb84f96..55833aea85 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -138,7 +138,8 @@ enum Preferences { WindowControllersManager.shared.showTab(with: .dataBrokerProtection) case .openITR: PixelKit.fire(PrivacyProPixel.privacyProIdentityRestorationSettings) - WindowControllersManager.shared.showTab(with: .identityTheftRestoration(.identityTheftRestoration)) + let url = Application.appDelegate.subscriptionManager.url(for: .identityTheftRestoration) + WindowControllersManager.shared.showTab(with: .identityTheftRestoration(url)) case .iHaveASubscriptionClick: PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseClick) case .activateAddEmailClick: @@ -169,7 +170,8 @@ enum Preferences { return } - await SubscriptionAppStoreRestorer.restoreAppStoreSubscription(mainViewController: mainViewController, windowController: windowControllerManager) + let subscriptionAppStoreRestorer = SubscriptionAppStoreRestorer(subscriptionManager: Application.appDelegate.subscriptionManager) + await subscriptionAppStoreRestorer.restoreAppStoreSubscription(mainViewController: mainViewController, windowController: windowControllerManager) } } }, @@ -179,7 +181,7 @@ enum Preferences { return PreferencesSubscriptionModel(openURLHandler: openURL, userEventHandler: handleUIEvent, sheetActionHandler: sheetActionHandler, - subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + subscriptionManager: Application.appDelegate.subscriptionManager) } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index 37d63c320f..967bc49768 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -34,7 +34,9 @@ final class PreferencesViewController: NSViewController { private var bitwardenManager: BWManagement = BWManager.shared init(syncService: DDGSyncing, duckPlayer: DuckPlayer = DuckPlayer.shared) { - model = PreferencesSidebarModel(syncService: syncService, includeDuckPlayer: duckPlayer.isAvailable) + model = PreferencesSidebarModel(syncService: syncService, + vpnVisibility: DefaultNetworkProtectionVisibility(subscriptionManager: Application.appDelegate.subscriptionManager), + includeDuckPlayer: duckPlayer.isAvailable) super.init(nibName: nil, bundle: nil) } diff --git a/DuckDuckGo/Subscription/DataBrokerProtectionSettings+Environment.swift b/DuckDuckGo/Subscription/DataBrokerProtectionSettings+Environment.swift new file mode 100644 index 0000000000..48c1c54f2c --- /dev/null +++ b/DuckDuckGo/Subscription/DataBrokerProtectionSettings+Environment.swift @@ -0,0 +1,34 @@ +// +// DataBrokerProtectionSettings+Environment.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import DataBrokerProtection +import Subscription + +public extension DataBrokerProtectionSettings { + + /// Align VPN environment to the Subscription environment + func alignTo(subscriptionEnvironment: SubscriptionEnvironment) { + switch subscriptionEnvironment.serviceEnvironment { + case .production: + self.selectedEnvironment = .production + case .staging: + self.selectedEnvironment = .staging + } + } +} diff --git a/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift b/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift index 73e6407c28..24929319ea 100644 --- a/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift +++ b/DuckDuckGo/Subscription/DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift @@ -17,12 +17,14 @@ // import Foundation +import AppKit import Subscription import BrowserServicesKit extension DefaultSubscriptionFeatureAvailability { + convenience init() { self.init(privacyConfigurationManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, - purchasePlatform: SubscriptionPurchaseEnvironment.current) + purchasePlatform: Application.appDelegate.subscriptionManager.currentEnvironment.purchasePlatform) } } diff --git a/DuckDuckGo/Subscription/SubscriptionEnvironment+Default.swift b/DuckDuckGo/Subscription/SubscriptionEnvironment+Default.swift new file mode 100644 index 0000000000..fdb1353662 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionEnvironment+Default.swift @@ -0,0 +1,48 @@ +// +// SubscriptionEnvironment+Default.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription + +extension SubscriptionEnvironment { + + public static var `default`: SubscriptionEnvironment { +#if APPSTORE || !STRIPE + let platform: SubscriptionEnvironment.PurchasePlatform = .appStore +#else + let platform: SubscriptionEnvironment.PurchasePlatform = .stripe +#endif + +#if ALPHA || DEBUG + let environment: SubscriptionEnvironment.ServiceEnvironment = .staging +#else + let environment: SubscriptionEnvironment.ServiceEnvironment = .production +#endif + return SubscriptionEnvironment(serviceEnvironment: environment, purchasePlatform: platform) + } +} + +extension SubscriptionManager { + + static public func getSavedOrDefaultEnvironment(userDefaults: UserDefaults) -> SubscriptionEnvironment { + if let savedEnvironment = loadEnvironmentFrom(userDefaults: userDefaults) { + return savedEnvironment + } + return SubscriptionEnvironment.default + } +} diff --git a/DuckDuckGo/Subscription/SubscriptionManager+StandardConfiguration.swift b/DuckDuckGo/Subscription/SubscriptionManager+StandardConfiguration.swift new file mode 100644 index 0000000000..552b8e28b7 --- /dev/null +++ b/DuckDuckGo/Subscription/SubscriptionManager+StandardConfiguration.swift @@ -0,0 +1,57 @@ +// +// SubscriptionManager+StandardConfiguration.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Subscription +import Common + +extension SubscriptionManager { + + // Init the SubscriptionManager using the standard dependencies and configuration, to be used only in the dependencies tree root + public convenience init() { + // MARK: - Configure Subscription + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! + let subscriptionEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + + let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, + key: UserDefaultsCacheKey.subscriptionEntitlements, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) + let subscriptionService = SubscriptionService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let authService = AuthService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let accountManager = AccountManager(accessTokenStorage: accessTokenStorage, + entitlementsCache: entitlementsCache, + subscriptionService: subscriptionService, + authService: authService) + + if #available(macOS 12.0, *) { + let storePurchaseManager = StorePurchaseManager() + self.init(storePurchaseManager: storePurchaseManager, + accountManager: accountManager, + subscriptionService: subscriptionService, + authService: authService, + subscriptionEnvironment: subscriptionEnvironment) + } else { + self.init(accountManager: accountManager, + subscriptionService: subscriptionService, + authService: authService, + subscriptionEnvironment: subscriptionEnvironment) + } + } +} diff --git a/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift index 66e6aefa04..2e9c3d1f62 100644 --- a/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift +++ b/DuckDuckGo/Subscription/SubscriptionRedirectManager.swift @@ -25,10 +25,20 @@ protocol SubscriptionRedirectManager: AnyObject { } final class PrivacyProSubscriptionRedirectManager: SubscriptionRedirectManager { + private let featureAvailabiltyProvider: () -> Bool + private let subscriptionEnvironment: SubscriptionEnvironment + private let canPurchase: () -> Bool + private let baseURL: URL - init(featureAvailabiltyProvider: @escaping @autoclosure () -> Bool = DefaultSubscriptionFeatureAvailability().isFeatureAvailable) { + init(featureAvailabiltyProvider: @escaping @autoclosure () -> Bool = DefaultSubscriptionFeatureAvailability().isFeatureAvailable, + subscriptionEnvironment: SubscriptionEnvironment, + baseURL: URL, + canPurchase: @escaping () -> Bool) { self.featureAvailabiltyProvider = featureAvailabiltyProvider + self.subscriptionEnvironment = subscriptionEnvironment + self.canPurchase = canPurchase + self.baseURL = baseURL } func redirectURL(for url: URL) -> URL? { @@ -36,15 +46,14 @@ final class PrivacyProSubscriptionRedirectManager: SubscriptionRedirectManager { if url.pathComponents == URL.privacyPro.pathComponents { let isFeatureAvailable = featureAvailabiltyProvider() - let shouldHidePrivacyProDueToNoProducts = SubscriptionPurchaseEnvironment.current == .appStore && SubscriptionPurchaseEnvironment.canPurchase == false + let shouldHidePrivacyProDueToNoProducts = subscriptionEnvironment.purchasePlatform == .appStore && canPurchase() == false let isPurchasePageRedirectActive = isFeatureAvailable && !shouldHidePrivacyProDueToNoProducts // Redirect the `/pro` URL to `/subscriptions` URL. If there are any query items in the original URL it appends to the `/subscriptions` URL. - return isPurchasePageRedirectActive ? URL.subscriptionBaseURL.addingQueryItems(from: url) : nil + return isPurchasePageRedirectActive ? baseURL.addingQueryItems(from: url) : nil } return nil } - } private extension URL { diff --git a/DuckDuckGo/Subscription/VPNSettings+Environment.swift b/DuckDuckGo/Subscription/VPNSettings+Environment.swift new file mode 100644 index 0000000000..4e046637ed --- /dev/null +++ b/DuckDuckGo/Subscription/VPNSettings+Environment.swift @@ -0,0 +1,34 @@ +// +// VPNSettings+Environment.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import NetworkProtection +import Subscription + +public extension VPNSettings { + + /// Align VPN environment to the Subscription environment + func alignTo(subscriptionEnvironment: SubscriptionEnvironment) { + switch subscriptionEnvironment.serviceEnvironment { + case .production: + self.selectedEnvironment = .production + case .staging: + self.selectedEnvironment = .staging + } + } +} diff --git a/DuckDuckGo/Sync/SyncDebugMenu.swift b/DuckDuckGo/Sync/SyncDebugMenu.swift index c5ff802e84..db74c0125d 100644 --- a/DuckDuckGo/Sync/SyncDebugMenu.swift +++ b/DuckDuckGo/Sync/SyncDebugMenu.swift @@ -16,7 +16,7 @@ // limitations under the License. // -import Foundation +import AppKit import DDGSync import Bookmarks diff --git a/DuckDuckGo/Tab/Model/Tab+Navigation.swift b/DuckDuckGo/Tab/Model/Tab+Navigation.swift index 2b86d68118..5d5ac5606f 100644 --- a/DuckDuckGo/Tab/Model/Tab+Navigation.swift +++ b/DuckDuckGo/Tab/Model/Tab+Navigation.swift @@ -71,7 +71,7 @@ extension Tab: NavigationResponder { // add extra headers to SERP requests .struct(SerpHeadersNavigationResponder()), - .struct(RedirectNavigationResponder()), + .struct(redirectNavigationResponder), // ensure Content Blocking Rules are applied before navigation .weak(nullable: self.contentBlockingAndSurrogates), @@ -107,4 +107,14 @@ extension Tab: NavigationResponder { } } + var redirectNavigationResponder: RedirectNavigationResponder { + let subscriptionManager = Application.appDelegate.subscriptionManager + let redirectManager = PrivacyProSubscriptionRedirectManager(subscriptionEnvironment: subscriptionManager.currentEnvironment, + baseURL: subscriptionManager.url(for: .baseURL), + canPurchase: { + subscriptionManager.canPurchase + }) + return RedirectNavigationResponder(redirectManager: redirectManager) + } + } diff --git a/DuckDuckGo/Tab/Model/TabContent.swift b/DuckDuckGo/Tab/Model/TabContent.swift index 369a036ba2..b213cc2ba5 100644 --- a/DuckDuckGo/Tab/Model/TabContent.swift +++ b/DuckDuckGo/Tab/Model/TabContent.swift @@ -117,12 +117,16 @@ extension TabContent { } if let url { - if url.isChild(of: URL.subscriptionBaseURL) { - if SubscriptionPurchaseEnvironment.currentServiceEnvironment == .staging, url.getParameter(named: "environment") == nil { + let subscriptionManager = Application.appDelegate.subscriptionManager + let environment = subscriptionManager.currentEnvironment.serviceEnvironment + let subscriptionBaseURL = subscriptionManager.url(for: .baseURL) + let identityTheftRestorationURL = subscriptionManager.url(for: .identityTheftRestoration) + if url.isChild(of: subscriptionBaseURL) { + if environment == .staging, url.getParameter(named: "environment") == nil { return .subscription(url.appendingParameter(name: "environment", value: "staging")) } return .subscription(url) - } else if url.isChild(of: URL.identityTheftRestoration) { + } else if url.isChild(of: identityTheftRestorationURL) { return .identityTheftRestoration(url) } } diff --git a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift index 81f400302c..5e57ccab46 100644 --- a/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift +++ b/DuckDuckGo/Tab/Navigation/RedirectNavigationResponder.swift @@ -23,7 +23,7 @@ struct RedirectNavigationResponder: NavigationResponder { private let redirectManager: SubscriptionRedirectManager - init(redirectManager: SubscriptionRedirectManager = PrivacyProSubscriptionRedirectManager()) { + init(redirectManager: SubscriptionRedirectManager) { self.redirectManager = redirectManager } @@ -37,4 +37,19 @@ struct RedirectNavigationResponder: NavigationResponder { } } +// private func redirectURL(for url: URL) -> URL? { +// guard url.isPart(ofDomain: "duckduckgo.com") else { return nil } +// +// if url.pathComponents == URL.privacyPro.pathComponents { +// let isFeatureAvailable = DefaultSubscriptionFeatureAvailability().isFeatureAvailable +// let subscriptionManager = Application.appDelegate.subscriptionManager +// let platform = subscriptionManager.currentEnvironment.purchasePlatform +// let shouldHidePrivacyProDueToNoProducts = platform == .appStore && subscriptionManager.canPurchase == false +// let isPurchasePageRedirectActive = isFeatureAvailable && !shouldHidePrivacyProDueToNoProducts +// let url = SubscriptionURL.baseURL.subscriptionURL(environment: subscriptionManager.currentEnvironment.serviceEnvironment) +// return isPurchasePageRedirectActive ? url : nil +// } +// +// return nil +// } } diff --git a/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift index a613d64566..a1ad9e3f9d 100644 --- a/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/IdentityTheftRestorationPagesUserScript.swift @@ -92,7 +92,7 @@ final class IdentityTheftRestorationPagesFeature: Subfeature { } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).accessToken { + if let accessToken = await Application.appDelegate.subscriptionManager.accountManager.accessToken { return ["token": accessToken] } else { return [String: String]() diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift index 35661ff131..68a02e6372 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionAppStoreRestorer.swift @@ -16,7 +16,7 @@ // limitations under the License. // -import Foundation +import AppKit import Subscription import SubscriptionUI import enum StoreKit.StoreKitError @@ -25,8 +25,16 @@ import PixelKit @available(macOS 12.0, *) struct SubscriptionAppStoreRestorer { - // swiftlint:disable:next cyclomatic_complexity - static func restoreAppStoreSubscription(mainViewController: MainViewController, windowController: MainWindowController) async { + private let subscriptionManager: SubscriptionManaging + @MainActor var window: NSWindow? { WindowControllersManager.shared.lastKeyMainWindowController?.window } + let subscriptionErrorReporter = SubscriptionErrorReporter() + + public init(subscriptionManager: SubscriptionManaging) { + self.subscriptionManager = subscriptionManager + } + + // swiftlint:disable:next cyclomatic_complexity function_body_length + func restoreAppStoreSubscription(mainViewController: MainViewController, windowController: MainWindowController) async { let progressViewController = await ProgressViewController(title: UserText.restoringSubscriptionTitle) defer { @@ -39,7 +47,7 @@ struct SubscriptionAppStoreRestorer { mainViewController.presentAsSheet(progressViewController) } - let syncResult = await PurchaseManager.shared.syncAppleIDAccount() + let syncResult = await subscriptionManager.storePurchaseManager().syncAppleIDAccount() switch syncResult { case .success: @@ -63,7 +71,8 @@ struct SubscriptionAppStoreRestorer { } } - let result = await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let appStoreRestoreFlow = AppStoreRestoreFlow(subscriptionManager: subscriptionManager) + let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() switch result { case .success: @@ -77,15 +86,56 @@ struct SubscriptionAppStoreRestorer { switch error { case .missingAccountOrTransactions: - SubscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) - await windowController.showSubscriptionNotFoundAlert() + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) + await showSubscriptionNotFoundAlert() case .subscriptionExpired: - SubscriptionErrorReporter.report(subscriptionActivationError: .subscriptionExpired) - await windowController.showSubscriptionInactiveAlert() + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionExpired) + await showSubscriptionInactiveAlert() case .pastTransactionAuthenticationError, .failedToObtainAccessToken, .failedToFetchAccountDetails, .failedToFetchSubscriptionDetails: - SubscriptionErrorReporter.report(subscriptionActivationError: .generalError) - await windowController.showSomethingWentWrongAlert() + subscriptionErrorReporter.report(subscriptionActivationError: .generalError) + await showSomethingWentWrongAlert() } } } } + +@available(macOS 12.0, *) +extension SubscriptionAppStoreRestorer { + + /* + WARNING: DUPLICATED CODE + This code will be moved as part of https://app.asana.com/0/0/1207157941206686/f + */ + + // MARK: - UI interactions + + @MainActor + func showSomethingWentWrongAlert() { + PixelKit.fire(PrivacyProPixel.privacyProPurchaseFailure, frequency: .dailyAndCount) + guard let window else { return } + + window.show(.somethingWentWrongAlert()) + } + + @MainActor + func showSubscriptionNotFoundAlert() { + guard let window else { return } + + window.show(.subscriptionNotFoundAlert(), firstButtonAction: { + let url = subscriptionManager.url(for: .purchase) + WindowControllersManager.shared.showTab(with: .subscription(url)) + PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) + }) + } + + @MainActor + func showSubscriptionInactiveAlert() { + guard let window else { return } + + window.show(.subscriptionInactiveAlert(), firstButtonAction: { + let url = subscriptionManager.url(for: .purchase) + WindowControllersManager.shared.showTab(with: .subscription(url)) + PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) + }) + } +} diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift index 438997c0f7..874e5ab9cb 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionErrorReporter.swift @@ -40,7 +40,7 @@ enum SubscriptionError: Error { struct SubscriptionErrorReporter { // swiftlint:disable:next cyclomatic_complexity - static func report(subscriptionActivationError: SubscriptionError) { + func report(subscriptionActivationError: SubscriptionError) { os_log(.error, log: .subscription, "Subscription purchase error: %{public}s", subscriptionActivationError.localizedDescription) diff --git a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift index f0e7525820..32f819fead 100644 --- a/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/Subscription/SubscriptionPagesUserScript.swift @@ -31,9 +31,7 @@ public extension Notification.Name { static let subscriptionPageCloseAndOpenPreferences = Notification.Name("com.duckduckgo.subscriptionPage.CloseAndOpenPreferences") } -/// /// The user script that will be the broker for all subscription features -/// public final class SubscriptionPagesUserScript: NSObject, UserScript, UserScriptMessaging { public var source: String = "" @@ -66,16 +64,14 @@ extension SubscriptionPagesUserScript: WKScriptMessageHandlerWithReply { } } -// MARK: - Fallback for macOS 10.15 +/// Fallback for macOS 10.15 extension SubscriptionPagesUserScript: WKScriptMessageHandler { public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { // unsupported } } -/// /// Use Subscription sub-feature -/// final class SubscriptionPagesUseSubscriptionFeature: Subfeature { weak var broker: UserScriptMessageBroker? var featureName = "useSubscription" @@ -84,18 +80,29 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { .exact(hostname: "abrown.duckduckgo.com") ]) - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + @MainActor + var window: NSWindow? { + WindowControllersManager.shared.lastKeyMainWindowController?.window + } + let subscriptionManager: SubscriptionManaging + var accountManager: AccountManaging { subscriptionManager.accountManager } + var subscriptionPlatform: SubscriptionEnvironment.PurchasePlatform { subscriptionManager.currentEnvironment.purchasePlatform } + + let stripePurchaseFlow: StripePurchaseFlow + let subscriptionErrorReporter = SubscriptionErrorReporter() + let subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler + + public init(subscriptionManager: SubscriptionManaging, + subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler()) { + self.subscriptionManager = subscriptionManager + self.stripePurchaseFlow = StripePurchaseFlow(subscriptionManager: subscriptionManager) + self.subscriptionSuccessPixelHandler = subscriptionSuccessPixelHandler + } func with(broker: UserScriptMessageBroker) { self.broker = broker } - private let subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler - - init(subscriptionSuccessPixelHandler: SubscriptionAttributionPixelHandler = PrivacyProSubscriptionAttributionPixelHandler()) { - self.subscriptionSuccessPixelHandler = subscriptionSuccessPixelHandler - } - struct Handlers { static let getSubscription = "getSubscription" static let setSubscription = "setSubscription" @@ -189,17 +196,13 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { func getSubscriptionOptions(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard DefaultSubscriptionFeatureAvailability().isSubscriptionPurchaseAllowed else { return SubscriptionOptions.empty } - if SubscriptionPurchaseEnvironment.current == .appStore { + switch subscriptionPlatform { + case .appStore: if #available(macOS 12.0, *) { - switch await AppStorePurchaseFlow.subscriptionOptions() { - case .success(let subscriptionOptions): - return subscriptionOptions - case .failure: - break - } + return await subscriptionManager.storePurchaseManager().subscriptionOptions() } - } else if SubscriptionPurchaseEnvironment.current == .stripe { - switch await StripePurchaseFlow.subscriptionOptions() { + case .stripe: + switch await stripePurchaseFlow.subscriptionOptions() { case .success(let subscriptionOptions): return subscriptionOptions case .failure: @@ -210,8 +213,6 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { return SubscriptionOptions.empty } - let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) - // swiftlint:disable:next function_body_length cyclomatic_complexity func subscriptionSelected(params: Any, original: WKScriptMessage) async throws -> Encodable? { PixelKit.fire(PrivacyProPixel.privacyProPurchaseAttempt, frequency: .dailyAndCount) @@ -223,8 +224,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { // Extract the origin from the webview URL to use for attribution pixel. subscriptionSuccessPixelHandler.origin = await originFrom(originalMessage: message) - - if SubscriptionPurchaseEnvironment.current == .appStore { + if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, *) { let mainViewController = await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController let progressViewController = await ProgressViewController(title: UserText.purchasingSubscriptionTitle) @@ -237,7 +237,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { guard let subscriptionSelection: SubscriptionSelection = DecodableHelper.decode(from: params) else { assertionFailure("SubscriptionPagesUserScript: expected JSON representation of SubscriptionSelection") - SubscriptionErrorReporter.report(subscriptionActivationError: .generalError) + subscriptionErrorReporter.report(subscriptionActivationError: .generalError) return nil } @@ -246,41 +246,44 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await mainViewController?.presentAsSheet(progressViewController) // Check for active subscriptions - if await PurchaseManager.hasActiveSubscription() { + if await subscriptionManager.storePurchaseManager().hasActiveSubscription() { PixelKit.fire(PrivacyProPixel.privacyProRestoreAfterPurchaseAttempt) os_log(.info, log: .subscription, "[Purchase] Found active subscription during purchase") - SubscriptionErrorReporter.report(subscriptionActivationError: .hasActiveSubscription) - await WindowControllersManager.shared.lastKeyMainWindowController?.showSubscriptionFoundAlert(originalMessage: message) + subscriptionErrorReporter.report(subscriptionActivationError: .hasActiveSubscription) + await showSubscriptionFoundAlert(originalMessage: message) return nil } let emailAccessToken = try? EmailManager().getToken() let purchaseTransactionJWS: String + let appStorePurchaseFlow = AppStorePurchaseFlow(subscriptionManager: subscriptionManager) os_log(.info, log: .subscription, "[Purchase] Purchasing") - switch await AppStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken, subscriptionAppGroup: subscriptionAppGroup) { + switch await appStorePurchaseFlow.purchaseSubscription(with: subscriptionSelection.id, emailAccessToken: emailAccessToken) { case .success(let transactionJWS): purchaseTransactionJWS = transactionJWS case .failure(let error): switch error { case .noProductsFound: - SubscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) case .activeSubscriptionAlreadyPresent: - SubscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) + subscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) case .authenticatingWithTransactionFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .generalError) + subscriptionErrorReporter.report(subscriptionActivationError: .generalError) case .accountCreationFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) + subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) case .purchaseFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) + subscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) case .cancelledByUser: - SubscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) + subscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) case .missingEntitlements: - SubscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) + subscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) + case .internalError: + assertionFailure("Internal error") } if error != .cancelledByUser { - await WindowControllersManager.shared.lastKeyMainWindowController?.showSomethingWentWrongAlert() + await showSomethingWentWrongAlert() } await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) return nil @@ -290,7 +293,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { os_log(.info, log: .subscription, "[Purchase] Completing purchase") - switch await AppStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS, subscriptionAppGroup: subscriptionAppGroup) { + switch await appStorePurchaseFlow.completeSubscriptionPurchase(with: purchaseTransactionJWS) { case .success(let purchaseUpdate): os_log(.info, log: .subscription, "[Purchase] Purchase complete") PixelKit.fire(PrivacyProPixel.privacyProPurchaseSuccess, frequency: .dailyAndCount) @@ -300,44 +303,46 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { case .failure(let error): switch error { case .noProductsFound: - SubscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) case .activeSubscriptionAlreadyPresent: - SubscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) + subscriptionErrorReporter.report(subscriptionActivationError: .activeSubscriptionAlreadyPresent) case .authenticatingWithTransactionFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .generalError) + subscriptionErrorReporter.report(subscriptionActivationError: .generalError) case .accountCreationFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) + subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) case .purchaseFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) + subscriptionErrorReporter.report(subscriptionActivationError: .purchaseFailed) case .cancelledByUser: - SubscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) + subscriptionErrorReporter.report(subscriptionActivationError: .cancelledByUser) case .missingEntitlements: - SubscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) + subscriptionErrorReporter.report(subscriptionActivationError: .missingEntitlements) DispatchQueue.main.async { NotificationCenter.default.post(name: .subscriptionPageCloseAndOpenPreferences, object: self) } return nil + case .internalError: + assertionFailure("Internal error") } await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "completed")) } } - } else if SubscriptionPurchaseEnvironment.current == .stripe { + } else if subscriptionPlatform == .stripe { let emailAccessToken = try? EmailManager().getToken() - let result = await StripePurchaseFlow.prepareSubscriptionPurchase(emailAccessToken: emailAccessToken, subscriptionAppGroup: subscriptionAppGroup) + let result = await stripePurchaseFlow.prepareSubscriptionPurchase(emailAccessToken: emailAccessToken) switch result { case .success(let success): await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: success) case .failure(let error): - await WindowControllersManager.shared.lastKeyMainWindowController?.showSomethingWentWrongAlert() + await showSomethingWentWrongAlert() switch error { case .noProductsFound: - SubscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) + subscriptionErrorReporter.report(subscriptionActivationError: .subscriptionNotFound) case .accountCreationFailed: - SubscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) + subscriptionErrorReporter.report(subscriptionActivationError: .accountCreationFailed) } await pushPurchaseUpdate(originalMessage: message, purchaseUpdate: PurchaseUpdate(type: "canceled")) } @@ -358,7 +363,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let actionHandlers = SubscriptionAccessActionHandlers(restorePurchases: { if #available(macOS 12.0, *) { Task { @MainActor in - await SubscriptionAppStoreRestorer.restoreAppStoreSubscription(mainViewController: mainViewController, windowController: windowControllerManager) + let subscriptionAppStoreRestorer = SubscriptionAppStoreRestorer(subscriptionManager: self.subscriptionManager) + await subscriptionAppStoreRestorer.restoreAppStoreSubscription(mainViewController: mainViewController, windowController: windowControllerManager) message.webView?.reload() } } @@ -375,8 +381,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } }) - let vc = await SubscriptionAccessViewController(accountManager: accountManager, actionHandlers: actionHandlers, subscriptionAppGroup: subscriptionAppGroup) - await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.presentAsSheet(vc) + let subscriptionAccessViewController = await SubscriptionAccessViewController(subscriptionManager: subscriptionManager, actionHandlers: actionHandlers) + await WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.presentAsSheet(subscriptionAccessViewController) return nil } @@ -414,7 +420,8 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { await WindowControllersManager.shared.showTab(with: .dataBrokerProtection) case .identityTheftRestoration: PixelKit.fire(PrivacyProPixel.privacyProWelcomeIdentityRestoration, frequency: .unique) - await WindowControllersManager.shared.showTab(with: .identityTheftRestoration(.identityTheftRestoration)) + let url = subscriptionManager.url(for: .identityTheftRestoration) + await WindowControllersManager.shared.showTab(with: .identityTheftRestoration(url)) } return nil @@ -425,7 +432,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { let progressViewController = await ProgressViewController(title: UserText.completingPurchaseTitle) await mainViewController?.presentAsSheet(progressViewController) - await StripePurchaseFlow.completeSubscriptionPurchase(subscriptionAppGroup: subscriptionAppGroup) + await stripePurchaseFlow.completeSubscriptionPurchase() await mainViewController?.dismiss(progressViewController) PixelKit.fire(PrivacyProPixel.privacyProPurchaseStripeSuccess, frequency: .dailyAndCount) @@ -461,7 +468,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } func getAccessToken(params: Any, original: WKScriptMessage) async throws -> Encodable? { - if let accessToken = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).accessToken { + if let accessToken = accountManager.accessToken { return ["token": accessToken] } else { return [String: String]() @@ -495,7 +502,14 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature { } } -extension MainWindowController { +extension SubscriptionPagesUseSubscriptionFeature { + + /* + WARNING: + This code will be moved as part of https://app.asana.com/0/0/1207157941206686/f + */ + + // MARK: - UI interactions @MainActor func showSomethingWentWrongAlert() { @@ -510,7 +524,8 @@ extension MainWindowController { guard let window else { return } window.show(.subscriptionNotFoundAlert(), firstButtonAction: { - WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) + let url = self.subscriptionManager.url(for: .purchase) + WindowControllersManager.shared.showTab(with: .subscription(url)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) }) } @@ -520,7 +535,8 @@ extension MainWindowController { guard let window else { return } window.show(.subscriptionInactiveAlert(), firstButtonAction: { - WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) + let url = self.subscriptionManager.url(for: .purchase) + WindowControllersManager.shared.showTab(with: .subscription(url)) PixelKit.fire(PrivacyProPixel.privacyProOfferScreenImpression) }) } @@ -532,10 +548,10 @@ extension MainWindowController { window.show(.subscriptionFoundAlert(), firstButtonAction: { if #available(macOS 12.0, *) { Task { - let result = await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let appStoreRestoreFlow = AppStoreRestoreFlow(subscriptionManager: self.subscriptionManager) + let result = await appStoreRestoreFlow.restoreAccountFromPastPurchase() switch result { - case .success: - PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseStoreSuccess, frequency: .dailyAndCount) + case .success: PixelKit.fire(PrivacyProPixel.privacyProRestorePurchaseStoreSuccess, frequency: .dailyAndCount) case .failure: break } originalMessage.webView?.reload() diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 1bc9443977..2668a5fde6 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -50,6 +50,7 @@ final class UserScripts: UserScriptsProvider { clickToLoadScript = ClickToLoadUserScript(scriptSourceProvider: sourceProvider) contentBlockerRulesScript = ContentBlockerRulesUserScript(configuration: sourceProvider.contentBlockerRulesConfig!) surrogatesScript = SurrogatesUserScript(configuration: sourceProvider.surrogatesConfig!) + let isGPCEnabled = WebTrackingProtectionPreferences.shared.isGPCEnabled let privacyConfig = sourceProvider.privacyConfigurationManager.privacyConfig let sessionKey = sourceProvider.sessionKey ?? "" @@ -92,7 +93,7 @@ final class UserScripts: UserScriptsProvider { } if DefaultSubscriptionFeatureAvailability().isFeatureAvailable { - subscriptionPagesUserScript.registerSubfeature(delegate: SubscriptionPagesUseSubscriptionFeature()) + subscriptionPagesUserScript.registerSubfeature(delegate: SubscriptionPagesUseSubscriptionFeature(subscriptionManager: Application.appDelegate.subscriptionManager)) userScripts.append(subscriptionPagesUserScript) identityTheftRestorationPagesUserScript.registerSubfeature(delegate: IdentityTheftRestorationPagesFeature()) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift index fe8b8258e2..590d958513 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift @@ -40,7 +40,7 @@ final class VPNFeedbackFormViewController: NSViewController { private var cancellables = Set() init() { - self.viewModel = VPNFeedbackFormViewModel() + self.viewModel = VPNFeedbackFormViewModel(metadataCollector: DefaultVPNMetadataCollector(accountManager: Application.appDelegate.subscriptionManager.accountManager)) super.init(nibName: nil, bundle: nil) self.viewModel.delegate = self } diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift index 7363d0d817..70aeb3e110 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift @@ -67,7 +67,8 @@ final class VPNFeedbackFormViewModel: ObservableObject { private let metadataCollector: VPNMetadataCollector private let feedbackSender: VPNFeedbackSender - init(metadataCollector: VPNMetadataCollector = DefaultVPNMetadataCollector(), feedbackSender: VPNFeedbackSender = DefaultVPNFeedbackSender()) { + init(metadataCollector: VPNMetadataCollector, + feedbackSender: VPNFeedbackSender = DefaultVPNFeedbackSender()) { self.viewState = .feedbackPending self.selectedFeedbackCategory = .landingPage diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index 700149aad7..e69c0c4a41 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -121,11 +121,14 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { private let statusReporter: NetworkProtectionStatusReporter private let ipcClient: TunnelControllerIPCClient private let defaults: UserDefaults + private let accountManager: AccountManaging + private let settings: VPNSettings - init(defaults: UserDefaults = .netP) { + init(defaults: UserDefaults = .netP, + accountManager: AccountManaging) { let ipcClient = TunnelControllerIPCClient() ipcClient.register() - + self.accountManager = accountManager self.ipcClient = ipcClient self.defaults = defaults @@ -141,6 +144,16 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { // Force refresh just in case. A refresh is requested when the IPC client is created, but distributed notifications don't guarantee delivery // so we'll play it safe and add one more attempt. self.statusReporter.forceRefresh() + + self.settings = VPNSettings(defaults: defaults) + updateSettings() + } + + func updateSettings() { + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! + let subscriptionEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + settings.alignTo(subscriptionEnvironment: subscriptionEnvironment) } @MainActor @@ -287,8 +300,6 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } func collectVPNSettingsState() -> VPNMetadata.VPNSettingsState { - let settings = VPNSettings(defaults: defaults) - return .init( connectOnLoginEnabled: settings.connectOnLogin, includeAllNetworksEnabled: settings.includeAllNetworks, @@ -302,10 +313,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } func collectPrivacyProInfo() async -> VPNMetadata.PrivacyProInfo { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - let hasVPNEntitlement = (try? await accountManager.hasEntitlement(for: .networkProtection).get()) ?? false - return .init( hasPrivacyProAccount: accountManager.isUserAuthenticated, hasVPNEntitlement: hasVPNEntitlement diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 753f6c3054..a9b92cdac0 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -44,22 +44,22 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation private let privacyConfigurationManager: PrivacyConfigurationManaging private let defaults: UserDefaults - let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) - let accountManager: AccountManager + private let subscriptionManager: SubscriptionManaging init(privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), featureOverrides: WaitlistBetaOverriding = DefaultWaitlistBetaOverrides(), vpnUninstaller: VPNUninstalling = VPNUninstaller(), defaults: UserDefaults = .netP, - log: OSLog = .networkProtection) { + log: OSLog = .networkProtection, + subscriptionManager: SubscriptionManaging) { self.privacyConfigurationManager = privacyConfigurationManager self.networkProtectionFeatureActivation = networkProtectionFeatureActivation self.vpnUninstaller = vpnUninstaller self.featureOverrides = featureOverrides self.defaults = defaults - self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) + self.subscriptionManager = subscriptionManager } var isInstalled: Bool { @@ -76,7 +76,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { return false } - switch await accountManager.hasEntitlement(for: .networkProtection) { + switch await subscriptionManager.accountManager.hasEntitlement(for: .networkProtection) { case .success(let hasEntitlement): return hasEntitlement case .failure(let error): @@ -93,8 +93,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { guard subscriptionFeatureAvailability.isFeatureAvailable else { return false } - - return accountManager.isUserAuthenticated + return subscriptionManager.accountManager.isUserAuthenticated } /// We've had to add this method because accessing the singleton in app delegate is crashing the integration tests. @@ -106,7 +105,7 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// Returns whether the VPN should be uninstalled automatically. /// This is only true when the user is not an Easter Egg user, the waitlist test has ended, and the user is onboarded. func shouldUninstallAutomatically() -> Bool { - return subscriptionFeatureAvailability.isFeatureAvailable && !accountManager.isUserAuthenticated && LoginItem.vpnMenu.status.isInstalled + return subscriptionFeatureAvailability.isFeatureAvailable && !subscriptionManager.accountManager.isUserAuthenticated && LoginItem.vpnMenu.status.isInstalled } /// Whether the user is fully onboarded diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift index 594eb0b3d3..b07a7c2796 100644 --- a/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift +++ b/DuckDuckGoDBPBackgroundAgent/DataBrokerAuthenticationManagerBuilder.swift @@ -21,10 +21,10 @@ import DataBrokerProtection import Subscription final public class DataBrokerAuthenticationManagerBuilder { - static func buildAuthenticationManager(redeemUseCase: RedeemUseCase = RedeemUseCase()) -> DataBrokerProtectionAuthenticationManager { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - let subscriptionManager = DataBrokerProtectionSubscriptionManager(accountManager: accountManager, - environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManager()) + + static func buildAuthenticationManager(redeemUseCase: RedeemUseCase = RedeemUseCase(), + subscriptionManager: SubscriptionManaging) -> DataBrokerProtectionAuthenticationManager { + let subscriptionManager = DataBrokerProtectionSubscriptionManager(subscriptionManager: subscriptionManager) return DataBrokerProtectionAuthenticationManager(redeemUseCase: redeemUseCase, subscriptionManager: subscriptionManager) diff --git a/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift new file mode 100644 index 0000000000..1098fda56b --- /dev/null +++ b/DuckDuckGoDBPBackgroundAgent/DataBrokerProtectionBackgroundManager.swift @@ -0,0 +1,116 @@ +// +// DataBrokerProtectionBackgroundManager.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Common +import BrowserServicesKit +import DataBrokerProtection +import PixelKit +import Subscription + +public final class DataBrokerProtectionBackgroundManager { + + private let pixelHandler: EventMapping = DataBrokerProtectionPixelsHandler() + + private let authenticationRepository: AuthenticationRepository = KeychainAuthenticationData() + private let authenticationService: DataBrokerProtectionAuthenticationService = AuthenticationService() + private let authenticationManager: DataBrokerProtectionAuthenticationManaging + private let fakeBrokerFlag: DataBrokerDebugFlag = DataBrokerDebugFlagFakeBroker() + + private lazy var ipcServiceManager = IPCServiceManager(scheduler: scheduler, pixelHandler: pixelHandler) + + lazy var dataManager: DataBrokerProtectionDataManager = { + DataBrokerProtectionDataManager(pixelHandler: pixelHandler, fakeBrokerFlag: fakeBrokerFlag) + }() + + lazy var scheduler: DataBrokerProtectionScheduler = { + let privacyConfigurationManager = PrivacyConfigurationManagingMock() // Forgive me, for I have sinned + let features = ContentScopeFeatureToggles(emailProtection: false, + emailProtectionIncontextSignup: false, + credentialsAutofill: false, + identitiesAutofill: false, + creditCardsAutofill: false, + credentialsSaving: false, + passwordGeneration: false, + inlineIconCredentials: false, + thirdPartyCredentialsProvider: false) + + let sessionKey = UUID().uuidString + let prefs = ContentScopeProperties(gpcEnabled: false, + sessionKey: sessionKey, + featureToggles: features) + + let pixelHandler = DataBrokerProtectionPixelsHandler() + + let userNotificationService = DefaultDataBrokerProtectionUserNotificationService(pixelHandler: pixelHandler) + + return DefaultDataBrokerProtectionScheduler(privacyConfigManager: privacyConfigurationManager, + contentScopeProperties: prefs, + dataManager: dataManager, + notificationCenter: NotificationCenter.default, + pixelHandler: pixelHandler, + authenticationManager: authenticationManager, + userNotificationService: userNotificationService) + }() + + public init(subscriptionManager: SubscriptionManaging) { + let redeemUseCase = RedeemUseCase(authenticationService: authenticationService, + authenticationRepository: authenticationRepository) + self.authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager( + redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) + + _ = ipcServiceManager + } + + public func runOperationsAndStartSchedulerIfPossible() { + pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossible) + + do { + // If there's no saved profile we don't need to start the scheduler + guard (try dataManager.fetchProfile()) != nil else { + pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleNoSavedProfile) + return + } + } catch { + pixelHandler.fire(.generalError(error: error, + functionOccurredIn: "DataBrokerProtectionBackgroundManager.runOperationsAndStartSchedulerIfPossible")) + return + } + + scheduler.runQueuedOperations(showWebView: false) { [weak self] errors in + if let errors = errors { + if let oneTimeError = errors.oneTimeError { + os_log("Error during BackgroundManager runOperationsAndStartSchedulerIfPossible in scheduler.runQueuedOperations(), error: %{public}@", + log: .dataBrokerProtection, + oneTimeError.localizedDescription) + self?.pixelHandler.fire(.generalError(error: oneTimeError, + functionOccurredIn: "DataBrokerProtectionBackgroundManager.runOperationsAndStartSchedulerIfPossible")) + } + if let operationErrors = errors.operationErrors, + operationErrors.count != 0 { + os_log("Operation error(s) during BackgroundManager runOperationsAndStartSchedulerIfPossible in scheduler.runQueuedOperations(), count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + } + return + } + + self?.pixelHandler.fire(.backgroundAgentRunOperationsAndStartSchedulerIfPossibleRunQueuedOperationsCallbackStartScheduler) + self?.scheduler.startScheduler() + } + } +} diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift index 7e2bcebbef..70465069d4 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift @@ -24,10 +24,12 @@ import DataBrokerProtection import BrowserServicesKit import PixelKit import Networking +import Subscription @objc(Application) final class DuckDuckGoDBPBackgroundAgentApplication: NSApplication { - private let _delegate = DuckDuckGoDBPBackgroundAgentAppDelegate() + private let _delegate: DuckDuckGoDBPBackgroundAgentAppDelegate + private let subscriptionManager: SubscriptionManaging override init() { os_log(.error, log: .dbpBackgroundAgent, "🟢 DBP background Agent starting: %{public}d", NSRunningApplication.current.processIdentifier) @@ -65,6 +67,11 @@ final class DuckDuckGoDBPBackgroundAgentApplication: NSApplication { exit(0) } + // Configure Subscription + subscriptionManager = SubscriptionManager() + + _delegate = DuckDuckGoDBPBackgroundAgentAppDelegate(subscriptionManager: subscriptionManager) + super.init() self.delegate = _delegate } @@ -80,19 +87,28 @@ final class DuckDuckGoDBPBackgroundAgentAppDelegate: NSObject, NSApplicationDele private let settings = DataBrokerProtectionSettings() private var cancellables = Set() private var statusBarMenu: StatusBarMenu? + private let subscriptionManager: SubscriptionManaging private var manager: DataBrokerProtectionAgentManager? + init(subscriptionManager: SubscriptionManaging) { + self.subscriptionManager = subscriptionManager + } + @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { os_log("DuckDuckGoAgent started", log: .dbpBackgroundAgent, type: .info) let redeemUseCase = RedeemUseCase(authenticationService: AuthenticationService(), authenticationRepository: KeychainAuthenticationData()) - let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(redeemUseCase: redeemUseCase) + let authenticationManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(redeemUseCase: redeemUseCase, + subscriptionManager: subscriptionManager) manager = DataBrokerProtectionAgentManagerProvider.agentManager(authenticationManager: authenticationManager) manager?.agentFinishedLaunching() setupStatusBarMenu() + + // Aligning the environment with the Subscription one + settings.alignTo(subscriptionEnvironment: subscriptionManager.currentEnvironment) } @MainActor diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 3f277460e5..de78d86b57 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -31,7 +31,9 @@ import Subscription @objc(Application) final class DuckDuckGoVPNApplication: NSApplication { - private let _delegate = DuckDuckGoVPNAppDelegate() + + public let accountManager: AccountManaging + private let _delegate: DuckDuckGoVPNAppDelegate override init() { os_log(.error, log: .networkProtection, "🟢 Status Bar Agent starting: %{public}d", NSRunningApplication.current.processIdentifier) @@ -42,14 +44,31 @@ final class DuckDuckGoVPNApplication: NSApplication { exit(0) } + // MARK: - Configure Subscription + let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! + let subscriptionEnvironment = SubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) + let subscriptionService = SubscriptionService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let authService = AuthService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) + let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, + key: UserDefaultsCacheKey.subscriptionEntitlements, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) + accountManager = AccountManager(accessTokenStorage: accessTokenStorage, + entitlementsCache: entitlementsCache, + subscriptionService: subscriptionService, + authService: authService) + + _delegate = DuckDuckGoVPNAppDelegate(bouncer: NetworkProtectionBouncer(accountManager: accountManager), + accountManager: accountManager, + accessTokenStorage: accessTokenStorage, + subscriptionEnvironment: subscriptionEnvironment) super.init() setupPixelKit() self.delegate = _delegate #if DEBUG - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - if let token = accountManager.accessToken { os_log(.error, log: .networkProtection, "🟢 VPN Agent found token: %{public}d", token) } else { @@ -104,7 +123,20 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private static let recentThreshold: TimeInterval = 5.0 private let appLauncher = AppLauncher() - private let bouncer = NetworkProtectionBouncer() + private let bouncer: NetworkProtectionBouncer + private let accountManager: AccountManaging + private let accessTokenStorage: SubscriptionTokenKeychainStorage + + public init(bouncer: NetworkProtectionBouncer, + accountManager: AccountManaging, + accessTokenStorage: SubscriptionTokenKeychainStorage, + subscriptionEnvironment: SubscriptionEnvironment) { + self.bouncer = bouncer + self.accountManager = accountManager + self.accessTokenStorage = accessTokenStorage + self.tunnelSettings = VPNSettings(defaults: .netP) + self.tunnelSettings.alignTo(subscriptionEnvironment: subscriptionEnvironment) + } private var cancellables = Set() @@ -126,7 +158,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { #endif } - private lazy var tunnelSettings = VPNSettings(defaults: .netP) + private let tunnelSettings: VPNSettings private lazy var userDefaults = UserDefaults.netP private lazy var proxySettings = TransparentProxySettings(defaults: .netP) @@ -187,7 +219,8 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { networkExtensionBundleID: tunnelExtensionBundleID, networkExtensionController: networkExtensionController, settings: tunnelSettings, - defaults: userDefaults) + defaults: userDefaults, + accessTokenStorage: accessTokenStorage) /// An IPC server that provides access to the tunnel controller. /// @@ -317,9 +350,8 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { - APIRequest.Headers.setUserAgent(UserAgent.duckDuckGoUserAgent()) - SubscriptionPurchaseEnvironment.currentServiceEnvironment = tunnelSettings.selectedEnvironment == .production ? .production : .staging + APIRequest.Headers.setUserAgent(UserAgent.duckDuckGoUserAgent()) os_log("DuckDuckGoVPN started", log: .networkProtectionLoginItemLog, type: .info) setupMenuVisibility() @@ -375,10 +407,9 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private lazy var entitlementMonitor = NetworkProtectionEntitlementMonitor() private func setUpSubscriptionMonitoring() { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) guard accountManager.isUserAuthenticated else { return } let entitlementsCheck = { - await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + await self.accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) } Task { diff --git a/DuckDuckGoVPN/NetworkProtectionBouncer.swift b/DuckDuckGoVPN/NetworkProtectionBouncer.swift index 302245b270..e9acf969b5 100644 --- a/DuckDuckGoVPN/NetworkProtectionBouncer.swift +++ b/DuckDuckGoVPN/NetworkProtectionBouncer.swift @@ -27,12 +27,16 @@ import Subscription /// final class NetworkProtectionBouncer { + let accountManager: AccountManaging + + init(accountManager: AccountManaging) { + self.accountManager = accountManager + } + /// Simply verifies that the VPN feature is enabled and if not, takes care of killing the /// current app. /// func requireAuthTokenOrKillApp(controller: TunnelController) async { - let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) - guard !accountManager.isUserAuthenticated else { return } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift index 4b94e75351..c2035d4c06 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionManaging.swift @@ -27,27 +27,24 @@ public protocol DataBrokerProtectionSubscriptionManaging { } public final class DataBrokerProtectionSubscriptionManager: DataBrokerProtectionSubscriptionManaging { - private let accountManager: DataBrokerProtectionAccountManaging - private let environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging + + let subscriptionManager: SubscriptionManaging public var isUserAuthenticated: Bool { - accountManager.accessToken != nil + subscriptionManager.accountManager.accessToken != nil } public var accessToken: String? { - accountManager.accessToken + subscriptionManager.accountManager.accessToken } - public init(accountManager: DataBrokerProtectionAccountManaging, - environmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging) { - self.accountManager = accountManager - self.environmentManager = environmentManager + public init(subscriptionManager: SubscriptionManaging) { + self.subscriptionManager = subscriptionManager } public func hasValidEntitlement() async throws -> Bool { - environmentManager.updateEnvironment() - - switch await accountManager.hasEntitlement(for: .reloadIgnoringLocalCacheData) { + switch await subscriptionManager.accountManager.hasEntitlement(for: .dataBrokerProtection, + cachePolicy: .reloadIgnoringLocalCacheData) { case let .success(result): return result case .failure(let error): diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift deleted file mode 100644 index a18796bace..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Authentication/DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Subscription - -/// This protocol exists only as a wrapper on top of the SubscriptionPurchaseEnvironment since it is a concrete type on BSK -public protocol DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging { - func updateEnvironment() -} - -public final class DataBrokerProtectionSubscriptionPurchaseEnvironmentManager: DataBrokerProtectionSubscriptionPurchaseEnvironmentManaging { - private let settings: DataBrokerProtectionSettings - - public init(settings: DataBrokerProtectionSettings = DataBrokerProtectionSettings()) { - self.settings = settings - } - - public func updateEnvironment() { - SubscriptionPurchaseEnvironment.currentServiceEnvironment = settings.selectedEnvironment == .production ? .production : .staging - } -} diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift index 556d1d4b9f..c25e2b486e 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseModel.swift @@ -23,17 +23,17 @@ import Subscription @available(macOS 12.0, *) public final class DebugPurchaseModel: ObservableObject { - var purchaseManager: PurchaseManager - var accountManager: AccountManager - private let subscriptionAppGroup: String + var purchaseManager: StorePurchaseManager + let appStorePurchaseFlow: AppStorePurchaseFlow @Published var subscriptions: [SubscriptionRowModel] - init(manager: PurchaseManager, subscriptions: [SubscriptionRowModel] = [], subscriptionAppGroup: String) { + init(manager: StorePurchaseManager, + subscriptions: [SubscriptionRowModel] = [], + appStorePurchaseFlow: AppStorePurchaseFlow) { self.purchaseManager = manager self.subscriptions = subscriptions - self.subscriptionAppGroup = subscriptionAppGroup - self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) + self.appStorePurchaseFlow = appStorePurchaseFlow } @MainActor @@ -41,7 +41,7 @@ public final class DebugPurchaseModel: ObservableObject { print("Attempting purchase: \(product.displayName)") Task { - await AppStorePurchaseFlow.purchaseSubscription(with: product.id, emailAccessToken: nil, subscriptionAppGroup: subscriptionAppGroup) + await appStorePurchaseFlow.purchaseSubscription(with: product.id, emailAccessToken: nil) } } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift index 1573bec52e..8cc9b5a07a 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/DebugPurchaseViewController.swift @@ -25,7 +25,7 @@ import Subscription @available(macOS 12.0, *) public final class DebugPurchaseViewController: NSViewController { - private let manager: PurchaseManager + private let manager: StorePurchaseManager private let model: DebugPurchaseModel private var cancellables = Set() @@ -34,9 +34,9 @@ public final class DebugPurchaseViewController: NSViewController { fatalError("init(coder:) has not been implemented") } - public init(subscriptionAppGroup: String) { - manager = PurchaseManager.shared - model = DebugPurchaseModel(manager: manager, subscriptionAppGroup: subscriptionAppGroup) + public init(storePurchaseManager: StorePurchaseManager, appStorePurchaseFlow: AppStorePurchaseFlow) { + manager = storePurchaseManager + model = DebugPurchaseModel(manager: manager, appStorePurchaseFlow: appStorePurchaseFlow) super.init(nibName: nil, bundle: nil) } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift index 4ab7eadd32..fd16741a1d 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/DebugMenu/SubscriptionDebugMenu.swift @@ -21,44 +21,49 @@ import Subscription public final class SubscriptionDebugMenu: NSMenuItem { - var currentEnvironment: () -> String - var updateEnvironment: (String) -> Void + var currentEnvironment: SubscriptionEnvironment + var updateServiceEnvironment: (SubscriptionEnvironment.ServiceEnvironment) -> Void + var updatePurchasingPlatform: (SubscriptionEnvironment.PurchasePlatform) -> Void + var isInternalTestingEnabled: () -> Bool var updateInternalTestingFlag: (Bool) -> Void private var purchasePlatformItem: NSMenuItem? var currentViewController: () -> NSViewController? - private let accountManager: AccountManager - private let subscriptionAppGroup: String + let subscriptionManager: SubscriptionManaging + var accountManager: AccountManaging { + subscriptionManager.accountManager + } private var _purchaseManager: Any? @available(macOS 12.0, *) - fileprivate var purchaseManager: PurchaseManager { + fileprivate var purchaseManager: StorePurchaseManager { if _purchaseManager == nil { - _purchaseManager = PurchaseManager() + _purchaseManager = StorePurchaseManager() } // swiftlint:disable:next force_cast - return _purchaseManager as! PurchaseManager + return _purchaseManager as! StorePurchaseManager } required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public init(currentEnvironment: @escaping () -> String, - updateEnvironment: @escaping (String) -> Void, + public init(currentEnvironment: SubscriptionEnvironment, + updateServiceEnvironment: @escaping (SubscriptionEnvironment.ServiceEnvironment) -> Void, + updatePurchasingPlatform: @escaping (SubscriptionEnvironment.PurchasePlatform) -> Void, isInternalTestingEnabled: @escaping () -> Bool, updateInternalTestingFlag: @escaping (Bool) -> Void, currentViewController: @escaping () -> NSViewController?, - subscriptionAppGroup: String) { + subscriptionManager: SubscriptionManaging) { self.currentEnvironment = currentEnvironment - self.updateEnvironment = updateEnvironment + self.updateServiceEnvironment = updateServiceEnvironment + self.updatePurchasingPlatform = updatePurchasingPlatform self.isInternalTestingEnabled = isInternalTestingEnabled self.updateInternalTestingFlag = updateInternalTestingFlag self.currentViewController = currentViewController - self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) - self.subscriptionAppGroup = subscriptionAppGroup + self.subscriptionManager = subscriptionManager super.init(title: "Subscription", action: nil, keyEquivalent: "") self.submenu = makeSubmenu() } @@ -105,11 +110,8 @@ public final class SubscriptionDebugMenu: NSMenuItem { private func makePurchasePlatformSubmenu() -> NSMenu { let menu = NSMenu(title: "Select purchase platform:") - - let currentPlatform = SubscriptionPurchaseEnvironment.current - let appStoreItem = NSMenuItem(title: "App Store", action: #selector(setPlatformToAppStore), target: self) - if currentPlatform == .appStore { + if currentEnvironment.purchasePlatform == .appStore { appStoreItem.state = .on appStoreItem.isEnabled = false appStoreItem.action = nil @@ -118,7 +120,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(appStoreItem) let stripeItem = NSMenuItem(title: "Stripe", action: #selector(setPlatformToStripe), target: self) - if currentPlatform == .stripe { + if currentEnvironment.purchasePlatform == .stripe { stripeItem.state = .on stripeItem.isEnabled = false stripeItem.action = nil @@ -128,7 +130,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(.separator()) - let disclaimerItem = NSMenuItem(title: "⚠️ Change not persisted between app restarts", action: nil, target: nil) + let disclaimerItem = NSMenuItem(title: "⚠️ App restart required! The changes are persistent", action: nil, target: nil) menu.addItem(disclaimerItem) return menu @@ -137,11 +139,10 @@ public final class SubscriptionDebugMenu: NSMenuItem { private func makeEnvironmentSubmenu() -> NSMenu { let menu = NSMenu(title: "Select environment:") - let currentEnvironment = currentEnvironment() - let stagingItem = NSMenuItem(title: "Staging", action: #selector(setEnvironmentToStaging), target: self) - stagingItem.state = currentEnvironment == "staging" ? .on : .off - if currentEnvironment == "staging" { + let isStaging = currentEnvironment.serviceEnvironment == .staging + stagingItem.state = isStaging ? .on : .off + if isStaging { stagingItem.isEnabled = false stagingItem.action = nil stagingItem.target = nil @@ -149,14 +150,18 @@ public final class SubscriptionDebugMenu: NSMenuItem { menu.addItem(stagingItem) let productionItem = NSMenuItem(title: "Production", action: #selector(setEnvironmentToProduction), target: self) - productionItem.state = currentEnvironment == "production" ? .on : .off - if currentEnvironment == "production" { + let isProduction = currentEnvironment.serviceEnvironment == .production + productionItem.state = isProduction ? .on : .off + if isProduction { productionItem.isEnabled = false productionItem.action = nil productionItem.target = nil } menu.addItem(productionItem) + let disclaimerItem = NSMenuItem(title: "⚠️ App restart required! The changes are persistent", action: nil, target: nil) + menu.addItem(disclaimerItem) + return menu } @@ -178,8 +183,8 @@ public final class SubscriptionDebugMenu: NSMenuItem { func showAccountDetails() { let title = accountManager.isUserAuthenticated ? "Authenticated" : "Not Authenticated" let message = accountManager.isUserAuthenticated ? ["AuthToken: \(accountManager.authToken ?? "")", - "AccessToken: \(accountManager.accessToken ?? "")", - "Email: \(accountManager.email ?? "")"].joined(separator: "\n") : nil + "AccessToken: \(accountManager.accessToken ?? "")", + "Email: \(accountManager.email ?? "")"].joined(separator: "\n") : nil showAlert(title: title, message: message) } @@ -187,7 +192,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { func validateToken() { Task { guard let token = accountManager.accessToken else { return } - switch await AuthService.validateToken(accessToken: token) { + switch await subscriptionManager.authService.validateToken(accessToken: token) { case .success(let response): showAlert(title: "Validate token", message: "\(response)") case .failure(let error): @@ -218,7 +223,7 @@ public final class SubscriptionDebugMenu: NSMenuItem { func getSubscriptionDetails() { Task { guard let token = accountManager.accessToken else { return } - switch await SubscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) { + switch await subscriptionManager.subscriptionService.getSubscription(accessToken: token, cachePolicy: .reloadIgnoringLocalCacheData) { case .success(let response): showAlert(title: "Subscription info", message: "\(response)") case .failure(let error): @@ -237,10 +242,15 @@ public final class SubscriptionDebugMenu: NSMenuItem { @IBAction func showPurchaseView(_ sender: Any?) { if #available(macOS 12.0, *) { - currentViewController()?.presentAsSheet(DebugPurchaseViewController(subscriptionAppGroup: subscriptionAppGroup)) + let storePurchaseManager = StorePurchaseManager() + let appStorePurchaseFlow = AppStorePurchaseFlow(subscriptionManager: subscriptionManager) + let vc = DebugPurchaseViewController(storePurchaseManager: storePurchaseManager, appStorePurchaseFlow: appStorePurchaseFlow) + currentViewController()?.presentAsSheet(vc) } } + // MARK: - Platform + @IBAction func setPlatformToAppStore(_ sender: Any?) { askAndUpdatePlatform(to: .appStore) } @@ -249,39 +259,46 @@ public final class SubscriptionDebugMenu: NSMenuItem { askAndUpdatePlatform(to: .stripe) } - private func askAndUpdatePlatform(to newPlatform: SubscriptionPurchaseEnvironment.Environment) { + private func askAndUpdatePlatform(to newPlatform: SubscriptionEnvironment.PurchasePlatform) { let alert = makeAlert(title: "Are you sure you want to change the purchase platform to \(newPlatform.rawValue.capitalized)", - message: "This setting is not persisted between app runs. After restarting the app it returns to the default determined on app's distribution method.", + message: "This setting IS persisted between app runs. This action will close the app, do you want to proceed?", buttonNames: ["Yes", "No"]) let response = alert.runModal() - guard case .alertFirstButtonReturn = response else { return } - - SubscriptionPurchaseEnvironment.current = newPlatform - - refreshSubmenu() + updatePurchasingPlatform(newPlatform) + closeTheApp() } + // MARK: - Environment + @IBAction func setEnvironmentToStaging(_ sender: Any?) { - askAndUpdateEnvironment(to: "staging") + askAndUpdateServiceEnvironment(to: SubscriptionEnvironment.ServiceEnvironment.staging) } @IBAction func setEnvironmentToProduction(_ sender: Any?) { - askAndUpdateEnvironment(to: "production") + askAndUpdateServiceEnvironment(to: SubscriptionEnvironment.ServiceEnvironment.production) } - private func askAndUpdateEnvironment(to newEnvironmentString: String) { - let alert = makeAlert(title: "Are you sure you want to change the environment to \(newEnvironmentString.capitalized)", - message: "Please make sure you have manually removed your current active Subscription and reset all related features. \nYou may also need to change environment of related features.", + private func askAndUpdateServiceEnvironment(to newServiceEnvironment: SubscriptionEnvironment.ServiceEnvironment) { + let alert = makeAlert(title: "Are you sure you want to change the environment to \(newServiceEnvironment.description.capitalized)", + message: """ + Please make sure you have manually removed your current active Subscription and reset all related features. + You may also need to change environment of related features. + This setting IS persisted between app runs. This action will close the app, do you want to proceed? + """, buttonNames: ["Yes", "No"]) let response = alert.runModal() - guard case .alertFirstButtonReturn = response else { return } + updateServiceEnvironment(newServiceEnvironment) + closeTheApp() + } - updateEnvironment(newEnvironmentString) - refreshSubmenu() + func closeTheApp() { + NSApp.terminate(self) } + // MARK: - + @objc func postDidSignInNotification(_ sender: Any?) { NotificationCenter.default.post(name: .accountDidSignIn, object: self, userInfo: nil) @@ -296,7 +313,8 @@ public final class SubscriptionDebugMenu: NSMenuItem { func restorePurchases(_ sender: Any?) { if #available(macOS 12.0, *) { Task { - await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: subscriptionAppGroup) + let appStoreRestoreFlow = AppStoreRestoreFlow(subscriptionManager: subscriptionManager) + await appStoreRestoreFlow.restoreAccountFromPastPurchase() } } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index f18041dace..406d632e8d 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -35,11 +35,13 @@ public final class PreferencesSubscriptionModel: ObservableObject { lazy var sheetModel: SubscriptionAccessModel = makeSubscriptionAccessModel() - private let accountManager: AccountManager + private let subscriptionManager: SubscriptionManaging + private var accountManager: AccountManaging { + subscriptionManager.accountManager + } private let openURLHandler: (URL) -> Void public let userEventHandler: (UserEvent) -> Void private let sheetActionHandler: SubscriptionAccessActionHandlers - private let subscriptionAppGroup: String private var fetchSubscriptionDetailsTask: Task<(), Never>? @@ -89,12 +91,11 @@ public final class PreferencesSubscriptionModel: ObservableObject { public init(openURLHandler: @escaping (URL) -> Void, userEventHandler: @escaping (UserEvent) -> Void, sheetActionHandler: SubscriptionAccessActionHandlers, - subscriptionAppGroup: String) { - self.accountManager = AccountManager(subscriptionAppGroup: subscriptionAppGroup) + subscriptionManager: SubscriptionManaging) { + self.subscriptionManager = subscriptionManager self.openURLHandler = openURLHandler self.userEventHandler = userEventHandler self.sheetActionHandler = sheetActionHandler - self.subscriptionAppGroup = subscriptionAppGroup self.isUserAuthenticated = accountManager.isUserAuthenticated @@ -136,9 +137,9 @@ public final class PreferencesSubscriptionModel: ObservableObject { private func makeSubscriptionAccessModel() -> SubscriptionAccessModel { if accountManager.isUserAuthenticated { - ShareSubscriptionAccessModel(actionHandlers: sheetActionHandler, email: accountManager.email, subscriptionAppGroup: subscriptionAppGroup) + ShareSubscriptionAccessModel(actionHandlers: sheetActionHandler, email: accountManager.email, subscriptionManager: subscriptionManager) } else { - ActivateSubscriptionAccessModel(actionHandlers: sheetActionHandler, shouldShowRestorePurchase: SubscriptionPurchaseEnvironment.current == .appStore) + ActivateSubscriptionAccessModel(actionHandlers: sheetActionHandler, subscriptionManager: subscriptionManager) } } @@ -149,7 +150,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor func purchaseAction() { - openURLHandler(.subscriptionPurchase) + openURLHandler(subscriptionManager.url(for: .purchase)) } enum ChangePlanOrBillingAction { @@ -181,14 +182,14 @@ public final class PreferencesSubscriptionModel: ObservableObject { } } - private func changePlanOrBilling(for environment: SubscriptionPurchaseEnvironment.Environment) { + private func changePlanOrBilling(for environment: SubscriptionEnvironment.PurchasePlatform) { switch environment { case .appStore: - NSWorkspace.shared.open(.manageSubscriptionsInAppStoreAppURL) + NSWorkspace.shared.open(subscriptionManager.url(for: .manageSubscriptionsInAppStore)) case .stripe: Task { guard let accessToken = accountManager.accessToken, let externalID = accountManager.externalID, - case let .success(response) = await SubscriptionService.getCustomerPortalURL(accessToken: accessToken, externalID: externalID) else { return } + case let .success(response) = await subscriptionManager.subscriptionService.getCustomerPortalURL(accessToken: accessToken, externalID: externalID) else { return } guard let customerPortalURL = URL(string: response.customerPortalUrl) else { return } openURLHandler(customerPortalURL) @@ -198,9 +199,8 @@ public final class PreferencesSubscriptionModel: ObservableObject { private func confirmIfSignedInToSameAccount() async -> Bool { if #available(macOS 12.0, *) { - guard let lastTransactionJWSRepresentation = await PurchaseManager.mostRecentTransaction() else { return false } - - switch await AuthService.storeLogin(signature: lastTransactionJWSRepresentation) { + guard let lastTransactionJWSRepresentation = await subscriptionManager.storePurchaseManager().mostRecentTransaction() else { return false } + switch await subscriptionManager.authService.storeLogin(signature: lastTransactionJWSRepresentation) { case .success(let response): return response.externalID == accountManager.externalID case .failure: @@ -234,15 +234,16 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor func openFAQ() { - openURLHandler(.subscriptionFAQ) + openURLHandler(subscriptionManager.url(for: .faq)) } @MainActor func refreshSubscriptionPendingState() { - if SubscriptionPurchaseEnvironment.current == .appStore { + if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, *) { Task { - _ = await AppStoreRestoreFlow.restoreAccountFromPastPurchase(subscriptionAppGroup: subscriptionAppGroup) + let appStoreRestoreFlow = AppStoreRestoreFlow(subscriptionManager: subscriptionManager) + await appStoreRestoreFlow.restoreAccountFromPastPurchase() fetchAndUpdateSubscriptionDetails() } } @@ -270,11 +271,11 @@ public final class PreferencesSubscriptionModel: ObservableObject { @MainActor private func updateSubscription(with cachePolicy: SubscriptionService.CachePolicy) async { guard let token = accountManager.accessToken else { - SubscriptionService.signOut() + subscriptionManager.subscriptionService.signOut() return } - switch await SubscriptionService.getSubscription(accessToken: token, cachePolicy: cachePolicy) { + switch await subscriptionManager.subscriptionService.getSubscription(accessToken: token, cachePolicy: cachePolicy) { case .success(let subscription): updateDescription(for: subscription.expiresOrRenewsAt, status: subscription.status, period: subscription.billingPeriod) subscriptionPlatform = subscription.platform @@ -285,7 +286,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { } @MainActor - private func updateAllEntitlement(with cachePolicy: AccountManager.CachePolicy) async { + private func updateAllEntitlement(with cachePolicy: AccountManaging.CachePolicy) async { switch await self.accountManager.hasEntitlement(for: .networkProtection, cachePolicy: cachePolicy) { case let .success(result): hasAccessToVPN = result diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift index 3e14765641..5d7517bb58 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ActivateSubscriptionAccessModel.swift @@ -20,10 +20,10 @@ import Foundation import Subscription public final class ActivateSubscriptionAccessModel: SubscriptionAccessModel, PurchaseRestoringSubscriptionAccessModel { - private var actionHandlers: SubscriptionAccessActionHandlers + private var actionHandlers: SubscriptionAccessActionHandlers public var title = UserText.activateModalTitle - public var description = UserText.activateModalDescription(platform: SubscriptionPurchaseEnvironment.current) + public let description: String public var email: String? public var emailLabel: String { UserText.email } @@ -34,13 +34,18 @@ public final class ActivateSubscriptionAccessModel: SubscriptionAccessModel, Pur public var restorePurchaseDescription = UserText.restorePurchaseDescription public var restorePurchaseButtonTitle = UserText.restorePurchaseButton - public init(actionHandlers: SubscriptionAccessActionHandlers, shouldShowRestorePurchase: Bool) { + let subscriptionManager: SubscriptionManaging + + public init(actionHandlers: SubscriptionAccessActionHandlers, + subscriptionManager: SubscriptionManaging) { self.actionHandlers = actionHandlers - self.shouldShowRestorePurchase = shouldShowRestorePurchase + self.shouldShowRestorePurchase = subscriptionManager.currentEnvironment.purchasePlatform == .appStore + self.subscriptionManager = subscriptionManager + self.description = UserText.activateModalDescription(platform: subscriptionManager.currentEnvironment.purchasePlatform) } public func handleEmailAction() { - actionHandlers.openURLHandler(.activateSubscriptionViaEmail) + actionHandlers.openURLHandler(subscriptionManager.url(for: .activateViaEmail)) actionHandlers.uiActionHandler(.activateAddEmailClick) } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift index 9e007f4e7e..d67abee49c 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/Model/ShareSubscriptionAccessModel.swift @@ -20,27 +20,28 @@ import Foundation import Subscription public final class ShareSubscriptionAccessModel: SubscriptionAccessModel { - public var title = UserText.shareModalTitle - public var description = UserText.shareModalDescription(platform: SubscriptionPurchaseEnvironment.current) - private let subscriptionAppGroup: String + public var title = UserText.shareModalTitle + public let description: String private var actionHandlers: SubscriptionAccessActionHandlers - public var email: String? public var emailLabel: String { UserText.email } public var emailDescription: String { hasEmail ? UserText.shareModalHasEmailDescription : UserText.shareModalNoEmailDescription } public var emailButtonTitle: String { hasEmail ? UserText.manageEmailButton : UserText.addEmailButton } + private let subscriptionManager: SubscriptionManaging - public init(actionHandlers: SubscriptionAccessActionHandlers, email: String?, subscriptionAppGroup: String) { + public init(actionHandlers: SubscriptionAccessActionHandlers, email: String?, subscriptionManager: SubscriptionManaging) { self.actionHandlers = actionHandlers self.email = email - self.subscriptionAppGroup = subscriptionAppGroup + self.subscriptionManager = subscriptionManager + self.description = UserText.shareModalDescription(platform: subscriptionManager.currentEnvironment.purchasePlatform) } private var hasEmail: Bool { !(email?.isEmpty ?? true) } public func handleEmailAction() { - let url: URL = hasEmail ? .manageSubscriptionEmail : .addEmailToSubscription + let type = hasEmail ? SubscriptionURL.manageEmail : SubscriptionURL.addEmail + let mailURL: URL = subscriptionManager.url(for: type) if hasEmail { actionHandlers.uiActionHandler(.postSubscriptionAddEmailClick) @@ -49,14 +50,15 @@ public final class ShareSubscriptionAccessModel: SubscriptionAccessModel { } Task { - if SubscriptionPurchaseEnvironment.current == .appStore { + if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { if #available(macOS 12.0, iOS 15.0, *) { - await AppStoreAccountManagementFlow.refreshAuthTokenIfNeeded(subscriptionAppGroup: subscriptionAppGroup) + let appStoreAccountManagementFlow = AppStoreAccountManagementFlow(subscriptionManager: subscriptionManager) + await appStoreAccountManagementFlow.refreshAuthTokenIfNeeded() } } DispatchQueue.main.async { - self.actionHandlers.openURLHandler(url) + self.actionHandlers.openURLHandler(mailURL) } } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift index 3833c60ff0..f08c95cfb0 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/SubscriptionAccessView/SubscriptionAccessViewController.swift @@ -22,18 +22,17 @@ import SwiftUI public final class SubscriptionAccessViewController: NSViewController { - private let accountManager: AccountManager + private let subscriptionManager: SubscriptionManaging private var actionHandlers: SubscriptionAccessActionHandlers - private let subscriptionAppGroup: String public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public init(accountManager: AccountManager, actionHandlers: SubscriptionAccessActionHandlers, subscriptionAppGroup: String) { - self.accountManager = accountManager + public init(subscriptionManager: SubscriptionManaging, + actionHandlers: SubscriptionAccessActionHandlers) { + self.subscriptionManager = subscriptionManager self.actionHandlers = actionHandlers - self.subscriptionAppGroup = subscriptionAppGroup super.init(nibName: nil, bundle: nil) } @@ -56,10 +55,10 @@ public final class SubscriptionAccessViewController: NSViewController { } private func makeSubscriptionAccessModel() -> SubscriptionAccessModel { - if accountManager.isUserAuthenticated { - ShareSubscriptionAccessModel(actionHandlers: actionHandlers, email: accountManager.email, subscriptionAppGroup: subscriptionAppGroup) + if subscriptionManager.accountManager.isUserAuthenticated { + ShareSubscriptionAccessModel(actionHandlers: actionHandlers, email: subscriptionManager.accountManager.email, subscriptionManager: subscriptionManager) } else { - ActivateSubscriptionAccessModel(actionHandlers: actionHandlers, shouldShowRestorePurchase: SubscriptionPurchaseEnvironment.current == .appStore) + ActivateSubscriptionAccessModel(actionHandlers: actionHandlers, subscriptionManager: subscriptionManager) } } } diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index f917d58b71..5e470bf2f8 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -101,7 +101,7 @@ enum UserText { // MARK: - Activate subscription modal static let activateModalTitle = NSLocalizedString("subscription.activate.modal.title", value: "Activate your subscription on this device", comment: "Activate subscription modal view title") - static func activateModalDescription(platform: SubscriptionPurchaseEnvironment.Environment) -> String { + static func activateModalDescription(platform: SubscriptionEnvironment.PurchasePlatform) -> String { switch platform { case .appStore: NSLocalizedString("subscription.appstore.activate.modal.description", value: "Access your Privacy Pro subscription on this device via Apple ID or an email address.", comment: "Activate subscription modal view subtitle description") @@ -115,7 +115,7 @@ enum UserText { // MARK: - Share subscription modal static let shareModalTitle = NSLocalizedString("subscription.share.modal.title", value: "Use your subscription on other devices", comment: "Share subscription modal view title") - static func shareModalDescription(platform: SubscriptionPurchaseEnvironment.Environment) -> String { + static func shareModalDescription(platform: SubscriptionEnvironment.PurchasePlatform) -> String { switch platform { case .appStore: NSLocalizedString("subscription.appstore.share.modal.description", value: "Access your subscription via Apple ID or by adding an email address.", comment: "Share subscription modal view subtitle description") diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index f9680be11d..f6d7e42781 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -21,6 +21,7 @@ import NetworkProtection import NetworkProtectionUI import XCTest import Subscription +import SubscriptionTestingUtilities @testable import DuckDuckGo_Privacy_Browser @@ -29,17 +30,8 @@ final class MoreOptionsMenuTests: XCTestCase { var tabCollectionViewModel: TabCollectionViewModel! var passwordManagerCoordinator: PasswordManagerCoordinator! var capturingActionDelegate: CapturingOptionsButtonMenuDelegate! - @MainActor - lazy var moreOptionMenu: MoreOptionsMenu! = { - let menu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, - passwordManagerCoordinator: passwordManagerCoordinator, - networkProtectionFeatureVisibility: networkProtectionVisibilityMock, - sharingMenu: NSMenu(), - internalUserDecider: internalUserDecider) - menu.actionDelegate = capturingActionDelegate - return menu - }() - + var accountManager: AccountManagerMock! + var moreOptionsMenu: MoreOptionsMenu! var internalUserDecider: InternalUserDeciderMock! var networkProtectionVisibilityMock: NetworkProtectionVisibilityMock! @@ -51,8 +43,15 @@ final class MoreOptionsMenuTests: XCTestCase { passwordManagerCoordinator = PasswordManagerCoordinator() capturingActionDelegate = CapturingOptionsButtonMenuDelegate() internalUserDecider = InternalUserDeciderMock() - + accountManager = AccountManagerMock(isUserAuthenticated: true) networkProtectionVisibilityMock = NetworkProtectionVisibilityMock(isInstalled: false, visible: false) + moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, + passwordManagerCoordinator: passwordManagerCoordinator, + networkProtectionFeatureVisibility: networkProtectionVisibilityMock, + sharingMenu: NSMenu(), + internalUserDecider: internalUserDecider, + accountManager: accountManager) + moreOptionsMenu.actionDelegate = capturingActionDelegate } @MainActor @@ -60,51 +59,77 @@ final class MoreOptionsMenuTests: XCTestCase { tabCollectionViewModel = nil passwordManagerCoordinator = nil capturingActionDelegate = nil - moreOptionMenu = nil + moreOptionsMenu = nil + accountManager = nil super.tearDown() } @MainActor - func testThatMoreOptionMenuHasTheExpectedItems() { - moreOptionMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, + func testThatMoreOptionMenuHasTheExpectedItemsAuthenticated() { + moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, passwordManagerCoordinator: passwordManagerCoordinator, networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), sharingMenu: NSMenu(), - internalUserDecider: internalUserDecider) - - XCTAssertEqual(moreOptionMenu.items[0].title, UserText.sendFeedback) - XCTAssertTrue(moreOptionMenu.items[1].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[2].title, UserText.plusButtonNewTabMenuItem) - XCTAssertEqual(moreOptionMenu.items[3].title, UserText.newWindowMenuItem) - XCTAssertEqual(moreOptionMenu.items[4].title, UserText.newBurnerWindowMenuItem) - XCTAssertTrue(moreOptionMenu.items[5].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[6].title, UserText.zoom) - XCTAssertTrue(moreOptionMenu.items[7].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[8].title, UserText.bookmarks) - XCTAssertEqual(moreOptionMenu.items[9].title, UserText.downloads) - XCTAssertEqual(moreOptionMenu.items[10].title, UserText.passwordManagement) - XCTAssertTrue(moreOptionMenu.items[11].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[12].title, UserText.emailOptionsMenuItem) - - if AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)).isUserAuthenticated { - XCTAssertTrue(moreOptionMenu.items[13].isSeparatorItem) - XCTAssertTrue(moreOptionMenu.items[14].title.hasPrefix(UserText.networkProtection)) - XCTAssertTrue(moreOptionMenu.items[15].title.hasPrefix(UserText.identityTheftRestorationOptionsMenuItem)) - XCTAssertTrue(moreOptionMenu.items[16].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[17].title, UserText.settings) - } else { - XCTAssertTrue(moreOptionMenu.items[13].isSeparatorItem) - XCTAssertTrue(moreOptionMenu.items[14].title.hasPrefix(UserText.networkProtection)) - XCTAssertTrue(moreOptionMenu.items[15].isSeparatorItem) - XCTAssertEqual(moreOptionMenu.items[16].title, UserText.settings) - } + internalUserDecider: internalUserDecider, + accountManager: accountManager) + + XCTAssertEqual(moreOptionsMenu.items[0].title, UserText.sendFeedback) + XCTAssertTrue(moreOptionsMenu.items[1].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[2].title, UserText.plusButtonNewTabMenuItem) + XCTAssertEqual(moreOptionsMenu.items[3].title, UserText.newWindowMenuItem) + XCTAssertEqual(moreOptionsMenu.items[4].title, UserText.newBurnerWindowMenuItem) + XCTAssertTrue(moreOptionsMenu.items[5].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[6].title, UserText.zoom) + XCTAssertTrue(moreOptionsMenu.items[7].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[8].title, UserText.bookmarks) + XCTAssertEqual(moreOptionsMenu.items[9].title, UserText.downloads) + XCTAssertEqual(moreOptionsMenu.items[10].title, UserText.passwordManagement) + XCTAssertTrue(moreOptionsMenu.items[11].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[12].title, UserText.emailOptionsMenuItem) + + XCTAssertTrue(moreOptionsMenu.items[13].isSeparatorItem) + XCTAssertTrue(moreOptionsMenu.items[14].title.hasPrefix(UserText.networkProtection)) + XCTAssertTrue(moreOptionsMenu.items[15].title.hasPrefix(UserText.identityTheftRestorationOptionsMenuItem)) + XCTAssertTrue(moreOptionsMenu.items[16].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[17].title, UserText.settings) + } + + @MainActor + func testThatMoreOptionMenuHasTheExpectedItemsNotAuthenticated() { + + accountManager = AccountManagerMock(isUserAuthenticated: false) + moreOptionsMenu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel, + passwordManagerCoordinator: passwordManagerCoordinator, + networkProtectionFeatureVisibility: NetworkProtectionVisibilityMock(isInstalled: false, visible: true), + sharingMenu: NSMenu(), + internalUserDecider: internalUserDecider, + accountManager: accountManager) + + XCTAssertEqual(moreOptionsMenu.items[0].title, UserText.sendFeedback) + XCTAssertTrue(moreOptionsMenu.items[1].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[2].title, UserText.plusButtonNewTabMenuItem) + XCTAssertEqual(moreOptionsMenu.items[3].title, UserText.newWindowMenuItem) + XCTAssertEqual(moreOptionsMenu.items[4].title, UserText.newBurnerWindowMenuItem) + XCTAssertTrue(moreOptionsMenu.items[5].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[6].title, UserText.zoom) + XCTAssertTrue(moreOptionsMenu.items[7].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[8].title, UserText.bookmarks) + XCTAssertEqual(moreOptionsMenu.items[9].title, UserText.downloads) + XCTAssertEqual(moreOptionsMenu.items[10].title, UserText.passwordManagement) + XCTAssertTrue(moreOptionsMenu.items[11].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[12].title, UserText.emailOptionsMenuItem) + + XCTAssertTrue(moreOptionsMenu.items[13].isSeparatorItem) + XCTAssertTrue(moreOptionsMenu.items[14].title.hasPrefix(UserText.networkProtection)) + XCTAssertTrue(moreOptionsMenu.items[15].isSeparatorItem) + XCTAssertEqual(moreOptionsMenu.items[16].title, UserText.settings) } // MARK: Zoom @MainActor func testWhenClickingDefaultZoomInZoomSubmenuThenTheActionDelegateIsAlerted() { - guard let zoomSubmenu = moreOptionMenu.zoomMenuItem.submenu else { + guard let zoomSubmenu = moreOptionsMenu.zoomMenuItem.submenu else { XCTFail("No zoom submenu available") return } @@ -116,19 +141,16 @@ final class MoreOptionsMenuTests: XCTestCase { } // MARK: Preferences - - @MainActor func testWhenClickingOnPreferenceMenuItemThenTheActionDelegateIsAlerted() { - moreOptionMenu.performActionForItem(at: moreOptionMenu.items.count - 1) + moreOptionsMenu.performActionForItem(at: moreOptionsMenu.items.count - 1) XCTAssertTrue(capturingActionDelegate.optionsButtonMenuRequestedPreferencesCalled) } // MARK: - Bookmarks - @MainActor func testWhenClickingOnBookmarkAllTabsMenuItemThenTheActionDelegateIsAlerted() throws { // GIVEN - let bookmarksMenu = try XCTUnwrap(moreOptionMenu.item(at: 8)?.submenu) + let bookmarksMenu = try XCTUnwrap(moreOptionsMenu.item(at: 8)?.submenu) let bookmarkAllTabsIndex = try XCTUnwrap(bookmarksMenu.indexOfItem(withTitle: UserText.bookmarkAllTabs)) let bookmarkAllTabsMenuItem = try XCTUnwrap(bookmarksMenu.items[bookmarkAllTabsIndex]) bookmarkAllTabsMenuItem.isEnabled = true diff --git a/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift b/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift index 814abc7a42..3f7d7da067 100644 --- a/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift +++ b/UnitTests/Subscriptions/SubscriptionRedirectManagerTests.swift @@ -17,7 +17,8 @@ // import XCTest -import Subscription +@testable import Subscription +import SubscriptionTestingUtilities @testable import DuckDuckGo_Privacy_Browser final class SubscriptionRedirectManagerTests: XCTestCase { @@ -25,8 +26,11 @@ final class SubscriptionRedirectManagerTests: XCTestCase { override func setUpWithError() throws { try super.setUpWithError() - sut = PrivacyProSubscriptionRedirectManager(featureAvailabiltyProvider: true) - SubscriptionPurchaseEnvironment.canPurchase = true + sut = PrivacyProSubscriptionRedirectManager(featureAvailabiltyProvider: true, + subscriptionEnvironment: SubscriptionEnvironment(serviceEnvironment: .production, + purchasePlatform: .appStore), + baseURL: SubscriptionURL.baseURL.subscriptionURL(environment: .production), + canPurchase: { true }) } override func tearDownWithError() throws { @@ -37,7 +41,8 @@ final class SubscriptionRedirectManagerTests: XCTestCase { func testWhenURLIsPrivacyProAndHasOriginQueryParameterThenRedirectToSubscriptionBaseURLAndAppendQueryParameter() throws { // GIVEN let url = try XCTUnwrap(URL(string: "https://www.duckduckgo.com/pro?origin=test")) - let expectedURL = URL.subscriptionBaseURL.appending(percentEncodedQueryItem: .init(name: "origin", value: "test")) + let baseURL = SubscriptionURL.baseURL.subscriptionURL(environment: .production) + let expectedURL = baseURL.appending(percentEncodedQueryItem: .init(name: "origin", value: "test")) // WHEN let result = sut.redirectURL(for: url) @@ -49,7 +54,7 @@ final class SubscriptionRedirectManagerTests: XCTestCase { func testWhenURLIsPrivacyProAndDoesNotHaveOriginQueryParameterThenRedirectToSubscriptionBaseURL() throws { // GIVEN let url = try XCTUnwrap(URL(string: "https://www.duckduckgo.com/pro")) - let expectedURL = URL.subscriptionBaseURL + let expectedURL = SubscriptionURL.baseURL.subscriptionURL(environment: .production) // WHEN let result = sut.redirectURL(for: url) diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index 975116e54b..ea46a83f31 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -17,8 +17,7 @@ // import XCTest -import Subscription - +@testable import Subscription @testable import DuckDuckGo_Privacy_Browser @MainActor @@ -228,7 +227,8 @@ final class TabBarViewItemTests: XCTestCase { tabBarViewItem.closeButton = mouseButton // Update url - let tab = Tab(content: .subscription(.subscriptionPurchase)) + let url = SubscriptionURL.purchase.subscriptionURL(environment: .production) + let tab = Tab(content: .subscription(url)) delegate.mockedCurrentTab = tab let vm = TabViewModel(tab: tab) tabBarViewItem.subscribe(to: vm, tabCollectionViewModel: TabCollectionViewModel())