diff --git a/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig b/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig index 662be4ddeb..b3b7f7ef35 100644 --- a/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig +++ b/Configuration/App/NetworkProtection/DuckDuckGoVPN.xcconfig @@ -49,10 +49,10 @@ PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*] = PROVISIONING_PROFILE_SPECIFIER[config=Review][sdk=macosx*] = macOS NetP VPN App - Review (XPC) PROVISIONING_PROFILE_SPECIFIER[config=Release][sdk=macosx*] = macOS NetP VPN App - Release (XPC) -FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_PROTECTION SUBSCRIPTION SWIFT_OBJC_BRIDGING_HEADER = SKIP_INSTALL = YES diff --git a/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig b/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig index 9cc1c7ae28..325f9024b7 100644 --- a/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig +++ b/Configuration/Extensions/NetworkProtection/NetworkProtectionSystemExtension.xcconfig @@ -31,10 +31,10 @@ INFOPLIST_FILE = NetworkProtectionSystemExtension/Info.plist INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2023 DuckDuckGo. All rights reserved. INFOPLIST_KEY_NSSystemExtensionUsageDescription = DuckDuckGo VPN -FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION -FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION +FEATURE_FLAGS[arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=CI][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Debug][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION +FEATURE_FLAGS[config=Review][arch=*][sdk=*] = NETP_SYSTEM_EXTENSION NETWORK_EXTENSION NETWORK_PROTECTION SUBSCRIPTION PRODUCT_BUNDLE_IDENTIFIER[sdk=*] = $(SYSEX_BUNDLE_ID) PRODUCT_BUNDLE_IDENTIFIER[config=CI][sdk=*] = $(SYSEX_BUNDLE_ID) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 66a3b706f0..1596da5917 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2278,8 +2278,6 @@ 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; 7BA7CC592AD1203B0042E5CE /* UserText+NetworkProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60D22A0C84F700BCD287 /* UserText+NetworkProtection.swift */; }; - 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */; }; - 7BA7CC5B2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60692A0B29FA00BCD287 /* NetworkProtection+ConvenienceInitializers.swift */; }; 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60722A0B29FA00BCD287 /* EventMapping+NetworkProtectionError.swift */; }; 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 7BA7CC5E2AD1210C0042E5CE /* Networking */; }; @@ -3223,6 +3221,11 @@ EE0629762B90EE8C00D868B4 /* AccountManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0629712B90EE8C00D868B4 /* AccountManagerExtension.swift */; }; EE2F9C5B2B90F2FF00D45FC9 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = EE2F9C5A2B90F2FF00D45FC9 /* Subscription */; }; EE339228291BDEFD009F62C1 /* JSAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE339227291BDEFD009F62C1 /* JSAlertController.swift */; }; + EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; + EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE34245D2BA0853900173B1B /* VPNUninstaller.swift */; }; + EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; + EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */; }; + EE66418F2B9B1BD1005BCD17 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = EE66418E2B9B1BD1005BCD17 /* Subscription */; }; EE66666F2B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; EE6666702B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; EE6666712B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */; }; @@ -3258,6 +3261,8 @@ EEC8EB402982CD550065AA39 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; EECE10E529DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; EECE10E629DD77E60044D027 /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECE10E429DD77E60044D027 /* FeatureFlag.swift */; }; + EEDE50112BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */; }; + EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */; }; EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; F1D43AEE2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */; }; @@ -4637,6 +4642,8 @@ EAFAD6C92728BD1200F9DF00 /* clickToLoad.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = clickToLoad.js; sourceTree = ""; }; EE0629712B90EE8C00D868B4 /* AccountManagerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManagerExtension.swift; sourceTree = ""; }; EE339227291BDEFD009F62C1 /* JSAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSAlertController.swift; sourceTree = ""; }; + EE34245D2BA0853900173B1B /* VPNUninstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNUninstaller.swift; sourceTree = ""; }; + EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift"; sourceTree = ""; }; EE66666E2B56EDE4001D898D /* VPNLocationsHostingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationsHostingViewController.swift; sourceTree = ""; }; EEA3EEB02B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVPNCountryLabelsModel.swift; sourceTree = ""; }; EEA3EEB22B24EC0600E8333A /* VPNLocationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLocationViewModel.swift; sourceTree = ""; }; @@ -4647,6 +4654,7 @@ EEC4A66C2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItemModel.swift; sourceTree = ""; }; EEC4A6702B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNLocationPreferenceItem.swift; sourceTree = ""; }; EECE10E429DD77E60044D027 /* FeatureFlag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlag.swift; sourceTree = ""; }; + 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 = ""; }; F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MainMenuActions+VanillaBrowser.swift"; sourceTree = ""; }; @@ -4734,6 +4742,7 @@ buildActionMask = 2147483647; files = ( 37269F012B332FC8005E8E46 /* Common in Frameworks */, + EE66418F2B9B1BD1005BCD17 /* Subscription in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, 4B2537772A11BFE100610219 /* PixelKit in Frameworks */, 7BBE2B7B2B61663C00697445 /* NetworkProtectionProxy in Frameworks */, @@ -5761,6 +5770,7 @@ 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, + EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */, ); path = NetworkExtensionTargets; sourceTree = ""; @@ -6414,9 +6424,11 @@ 7BA7CC132AD11DC80042E5CE /* AppLauncher+DefaultInitializer.swift */, 7BA7CC0E2AD11DC80042E5CE /* DuckDuckGoVPNAppDelegate.swift */, 7BD1688D2AD4A4C400D24876 /* NetworkExtensionController.swift */, + EEDE50102BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift */, 7BA7CC152AD11DC80042E5CE /* NetworkProtectionBouncer.swift */, 7B8DB3192B504D7500EC16DA /* VPNAppEventsHandler.swift */, 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */, + EE34245D2BA0853900173B1B /* VPNUninstaller.swift */, 7BA7CC112AD11DC80042E5CE /* TunnelControllerIPCService.swift */, 7BA7CC172AD11DC80042E5CE /* UserText.swift */, 7BA7CC122AD11DC80042E5CE /* Assets.xcassets */, @@ -8800,6 +8812,7 @@ EE7295E82A545BC4008C0991 /* NetworkProtection */, 37269F002B332FC8005E8E46 /* Common */, 7BBE2B7A2B61663C00697445 /* NetworkProtectionProxy */, + EE66418E2B9B1BD1005BCD17 /* Subscription */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -11067,6 +11080,7 @@ files = ( 4B25377A2A11C01700610219 /* UserText+NetworkProtectionExtensions.swift in Sources */, B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, + EE66418D2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, @@ -11090,10 +11104,10 @@ files = ( B6F92BA22A691580002ABA6B /* UserDefaultsWrapper.swift in Sources */, 4B2D065B2A11D1FF00DE1F49 /* Logging.swift in Sources */, - 7BA7CC5B2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 7BA7CC3A2AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */, 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, 7BA7CC592AD1203B0042E5CE /* UserText+NetworkProtection.swift in Sources */, + EEDE50112BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, 7BA7CC562AD11FFB0042E5CE /* NetworkProtectionOptionKeyExtension.swift in Sources */, 7B2DDCFA2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */, 7BA7CC4C2AD11EC70042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, @@ -11104,6 +11118,7 @@ EE0629742B90EE8C00D868B4 /* AccountManagerExtension.swift in Sources */, 7BA7CC5D2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, + EE3424602BA0853900173B1B /* VPNUninstaller.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, @@ -11127,9 +11142,9 @@ 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */, 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, + EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, 4BA7C4D92B3F61FB00AFE511 /* BundleExtension.swift in Sources */, - 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */, 7BA7CC3F2AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, 4B0EF7292B5780EB009D6481 /* VPNAppEventsHandler.swift in Sources */, @@ -11138,6 +11153,7 @@ EE0629752B90EE8C00D868B4 /* AccountManagerExtension.swift in Sources */, 7BA7CC582AD1203A0042E5CE /* UserText+NetworkProtection.swift in Sources */, 7BA7CC4B2AD11EC60042E5CE /* NetworkProtectionControllerErrorStore.swift in Sources */, + EE3424612BA0853900173B1B /* VPNUninstaller.swift in Sources */, 4BF0E5152AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 7BFE95592A9DF2AF0081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift in Sources */, 7BA7CC5C2AD120C30042E5CE /* EventMapping+NetworkProtectionError.swift in Sources */, @@ -11175,6 +11191,7 @@ 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, 7B25856C2BA2F2D000D49F79 /* AppLauncher.swift in Sources */, 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, + EE66418C2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift in Sources */, 7B7DFB202B7E736B009EA1A3 /* MacPacketTunnelProvider.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, @@ -14074,7 +14091,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 126.1.0; + version = 126.2.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -14774,6 +14791,10 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Subscription; }; + EE66418E2B9B1BD1005BCD17 /* Subscription */ = { + isa = XCSwiftPackageProductDependency; + productName = Subscription; + }; EE7295E22A545B9A008C0991 /* NetworkProtection */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7a11e2163c..7d241ed988 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" : "f4894b9c00dd7514c66d6b929c12315e0cd9c151", - "version" : "126.1.0" + "branch" : "graeme/expired-entitlements-stuff", + "revision" : "925d0dd50e47f38c7fe922622002e8961569bc32" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index d0aa5916c6..e4f938425a 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -82,7 +82,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel var privacyDashboardWindow: NSWindow? #if NETWORK_PROTECTION && SUBSCRIPTION - private let networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler() + // Needs to be lazy as indirectly depends on AppDelegate + private lazy var networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler() #endif #if DBP && SUBSCRIPTION diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index d73a1ea414..e0e752ce6f 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -143,8 +143,12 @@ final class URLEventHandler { case AppLaunchCommand.showVPNLocations.launchURL: WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) WindowControllersManager.shared.showLocationPickerSheet() - case AppLaunchCommand.moveAppToApplications.launchURL: +#if SUBSCRIPTION + case AppLaunchCommand.showPrivacyPro.launchURL: + WindowControllersManager.shared.showTab(with: .subscription(.subscriptionPurchase)) +#endif #if !APPSTORE && !DEBUG + case AppLaunchCommand.moveAppToApplications.launchURL: // this should be run after NSApplication.shared is set PFMoveToApplicationsFolderIfNecessary(false) #endif diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 8dae1a9c7f..e5950d0231 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -75,7 +75,7 @@ final class MainViewController: NSViewController { let ipcClient = TunnelControllerIPCClient(machServiceName: vpnBundleID) ipcClient.register() - return NetworkProtectionNavBarPopoverManager(ipcClient: ipcClient) + return NetworkProtectionNavBarPopoverManager(ipcClient: ipcClient, networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabler()) }() let networkProtectionStatusReporter: NetworkProtectionStatusReporter = { var connectivityIssuesObserver: ConnectivityIssueObserver! diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index aa050d3a98..fbe69492b5 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -438,14 +438,20 @@ final class NavigationBarViewController: NSViewController { object: nil) #if NETWORK_PROTECTION - NotificationCenter.default.addObserver(self, - selector: #selector(showVPNUninstalledFeedback(_:)), - name: NetworkProtectionFeatureDisabler.vpnUninstalledNotificationName, - object: nil) + UserDefaults.netP + .publisher(for: \.networkProtectionShouldShowVPNUninstalledMessage) + .receive(on: DispatchQueue.main) + .sink { [weak self] shouldShowUninstalledMessage in + if shouldShowUninstalledMessage { + self?.showVPNUninstalledFeedback() + UserDefaults.netP.networkProtectionShouldShowVPNUninstalledMessage = false + } + } + .store(in: &cancellables) #endif } - @objc private func showVPNUninstalledFeedback(_ sender: Notification) { + @objc private func showVPNUninstalledFeedback() { guard view.window?.isKeyWindow == true else { return } DispatchQueue.main.async { diff --git a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift index 0ec2416135..b85ed68d8b 100644 --- a/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift +++ b/DuckDuckGo/NetworkProtection/AppAndExtensionAndAgentTargets/AppLauncher.swift @@ -36,6 +36,7 @@ extension AppLaunchCommand { case .showVPNLocations: return "showVPNLocations" case .enableOnDemand: return "enableOnDemand" case .moveAppToApplications: return "moveAppToApplications" + case .showPrivacyPro: return "showPrivacyPro" } } } @@ -97,6 +98,8 @@ extension AppLaunchCommand { return "networkprotection://show-settings/locations" case .moveAppToApplications: return "networkprotection://move-app-to-applications" + case .showPrivacyPro: + return "networkprotection://show-privacy-pro" default: return nil } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index fcdb50ff75..8eed556fac 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -22,6 +22,10 @@ import Foundation import NetworkProtection import Common +#if SUBSCRIPTION +import Subscription +#endif + extension NetworkProtectionDeviceManager { static func create() -> NetworkProtectionDeviceManager { @@ -32,7 +36,7 @@ extension NetworkProtectionDeviceManager { tokenStore: tokenStore, keyStore: keyStore, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false) + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable()) } } @@ -42,16 +46,21 @@ extension NetworkProtectionCodeRedemptionCoordinator { self.init(environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false) + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable()) } } extension NetworkProtectionKeychainTokenStore { convenience init() { +#if SUBSCRIPTION + let accessTokenProvider: () -> String? = { AccountManager().accessToken } +#else + let accessTokenProvider: () -> String? = { return nil } +#endif self.init(keychainType: .default, errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false, - accessTokenProvider: { nil }) + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable(), + accessTokenProvider: accessTokenProvider) } } @@ -62,4 +71,16 @@ extension NetworkProtectionKeychainKeyStore { } } +extension NetworkProtectionLocationListCompositeRepository { + convenience init() { + let settings = VPNSettings(defaults: .netP) + self.init( + environment: settings.selectedEnvironment, + tokenStore: NetworkProtectionKeychainTokenStore(), + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable() + ) + } +} + #endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index ce62ed2734..23d20a89d2 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -72,9 +72,11 @@ final class NetworkProtectionAppEvents { /// func applicationDidBecomeActive() { guard featureVisibility.isNetworkProtectionVisible() else { + UserDefaults.netP.networkProtectionVPNEnabledViaWaitlist = false featureVisibility.disableForAllUsers() return } + UserDefaults.netP.networkProtectionVPNEnabledViaWaitlist = true } private func restartNetworkProtectionIfVersionChanged(using loginItemsManager: LoginItemsManager) { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 16853163e8..331892d0f5 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -24,6 +24,10 @@ import NetworkProtection import NetworkProtectionIPC import NetworkProtectionUI +#if SUBSCRIPTION +import Subscription +#endif + #if NETWORK_PROTECTION protocol NetworkProtectionIPCClient { @@ -34,6 +38,7 @@ protocol NetworkProtectionIPCClient { func start() func stop() } + extension TunnelControllerIPCClient: NetworkProtectionIPCClient { public var ipcStatusObserver: any NetworkProtection.ConnectionStatusObserver { connectionStatusObserver } public var ipcServerInfoObserver: any NetworkProtection.ConnectionServerInfoObserver { serverInfoObserver } @@ -43,9 +48,12 @@ extension TunnelControllerIPCClient: NetworkProtectionIPCClient { final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { private var networkProtectionPopover: NetworkProtectionPopover? let ipcClient: NetworkProtectionIPCClient + let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling - init(ipcClient: NetworkProtectionIPCClient) { + init(ipcClient: TunnelControllerIPCClient, + networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling) { self.ipcClient = ipcClient + self.networkProtectionFeatureDisabler = networkProtectionFeatureDisabler } var isShown: Bool { @@ -56,15 +64,8 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { #endif } - private func show(_ popover: NSPopover, positionedBelow view: NSView) { - view.isHidden = false - - popover.show(positionedBelow: view.bounds.insetFromLineOfDeath(flipped: view.isFlipped), in: view) - } - // swiftlint:disable:next function_body_length func show(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) { - let popover = networkProtectionPopover ?? { let controller = NetworkProtectionIPCTunnelController(ipcClient: ipcClient) @@ -117,8 +118,11 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { } }, agentLoginItem: LoginItem.vpnMenu, - isMenuBarStatusView: false - ) + isMenuBarStatusView: false, + userDefaults: .netP, + uninstallHandler: { [weak self] in + _ = await self?.networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: true) + }) popover.delegate = delegate networkProtectionPopover = popover @@ -128,6 +132,12 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { show(popover, positionedBelow: view) } + private func show(_ popover: NSPopover, positionedBelow view: NSView) { + view.isHidden = false + + popover.show(positionedBelow: view.bounds.insetFromLineOfDeath(flipped: view.isFlipped), in: view) + } + func toggle(positionedBelow view: NSView, withDelegate delegate: NSPopoverDelegate) { if let networkProtectionPopover, networkProtectionPopover.isShown { networkProtectionPopover.close() diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 619da111ff..8ecf83100d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -34,6 +34,10 @@ import SystemExtensionManager import SystemExtensions #endif +#if SUBSCRIPTION +import Subscription +#endif + typealias NetworkProtectionStatusChangeHandler = (NetworkProtection.ConnectionStatus) -> Void typealias NetworkProtectionConfigChangeHandler = () -> Void @@ -72,6 +76,12 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// Auth token store private let tokenStore: NetworkProtectionTokenStore +#if SUBSCRIPTION + // MARK: - Subscriptions + + private let accountManager = AccountManager() +#endif + // MARK: - Debug Options Support private let networkExtensionBundleID: String @@ -538,7 +548,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr var options = [String: NSObject]() options[NetworkProtectionOptionKey.activationAttemptId] = UUID().uuidString as NSString - guard let authToken = try tokenStore.fetchToken() as NSString? else { + guard let authToken = try fetchAuthToken() else { throw StartError.noAuthToken } options[NetworkProtectionOptionKey.authToken] = authToken @@ -738,6 +748,21 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr throw TunnelFailureError(errorDescription: errorMessage.value) } } + + private func fetchAuthToken() throws -> NSString? { +#if SUBSCRIPTION + if let accessToken = accountManager.accessToken { + os_log(.error, log: .networkProtection, "🟢 TunnelController found token: %{public}d", accessToken) + return Self.adaptAccessTokenForVPN(accessToken) as NSString? + } +#endif + os_log(.error, log: .networkProtection, "🔴 TunnelController found no token :(") + return try tokenStore.fetchToken() as NSString? + } + + private static func adaptAccessTokenForVPN(_ token: String) -> String { + "ddg:\(token)" + } } #endif diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift index fb1b74c2ff..24dfd9c4ab 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift @@ -185,18 +185,6 @@ extension VPNCityItemModel { } } -extension NetworkProtectionLocationListCompositeRepository { - convenience init() { - let settings = VPNSettings(defaults: .netP) - self.init( - environment: settings.selectedEnvironment, - tokenStore: NetworkProtectionKeychainTokenStore(), - errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: false - ) - } -} - extension VPNLocationViewModel { convenience init() { let locationListRepository = NetworkProtectionLocationListCompositeRepository() diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift index 7ad69d346b..9f7287b8a7 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionSubscriptionEventHandler.swift @@ -21,6 +21,7 @@ import Foundation import Subscription import NetworkProtection +import NetworkProtectionUI final class NetworkProtectionSubscriptionEventHandler { @@ -28,40 +29,60 @@ final class NetworkProtectionSubscriptionEventHandler { private let networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming private let networkProtectionTokenStorage: NetworkProtectionTokenStore private let networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling + private let userDefaults: UserDefaults init(accountManager: AccountManaging = AccountManager(), networkProtectionRedemptionCoordinator: NetworkProtectionCodeRedeeming = NetworkProtectionCodeRedemptionCoordinator(), networkProtectionTokenStorage: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), - networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler()) { + networkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler(), + userDefaults: UserDefaults = .netP) { self.accountManager = accountManager self.networkProtectionRedemptionCoordinator = networkProtectionRedemptionCoordinator self.networkProtectionTokenStorage = networkProtectionTokenStorage self.networkProtectionFeatureDisabler = networkProtectionFeatureDisabler + self.userDefaults = userDefaults + } + + private lazy var entitlementMonitor = NetworkProtectionEntitlementMonitor() + + private func setUpEntitlementMonitoring() { + guard AccountManager().isUserAuthenticated else { return } + let entitlementsCheck = { + await AccountManager().hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + } + + Task { + await entitlementMonitor.start(entitlementCheck: entitlementsCheck) { result in + switch result { + case .validEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = false + case .invalidEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = true + case .error: + break + } + } + } } func registerForSubscriptionAccountManagerEvents() { NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignIn), name: .accountDidSignIn, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignOut), name: .accountDidSignOut, object: nil) + setUpEntitlementMonitoring() } @objc private func handleAccountDidSignIn() { - guard let token = accountManager.accessToken else { + guard accountManager.accessToken != nil else { assertionFailure("[NetP Subscription] AccountManager signed in but token could not be retrieved") return } - - Task { - do { - try await networkProtectionRedemptionCoordinator.exchange(accessToken: token) - print("[NetP Subscription] Exchanged access token for auth token successfully") - } catch { - print("[NetP Subscription] Failed to exchange access token for auth token: \(error)") - } - } + userDefaults.networkProtectionEntitlementsExpired = false + setUpEntitlementMonitoring() } @objc private func handleAccountDidSignOut() { print("[NetP Subscription] Deleted NetP auth token after signing out from Privacy Pro") + userDefaults.networkProtectionEntitlementsExpired = true Task { await networkProtectionFeatureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift index eefd12cff5..3977fccef5 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift @@ -47,6 +47,7 @@ extension UNNotificationCategory { /// This class takes care of requesting the presentation of notifications using UNNotificationCenter /// final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtectionNotificationsPresenter { + private static let threadIdentifier = "com.duckduckgo.NetworkProtectionNotificationsManager.threadIdentifier" private let appLauncher: AppLauncher @@ -134,6 +135,12 @@ final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtecti showNotification(.superseded, content) } + func showEntitlementNotification() { + let content = notificationContent(title: UserText.networkProtectionEntitlementExpiredNotificationTitle, + subtitle: UserText.networkProtectionEntitlementExpiredNotificationBody) + showNotification(.expiredEntitlement, content) + } + func showTestNotification() { // These strings are deliberately hardcoded as we don't want them localized, they're only for debugging: let content = notificationContent(title: "Test notification", @@ -141,10 +148,6 @@ final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtecti showNotification(.test, content) } - func showEntitlementNotification() { - // todo - } - private func showNotification(_ identifier: NetworkProtectionNotificationIdentifier, _ content: UNNotificationContent) { let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: .none) @@ -166,6 +169,7 @@ public enum NetworkProtectionNotificationIdentifier: String { case reconnecting = "network-protection.notification.reconnecting" case connected = "network-protection.notification.connected" case superseded = "network-protection.notification.superseded" + case expiredEntitlement = "network-protection.notification.expired-entitlement" case test = "network-protection.notification.test" } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift index 607fb081be..ecc8f96073 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionAndNotificationTargets/UserText+NetworkProtectionExtensions.swift @@ -43,4 +43,6 @@ final class UserText { static let networkProtectionSupersededNotificationSubtitle = NSLocalizedString("network.protection.superceded.notification.subtitle", value: "Another VPN app on your Mac may have disabled it.", comment: "The subtitle of the notification shown when VPN connection is replaced by another app VPN connection taking over") static let networkProtectionSupersededReconnectActionTitle = NSLocalizedString("network.protection.superceded.action.reconnect.title", value: "Reconnect", comment: "The title of the `Reconnect` notification action button shown when VPN connection is replaced by another app VPN connection taking over") + static let networkProtectionEntitlementExpiredNotificationTitle = NSLocalizedString("network.protection.entitlement.expired.notification.title", value: "VPN disconnected", comment: "The title of the notification when Privacy Pro subscription expired") + static let networkProtectionEntitlementExpiredNotificationBody = NSLocalizedString("network.protection.entitlement.expired.notification.body", value: "Subscribe to Privacy Pro to reconnect DuckDuckGo VPN.", comment: "The body of the notification when Privacy Pro subscription expired") } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index da8d81fc24..acb06735b3 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -24,6 +24,10 @@ import NetworkExtension import Networking import PixelKit +#if SUBSCRIPTION +import Subscription +#endif + final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - Additional Status Info @@ -265,6 +269,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - Initialization @objc public init() { + let isSubscriptionEnabled = false + #if NETP_SYSTEM_EXTENSION let defaults = UserDefaults.standard #else @@ -274,12 +280,26 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) - let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, - serviceName: Self.tokenServiceName, - errorEvents: debugEvents, - isSubscriptionEnabled: false, - accessTokenProvider: { nil }) let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings, defaults: defaults) + let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType, + serviceName: Self.tokenServiceName, + errorEvents: debugEvents, + isSubscriptionEnabled: isSubscriptionEnabled, + accessTokenProvider: { nil } + ) +#if SUBSCRIPTION + + let accountManager = AccountManager( + accessTokenStorage: tokenStore, + entitlementsCache: UserDefaultsCache<[Entitlement]>(key: UserDefaultsCacheKey.subscriptionEntitlements) + ) + SubscriptionPurchaseEnvironment.currentServiceEnvironment = settings.selectedEnvironment == .production ? .production : .staging + let entitlementsCheck = { + await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + } +#else + let entitlementsCheck: (() async -> Result)? = nil +#endif super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: tunnelHealthStore, @@ -290,8 +310,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { providerEvents: Self.packetTunnelProviderEvents, settings: settings, defaults: defaults, - isSubscriptionEnabled: false, - entitlementCheck: nil) + isSubscriptionEnabled: isSubscriptionEnabled, + entitlementCheck: entitlementsCheck) setupPixels() observeServerChanges() diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift new file mode 100644 index 0000000000..2987ab2ff9 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift @@ -0,0 +1,45 @@ +// +// NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.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. +// + +#if SUBSCRIPTION + +import Foundation +import Subscription +import NetworkProtection +import Common + +extension NetworkProtectionKeychainTokenStore: SubscriptionTokenStorage { + public func store(accessToken: String) throws { + try store(accessToken) + } + + public func getAccessToken() throws -> String? { + guard var token = try fetchToken() else { return nil } + if token.hasPrefix("ddg:") { + token = token.replacingOccurrences(of: "ddg:", with: "") + } + os_log("🔵 Wrapper successfully fetched token %{token}@", log: .networkProtection, type: .info, token) + return token + } + + public func removeAccessToken() throws { + try deleteToken() + } +} + +#endif diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index 97c5a06a42..18cbf84cda 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -21,6 +21,10 @@ import Combine import DDGSync import SwiftUI +#if SUBSCRIPTION +import Subscription +#endif + final class PreferencesSidebarModel: ObservableObject { let tabSwitcherTabs: [Tab.TabContent] @@ -75,10 +79,13 @@ final class PreferencesSidebarModel: ObservableObject { tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes, privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, syncService: DDGSyncing, - includeDuckPlayer: Bool + includeDuckPlayer: Bool, + userDefaults: UserDefaults = .netP ) { let loadSections = { -#if NETWORK_PROTECTION +#if SUBSCRIPTION + let includingVPN = !userDefaults.networkProtectionEntitlementsExpired && DefaultNetworkProtectionVisibility().isOnboarded +#elseif NETWORK_PROTECTION let includingVPN = DefaultNetworkProtectionVisibility().isOnboarded #else let includingVPN = false @@ -113,6 +120,16 @@ final class PreferencesSidebarModel: ObservableObject { self.refreshSections() } .store(in: &cancellables) + + UserDefaults.netP.publisher(for: \.networkProtectionEntitlementsExpired) + .receive(on: DispatchQueue.main) + .sink { [weak self] entitlementsExpired in + guard let self else { return } + if !entitlementsExpired && self.selectedPane == .vpn { + self.selectedPane = .general + } + self.refreshSections() + }.store(in: &cancellables) } #endif diff --git a/DuckDuckGo/Subscription/AccountManagerExtension.swift b/DuckDuckGo/Subscription/AccountManagerExtension.swift index c1758fb2e5..5f30dad322 100644 --- a/DuckDuckGo/Subscription/AccountManagerExtension.swift +++ b/DuckDuckGo/Subscription/AccountManagerExtension.swift @@ -17,6 +17,7 @@ // #if SUBSCRIPTION +import Foundation import Subscription public extension AccountManager { diff --git a/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift b/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift index 6599bd0ee9..9090ffad02 100644 --- a/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift +++ b/DuckDuckGo/Subscription/SubscriptionFeatureAvailability.swift @@ -24,6 +24,7 @@ import Subscription #if NETWORK_PROTECTION import NetworkProtection +import BrowserServicesKit #endif protocol SubscriptionFeatureAvailability { @@ -36,8 +37,8 @@ struct DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { #if SUBSCRIPTION_OVERRIDE_ENABLED return true #elseif SUBSCRIPTION - print("isUserAuthenticated: [\(AccountManager().isUserAuthenticated)] | isSubscriptionInternalTestingEnabled: [\(isSubscriptionInternalTestingEnabled)] isInternalUser: [\(isInternalUser)] | isVPNActivated: [\(isVPNActivated)] | isDBPActivated: [\(isDBPActivated)]") - return AccountManager().isUserAuthenticated || (isSubscriptionInternalTestingEnabled && isInternalUser && !isVPNActivated && !isDBPActivated) + print("isUserAuthenticated: [\(AccountManager().isUserAuthenticated)] | isSubscriptionInternalTestingEnabled: [\(isSubscriptionInternalTestingEnabled)] isInternalUser: [\(isInternalUser)] | isDBPActivated: [\(isDBPActivated)]") + return AccountManager().isUserAuthenticated || (isSubscriptionInternalTestingEnabled && isInternalUser && !isDBPActivated) #else return false #endif @@ -48,15 +49,7 @@ struct DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { } private var isInternalUser: Bool { - NSApp.delegateTyped.internalUserDecider.isInternalUser - } - - private var isVPNActivated: Bool { -#if NETWORK_PROTECTION - return NetworkProtectionKeychainTokenStore().isFeatureActivated -#else - return false -#endif + Self.internalUserDecider.isInternalUser } private var isDBPActivated: Bool { @@ -66,4 +59,18 @@ struct DefaultSubscriptionFeatureAvailability: SubscriptionFeatureAvailability { return false #endif } + + private static var internalUserDecider: InternalUserDecider = { + let keyStore = EncryptionKeyStore() + let fileStore: FileStore + do { + let encryptionKey = NSApplication.runType.requiresEnvironment ? try keyStore.readKey() : nil + fileStore = EncryptedFileStore(encryptionKey: encryptionKey) + } catch { + fileStore = EncryptedFileStore() + } + + let internalUserDeciderStore = InternalUserDeciderStore(fileStore: fileStore) + return DefaultInternalUserDecider(store: internalUserDeciderStore) + }() } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift index f33b15c8c7..dd5572e94c 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift @@ -33,8 +33,6 @@ protocol NetworkProtectionFeatureDisabling { } final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling { - static let vpnUninstalledNotificationName = NSNotification.Name(rawValue: "com.duckduckgo.NetworkProtection.uninstalled") - private let log: OSLog private let loginItemsManager: LoginItemsManager private let pinningManager: LocalPinningManager @@ -91,7 +89,7 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling } unpinNetworkProtection() - postVPNUninstalledNotification() + notifyVPNUninstalled() return true } @@ -126,14 +124,11 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling settings.resetToDefaults() } - private func postVPNUninstalledNotification() { - Task { @MainActor in + private func notifyVPNUninstalled() { // Wait a bit since the NetP button is likely being hidden + Task { try? await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC) - - NotificationCenter.default.post( - name: Self.vpnUninstalledNotificationName, - object: nil) + userDefaults.networkProtectionShouldShowVPNUninstalledMessage = true } } } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index 6e9386c491..08d52679a3 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -24,6 +24,7 @@ import Common import NetworkExtension import NetworkProtection import NetworkProtectionUI +import LoginItems protocol NetworkProtectionFeatureVisibility { func isNetworkProtectionVisible() -> Bool @@ -72,11 +73,15 @@ 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 { +#if SUBSCRIPTION + return defaults.networkProtectionEntitlementsExpired && LoginItem.vpnMenu.status.isInstalled +#else let waitlistAccessEnded = isWaitlistUser && !waitlistIsOngoing let isNotEasterEggUser = !isEasterEggUser let isOnboarded = UserDefaults.netP.networkProtectionOnboardingStatus != .default return isNotEasterEggUser && waitlistAccessEnded && isOnboarded +#endif } /// Whether the user is fully onboarded diff --git a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift index bd226a82c6..41677dc8f1 100644 --- a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift +++ b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift @@ -111,6 +111,12 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate os_log("Got notification: listener started") self?.notificationsPresenter.requestAuthorization() }.store(in: &cancellables) + + distributedNotificationCenter.publisher(for: .showExpiredEntitlementNotification) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.showEntitlementNotification() + }.store(in: &cancellables) } func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { @@ -139,6 +145,12 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate notificationsPresenter.showSupersededNotification() } + func showEntitlementNotification() { + os_log("Presenting Entitlements notification", log: .networkProtection, type: .info) + + notificationsPresenter.showEntitlementNotification() + } + func showTestNotification() { os_log("Presenting test notification", log: .networkProtection, type: .info) notificationsPresenter.showTestNotification() diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 4ca0fe2aa5..c44f70bbd7 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -47,6 +47,7 @@ final class DuckDuckGoVPNApplication: NSApplication { super.init() self.delegate = _delegate + #if DEBUG && SUBSCRIPTION let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) @@ -144,7 +145,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { }() private func handleControllerEvent(_ event: TransparentProxyController.Event) { - PixelKit.fire(event) + } @MainActor @@ -200,6 +201,10 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { VPNAppEventsHandler(tunnelController: tunnelController) }() + private lazy var vpnUninstaller: VPNUninstaller = { + VPNUninstaller(networkExtensionController: networkExtensionController, vpnConfigurationManager: VPNConfigurationManager()) + }() + /// The status bar NetworkProtection menu /// /// For some reason the App will crash if this is initialized right away, which is why it was changed to be lazy. @@ -249,12 +254,21 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { ] }, agentLoginItem: nil, - isMenuBarStatusView: true) + isMenuBarStatusView: true, + userDefaults: .netP, + uninstallHandler: { [weak self] in + guard let self else { return } + await self.vpnUninstaller.uninstall(includingSystemExtension: true) + } + ) } @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { APIRequest.Headers.setUserAgent(UserAgent.duckDuckGoUserAgent()) +#if SUBSCRIPTION + SubscriptionPurchaseEnvironment.currentServiceEnvironment = tunnelSettings.selectedEnvironment == .production ? .production : .staging +#endif os_log("DuckDuckGoVPN started", log: .networkProtectionLoginItemLog, type: .info) @@ -308,6 +322,8 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { let launchedOnStartup = launchInformation.wasLaunchedByStartup launchInformation.update() + setUpSubscriptionMonitoring() + if launchedOnStartup { Task { let isConnected = await tunnelController.isConnected @@ -338,6 +354,38 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { } }.store(in: &cancellables) } + + private lazy var entitlementMonitor = NetworkProtectionEntitlementMonitor() + + private func setUpSubscriptionMonitoring() { +#if SUBSCRIPTION + guard AccountManager().isUserAuthenticated else { return } + let entitlementsCheck = { + await AccountManager().hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + } + + Task { + await entitlementMonitor.start(entitlementCheck: entitlementsCheck) { [weak self] result in + switch result { + case .validEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = false + case .invalidEntitlement: + UserDefaults.netP.networkProtectionEntitlementsExpired = true + guard let self else { return } + Task { + let isConnected = await self.tunnelController.isConnected + if isConnected { + await self.tunnelController.stop() + DistributedNotificationCenter.default().post(.showExpiredEntitlementNotification) + } + } + case .error: + break + } + } + } +#endif + } } extension NSApplication { diff --git a/DuckDuckGoVPN/NetworkProtection+VPNAgentConvenienceInitializers.swift b/DuckDuckGoVPN/NetworkProtection+VPNAgentConvenienceInitializers.swift new file mode 100644 index 0000000000..b61f1779c1 --- /dev/null +++ b/DuckDuckGoVPN/NetworkProtection+VPNAgentConvenienceInitializers.swift @@ -0,0 +1,33 @@ +// +// NetworkProtection+VPNAgentConvenienceInitializers.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 + +#if SUBSCRIPTION +import Subscription +#endif + +extension NetworkProtectionKeychainTokenStore { + convenience init() { + self.init(keychainType: .default, + errorEvents: .networkProtectionAppDebugEvents, + isSubscriptionEnabled: false, + accessTokenProvider: { return nil }) + } +} diff --git a/DuckDuckGoVPN/NetworkProtectionBouncer.swift b/DuckDuckGoVPN/NetworkProtectionBouncer.swift index 52963e7b61..e34540d016 100644 --- a/DuckDuckGoVPN/NetworkProtectionBouncer.swift +++ b/DuckDuckGoVPN/NetworkProtectionBouncer.swift @@ -22,6 +22,10 @@ import NetworkProtection import ServiceManagement import AppKit +#if SUBSCRIPTION +import Subscription +#endif + /// Class that implements the necessary logic to ensure the VPN is enabled, or prevent the app from running otherwise. /// final class NetworkProtectionBouncer { @@ -30,13 +34,31 @@ final class NetworkProtectionBouncer { /// current app. /// func requireAuthTokenOrKillApp(controller: TunnelController) async { +#if SUBSCRIPTION + let accountManager = AccountManager(subscriptionAppGroup: Bundle.main.appGroup(bundle: .subs)) + let result = await accountManager.hasEntitlement(for: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) + switch result { + case .success(true): + return + case .failure: + break + case .success(false): + os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized. Missing entitlement.") + await controller.stop() + + // EXIT_SUCCESS ensures the login item won't relaunch + // Ref: https://developer.apple.com/documentation/servicemanagement/smappservice/register() + // See where it mentions: + // "If the helper crashes or exits with a non-zero status, the system relaunches it" + exit(EXIT_SUCCESS) + } +#endif let keychainStore = NetworkProtectionKeychainTokenStore(keychainType: .default, errorEvents: nil, isSubscriptionEnabled: false, accessTokenProvider: { nil }) - guard keychainStore.isFeatureActivated else { - os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized.") + os_log(.error, log: .networkProtection, "🔴 Stopping: DuckDuckGo VPN not authorized. Missing token.") await controller.stop() diff --git a/DuckDuckGoVPN/VPNUninstaller.swift b/DuckDuckGoVPN/VPNUninstaller.swift new file mode 100644 index 0000000000..52593d9faa --- /dev/null +++ b/DuckDuckGoVPN/VPNUninstaller.swift @@ -0,0 +1,60 @@ +// +// VPNUninstaller.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 NetworkProtectionIPC + +protocol VPNUninstalling { + func uninstall(includingSystemExtension: Bool) async +} + +final class VPNUninstaller: VPNUninstalling { + let networkExtensionController: NetworkExtensionController + let vpnConfiguration: VPNConfigurationManager + let defaults: UserDefaults + + init(networkExtensionController: NetworkExtensionController, vpnConfigurationManager: VPNConfigurationManager, defaults: UserDefaults = .netP) { + self.networkExtensionController = networkExtensionController + self.vpnConfiguration = vpnConfigurationManager + self.defaults = defaults + } + + func uninstall(includingSystemExtension: Bool) async { +#if NETP_SYSTEM_EXTENSION + if includingSystemExtension { + do { + try await networkExtensionController.deactivateSystemExtension() + defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowExtension) + } catch { + + } + } +#endif + + await vpnConfiguration.removeVPNConfiguration() + + if defaults.networkProtectionOnboardingStatus == .completed { + defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowVPNConfiguration) + } + + defaults.networkProtectionShouldShowVPNUninstalledMessage = true + + exit(EXIT_SUCCESS) + } +} diff --git a/LocalPackages/LoginItems/Package.swift b/LocalPackages/LoginItems/Package.swift index b1a72a5caa..0e2d71d1b3 100644 --- a/LocalPackages/LoginItems/Package.swift +++ b/LocalPackages/LoginItems/Package.swift @@ -14,6 +14,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") ], targets: [ .target( diff --git a/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift b/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift index 87fda099f7..48a16a8689 100644 --- a/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift +++ b/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift @@ -52,6 +52,10 @@ public struct LoginItem: Equatable, Hashable { self == .enabled } + public var isInstalled: Bool { + self == .enabled || self == .requiresApproval + } + @available(macOS 13.0, *) public init(_ status: SMAppService.Status) { switch status { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift index 2e408557b0..e64f0a605a 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/AppLaunching/AppLaunching.swift @@ -32,6 +32,7 @@ public enum AppLaunchCommand: Codable { case stopVPN case enableOnDemand case moveAppToApplications + case showPrivacyPro } public protocol AppLaunching { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+ShowVPNUninstalledMessage.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+ShowVPNUninstalledMessage.swift new file mode 100644 index 0000000000..8f85e818ea --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+ShowVPNUninstalledMessage.swift @@ -0,0 +1,44 @@ +// +// UserDefault+ShowVPNUninstalledMessage.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 + +public extension UserDefaults { + private enum Key { + static var networkProtectionShouldShowVPNUninstalledMessage = "networkProtectionShouldShowVPNUninstalledMessage" + } + + // Convenience declaration + private var networkProtectionShowVPNUninstalledMessageRawValueKey: String { + Key.networkProtectionShouldShowVPNUninstalledMessage + } + + /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` + /// extension, and the key for this property must match its name exactly. + /// + @objc + dynamic var networkProtectionShouldShowVPNUninstalledMessage: Bool { + get { + value(forKey: networkProtectionShowVPNUninstalledMessageRawValueKey) as? Bool ?? false + } + + set { + set(newValue, forKey: networkProtectionShowVPNUninstalledMessageRawValueKey) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift new file mode 100644 index 0000000000..397f67f6ac --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefault+VPNEnabledViaWaitlist.swift @@ -0,0 +1,44 @@ +// +// UserDefault+VPNEnabledViaWaitlist.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 + +public extension UserDefaults { + private enum Key { + static var networkProtectionVPNEnabledViaWaitlist = "networkProtectionVPNEnabledViaWaitlist" + } + + // Convenience declaration + private var networkProtectionVPNEnabledViaWaitlistRawValueKey: String { + Key.networkProtectionVPNEnabledViaWaitlist + } + + /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` + /// extension, and the key for this property must match its name exactly. + /// + @objc + dynamic var networkProtectionVPNEnabledViaWaitlist: Bool { + get { + value(forKey: networkProtectionVPNEnabledViaWaitlistRawValueKey) as? Bool ?? false + } + + set { + set(newValue, forKey: networkProtectionVPNEnabledViaWaitlistRawValueKey) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefaults+NetworkProtectionExpiredEntitlements.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefaults+NetworkProtectionExpiredEntitlements.swift new file mode 100644 index 0000000000..0768592eff --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserDefaults+NetworkProtectionExpiredEntitlements.swift @@ -0,0 +1,44 @@ +// +// UserDefaults+NetworkProtectionExpiredEntitlements.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 + +public extension UserDefaults { + private enum Key { + static var networkProtectionEntitlementsExpired = "networkProtectionEntitlementsExpired" + } + + // Convenience declaration + private var networkProtectionEntitlementsExpiredRawValueKey: String { + Key.networkProtectionEntitlementsExpired + } + + /// For KVO to work across processes (Menu App + Main App) we need to declare this dynamic var in a `UserDefaults` + /// extension, and the key for this property must match its name exactly. + /// + @objc + dynamic var networkProtectionEntitlementsExpired: Bool { + get { + value(forKey: networkProtectionEntitlementsExpiredRawValueKey) as? Bool ?? false + } + + set { + set(newValue, forKey: networkProtectionEntitlementsExpiredRawValueKey) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift index 1ec0e4d17a..402f85098f 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift @@ -67,4 +67,11 @@ final class UserText { let localized = NSLocalizedString("network.protection.server.location.link", value: "%@...", comment: "Clickable text linking to the server location picker screen") return String(format: localized, location) } + + // MARK: Subscription Expired + + static let networkProtectionSubscriptionExpiredTitle = NSLocalizedString("network.protection.subscription.expired.title", value: "VPN disconnected", comment: "Title for the prompt that tells the user their subscription expired.") + static let networkProtectionSubscriptionExpiredSubtitle = NSLocalizedString("network.protection.subscription.expired.subtitle", value: "Subscribe to Privacy Pro to reconnect DuckDuckGo VPN.", comment: "Subtitle for the prompt that tells the user their subscription expired.") + static let networkProtectionSubscriptionExpiredResubscribeButton = NSLocalizedString("network.protection.subscription.expired.resubscribe.button", value: "Subscribe to Privacy Pro", comment: "Button for the prompt that takes the user to the page to resubscribe.") + static let networkProtectionSubscriptionExpiredUninstallButton = NSLocalizedString("network.protection.subscription.expired.uninstall.button", value: "Uninstall DuckDuckGo VPN", comment: "Button for the prompt that uninstalls the VPN.") } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift index 488970f7d4..1833f8482e 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/StatusBarMenu.swift @@ -56,7 +56,9 @@ public final class StatusBarMenu: NSObject { appLauncher: AppLaunching, menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, - isMenuBarStatusView: Bool) { + isMenuBarStatusView: Bool, + userDefaults: UserDefaults, + uninstallHandler: @escaping () async -> Void) { self.model = model let statusItem = statusItem ?? NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) @@ -69,7 +71,10 @@ public final class StatusBarMenu: NSObject { appLauncher: appLauncher, menuItems: menuItems, agentLoginItem: agentLoginItem, - isMenuBarStatusView: isMenuBarStatusView) + isMenuBarStatusView: isMenuBarStatusView, + userDefaults: userDefaults, + uninstallHandler: uninstallHandler) + popover.behavior = .transient super.init() diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift index efbe5872eb..0acbf84065 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift @@ -56,7 +56,9 @@ public final class NetworkProtectionPopover: NSPopover { appLauncher: AppLaunching, menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, - isMenuBarStatusView: Bool) { + isMenuBarStatusView: Bool, + userDefaults: UserDefaults, + uninstallHandler: @escaping () async -> Void) { self.statusReporter = statusReporter self.model = NetworkProtectionStatusView.Model(controller: controller, @@ -66,7 +68,9 @@ public final class NetworkProtectionPopover: NSPopover { appLauncher: appLauncher, menuItems: menuItems, agentLoginItem: agentLoginItem, - isMenuBarStatusView: isMenuBarStatusView) + isMenuBarStatusView: isMenuBarStatusView, + userDefaults: userDefaults, + uninstallHandler: uninstallHandler) super.init() diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index a70e22f729..aae033b17d 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -50,7 +50,14 @@ public struct NetworkProtectionStatusView: View { public var body: some View { VStack(spacing: 0) { - if let promptActionViewModel = model.promptActionViewModel { + if model.shouldShowSubscriptionExpired { + SubscriptionExpiredView { + model.openPrivacyPro() + } uninstallButtonHandler: { + model.uninstallVPN() + } + .padding(5) + } else if let promptActionViewModel = model.promptActionViewModel { PromptActionView(model: promptActionViewModel) .padding(.horizontal, 5) .padding(.top, 5) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 454ec28102..7df51daf58 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -57,7 +57,7 @@ extension NetworkProtectionStatusView { private(set) var onboardingStatus: OnboardingStatus = .completed var tunnelControllerViewDisabled: Bool { - onboardingStatus != .completed || loginItemNeedsApproval + onboardingStatus != .completed || loginItemNeedsApproval || shouldShowSubscriptionExpired } @MainActor @@ -94,6 +94,10 @@ extension NetworkProtectionStatusView { /// private let runLoopMode: RunLoop.Mode? + private let appLauncher: AppLaunching + + private let uninstallHandler: () async -> Void + private var cancellables = Set() // MARK: - Dispatch Queues @@ -113,7 +117,9 @@ extension NetworkProtectionStatusView { menuItems: @escaping () -> [MenuItem], agentLoginItem: LoginItem?, isMenuBarStatusView: Bool, - runLoopMode: RunLoop.Mode? = nil) { + runLoopMode: RunLoop.Mode? = nil, + userDefaults: UserDefaults, + uninstallHandler: @escaping () async -> Void) { self.tunnelController = controller self.onboardingStatusPublisher = onboardingStatusPublisher @@ -123,6 +129,8 @@ extension NetworkProtectionStatusView { self.agentLoginItem = agentLoginItem self.isMenuBarStatusView = isMenuBarStatusView self.runLoopMode = runLoopMode + self.appLauncher = appLauncher + self.uninstallHandler = uninstallHandler tunnelControllerViewModel = TunnelControllerViewModel(controller: tunnelController, onboardingStatusPublisher: onboardingStatusPublisher, @@ -146,8 +154,14 @@ extension NetworkProtectionStatusView { onboardingStatusPublisher .receive(on: DispatchQueue.main) .sink { [weak self] status in - self?.onboardingStatus = status - } + self?.onboardingStatus = status + } + .store(in: &cancellables) + + userDefaults + .publisher(for: \.networkProtectionEntitlementsExpired) + .receive(on: DispatchQueue.main) + .assign(to: \.shouldShowSubscriptionExpired, onWeaklyHeld: self) .store(in: &cancellables) } @@ -164,6 +178,18 @@ extension NetworkProtectionStatusView { } } + func openPrivacyPro() { + Task { + await appLauncher.launchApp(withCommand: .showPrivacyPro) + } + } + + func uninstallVPN() { + Task { + await uninstallHandler() + } + } + private func subscribeToStatusChanges() { statusReporter.statusObserver.publisher .receive(on: DispatchQueue.main) @@ -280,6 +306,9 @@ extension NetworkProtectionStatusView { let tunnelControllerViewModel: TunnelControllerViewModel + @Published + var shouldShowSubscriptionExpired: Bool = false + var promptActionViewModel: PromptActionView.Model? { #if !APPSTORE && !DEBUG guard Bundle.main.isInApplicationDirectory else { diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SubscriptionExpiredView/SubscriptionExpiredView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SubscriptionExpiredView/SubscriptionExpiredView.swift new file mode 100644 index 0000000000..a4fe590b8e --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SubscriptionExpiredView/SubscriptionExpiredView.swift @@ -0,0 +1,74 @@ +// +// SubscriptionExpiredView.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 SwiftUI +import SwiftUIExtensions + +struct SubscriptionExpiredView: View { + enum Constants { + static let backgroundCornerRadius = 6.0 + } + + let subscribeButtonHandler: () -> Void + let uninstallButtonHandler: () -> Void + + public var body: some View { + VStack(alignment: .leading, spacing: 5) { + Text(UserText.networkProtectionSubscriptionExpiredTitle) + .font(.system(size: 13).weight(.bold)) + .foregroundColor(Color(.defaultText)) + .multilineText() + + Text(UserText.networkProtectionSubscriptionExpiredSubtitle) + .font(.system(size: 13)) + .foregroundColor(Color(.defaultText)) + .multilineText() + + Button(UserText.networkProtectionSubscriptionExpiredResubscribeButton, action: subscribeButtonHandler) + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + .padding(.top, 3) + + Divider() + .padding(.top, 8) + .padding(.bottom, 3) + + Button(UserText.networkProtectionSubscriptionExpiredUninstallButton, action: uninstallButtonHandler) + .buttonStyle(.borderless) + .foregroundColor(.accentColor) + .padding(.top, 3) + } + .padding(.vertical, 16) + .padding(.horizontal, 10) + .cornerRadius(8) + .background( + RoundedRectangle(cornerRadius: Constants.backgroundCornerRadius, style: .circular) + .stroke(Color(.onboardingStepBorder), lineWidth: 1) + .background( + RoundedRectangle(cornerRadius: Constants.backgroundCornerRadius, style: .circular) + .fill(Color(.onboardingStepBackground)) + ) + ) + } +} + +struct SubscriptionExpiredView_Preview: PreviewProvider { + static var previews: some View { + SubscriptionExpiredView(subscribeButtonHandler: {}, uninstallButtonHandler: {}) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift index b680b062b6..5b9e7e0dea 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift @@ -56,7 +56,9 @@ final class StatusBarMenuTests: XCTestCase { appLauncher: MockAppLauncher(), menuItems: { [] }, agentLoginItem: nil, - isMenuBarStatusView: false) + isMenuBarStatusView: false, + userDefaults: .standard, + uninstallHandler: { }) menu.show() @@ -79,7 +81,9 @@ final class StatusBarMenuTests: XCTestCase { appLauncher: MockAppLauncher(), menuItems: { [] }, agentLoginItem: nil, - isMenuBarStatusView: false) + isMenuBarStatusView: false, + userDefaults: .standard, + uninstallHandler: { }) menu.hide() diff --git a/LocalPackages/SyncUI/Package.swift b/LocalPackages/SyncUI/Package.swift index 63994d4a45..4693eb6b4a 100644 --- a/LocalPackages/SyncUI/Package.swift +++ b/LocalPackages/SyncUI/Package.swift @@ -15,6 +15,7 @@ let package = Package( dependencies: [ .package(path: "../SwiftUIExtensions"), .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") ], targets: [ .target( diff --git a/LocalPackages/SystemExtensionManager/Package.swift b/LocalPackages/SystemExtensionManager/Package.swift index bf7cce608f..58eeb26cc1 100644 --- a/LocalPackages/SystemExtensionManager/Package.swift +++ b/LocalPackages/SystemExtensionManager/Package.swift @@ -17,6 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/apple-toolbox.git", exact: "2.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", branch: "graeme/expired-entitlements-stuff") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. diff --git a/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift b/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift index 7787d3d93b..8f2ffe08d6 100644 --- a/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift +++ b/NetworkProtectionSystemExtension/NetworkProtectionAgentNotificationsPresenter.swift @@ -48,11 +48,11 @@ final class NetworkProtectionAgentNotificationsPresenter: NetworkProtectionNotif notificationCenter.post(.showVPNSupersededNotification) } - func showTestNotification() { - notificationCenter.post(.showTestNotification) + func showEntitlementNotification() { + notificationCenter.post(.showExpiredEntitlementNotification) } - func showEntitlementNotification() { - // todo + func showTestNotification() { + notificationCenter.post(.showTestNotification) } }