diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5dd35362b5..d5b9ce06ae 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1107,6 +1107,15 @@ 4B3F641E27A8D3BD00E0C118 /* BrowserProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3F641D27A8D3BD00E0C118 /* BrowserProfileTests.swift */; }; 4B4032842AAAC24400CCA602 /* WaitlistActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4032832AAAC24400CCA602 /* WaitlistActivationDateStore.swift */; }; 4B4032852AAAC24400CCA602 /* WaitlistActivationDateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4032832AAAC24400CCA602 /* WaitlistActivationDateStore.swift */; }; + 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */; }; + 4B41EDA12B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */; }; + 4B41EDA32B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDA22B1543B9001EEDF4 /* VPNPreferencesModel.swift */; }; + 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDA22B1543B9001EEDF4 /* VPNPreferencesModel.swift */; }; + 4B41EDA52B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDA22B1543B9001EEDF4 /* VPNPreferencesModel.swift */; }; + 4B41EDA72B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDA62B1543C9001EEDF4 /* PreferencesVPNView.swift */; }; + 4B41EDA82B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDA62B1543C9001EEDF4 /* PreferencesVPNView.swift */; }; + 4B41EDA92B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDA62B1543C9001EEDF4 /* PreferencesVPNView.swift */; }; + 4B41EDAB2B1544B2001EEDF4 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 4B41EDAA2B1544B2001EEDF4 /* LoginItems */; }; 4B434690285ED7A100177407 /* BookmarksBarViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B43468F285ED7A100177407 /* BookmarksBarViewModelTests.swift */; }; 4B43469528655D1400177407 /* FirefoxDataImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B43469428655D1400177407 /* FirefoxDataImporterTests.swift */; }; 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC382A11B509001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift */; }; @@ -1339,7 +1348,6 @@ 4B9579A32AC7AE700062CA31 /* PopUpButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBE0AA627B9B027003B37A8 /* PopUpButton.swift */; }; 4B9579A42AC7AE700062CA31 /* NetworkProtectionInviteDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D606C2A0B29FA00BCD287 /* NetworkProtectionInviteDialog.swift */; }; 4B9579A52AC7AE700062CA31 /* SuggestionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABEE6A424AA0A7F0043105B /* SuggestionViewController.swift */; }; - 4B9579A62AC7AE700062CA31 /* NetworkProtectionUserDefaultsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F92BA42A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift */; }; 4B9579A72AC7AE700062CA31 /* AddEditFavoriteWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E7A27BBB8620038AD11 /* AddEditFavoriteWindow.swift */; }; 4B9579A82AC7AE700062CA31 /* BWKeyStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6216B129069BBF00386B2C /* BWKeyStorage.swift */; }; 4B9579A92AC7AE700062CA31 /* VisitViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E919E287872EA00AB6B62 /* VisitViewModel.swift */; }; @@ -2902,13 +2910,6 @@ B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6F7128029F681EB00594A45 /* QuickLookUI.framework */; }; B6F92BA22A691580002ABA6B /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; }; - B6F92BA52A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F92BA42A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift */; }; - B6F92BA62A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F92BA42A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift */; }; - B6F92BA72A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F92BA42A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift */; }; - B6F92BA82A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F92BA42A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift */; }; - B6F92BA92A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F92BA42A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift */; }; - B6F92BAA2A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F92BA42A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift */; }; - B6F92BAB2A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F92BA42A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift */; }; B6F92BAC2A6937B3002ABA6B /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */; }; B6FA893D269C423100588ECD /* PrivacyDashboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */; }; @@ -3325,6 +3326,9 @@ 4B3B848F297A0E1000A384BD /* EmailManagerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailManagerExtension.swift; sourceTree = ""; }; 4B3F641D27A8D3BD00E0C118 /* BrowserProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserProfileTests.swift; sourceTree = ""; }; 4B4032832AAAC24400CCA602 /* WaitlistActivationDateStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistActivationDateStore.swift; sourceTree = ""; }; + 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionNotificationsPresenterFactory.swift; sourceTree = ""; }; + 4B41EDA22B1543B9001EEDF4 /* VPNPreferencesModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNPreferencesModel.swift; sourceTree = ""; }; + 4B41EDA62B1543C9001EEDF4 /* PreferencesVPNView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesVPNView.swift; sourceTree = ""; }; 4B43468F285ED7A100177407 /* BookmarksBarViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarViewModelTests.swift; sourceTree = ""; }; 4B43469428655D1400177407 /* FirefoxDataImporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirefoxDataImporterTests.swift; sourceTree = ""; }; 4B4BEC182A11B3EA001D9AC5 /* DuckDuckGoNotifications.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoNotifications.xcconfig; sourceTree = ""; }; @@ -4165,7 +4169,6 @@ B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = ""; }; B6F7127D29F6779000594A45 /* QRSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingService.swift; sourceTree = ""; }; B6F7128029F681EB00594A45 /* QuickLookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookUI.framework; path = System/Library/Frameworks/QuickLookUI.framework; sourceTree = SDKROOT; }; - B6F92BA42A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionUserDefaultsConstants.swift; sourceTree = ""; }; B6FA893C269C423100588ECD /* PrivacyDashboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PrivacyDashboard.storyboard; sourceTree = ""; }; B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = ""; }; B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = ""; }; @@ -4265,6 +4268,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4B41EDAB2B1544B2001EEDF4 /* LoginItems in Frameworks */, 7BEEA5122AD1235B00A9E72B /* NetworkProtectionIPC in Frameworks */, 7BA7CC5F2AD1210C0042E5CE /* Networking in Frameworks */, 7BEEA5162AD1236E00A9E72B /* NetworkProtectionUI in Frameworks */, @@ -4842,6 +4846,7 @@ 3776582E27F82E62009A6B35 /* AutofillPreferences.swift */, 37CD54C327F2FDD100F1F7B9 /* DownloadsPreferences.swift */, 37CD54C527F2FDD100F1F7B9 /* AboutModel.swift */, + 4B41EDA22B1543B9001EEDF4 /* VPNPreferencesModel.swift */, ); path = Model; sourceTree = ""; @@ -4936,6 +4941,7 @@ 379DE4BC27EA31AC002CC3DE /* PreferencesAutofillView.swift */, 37CC53EF27E8D1440028713D /* PreferencesDownloadsView.swift */, 37AFCE9127DB8CAD00471A10 /* PreferencesAboutView.swift */, + 4B41EDA62B1543C9001EEDF4 /* PreferencesVPNView.swift */, ); path = View; sourceTree = ""; @@ -5173,6 +5179,7 @@ 4B4D607D2A0B29FA00BCD287 /* NetworkExtensionTargets */ = { isa = PBXGroup; children = ( + 4B41ED9F2B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift */, EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, ); path = NetworkExtensionTargets; @@ -7834,7 +7841,6 @@ children = ( EEAD7A6E2A1D3E1F002A24E7 /* AppLauncher.swift */, 4BF0E5042AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift */, - B6F92BA42A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift */, 7B934C402A866DD400FC8F9C /* UserDefaults+NetworkProtectionShared.swift */, 7BFE95532A9DF2930081ABE9 /* UserDefaults+NetworkProtectionWaitlist.swift */, ); @@ -8003,6 +8009,7 @@ 7BEEA5152AD1236E00A9E72B /* NetworkProtectionUI */, 7BEC182E2AD5D8DC00D30536 /* SystemExtensionManager */, 7BFCB74D2ADE7E1A00DA3EA7 /* PixelKit */, + 4B41EDAA2B1544B2001EEDF4 /* LoginItems */, ); productName = DuckDuckGoAgent; productReference = 4B2D06392A11CFBB00DE1F49 /* DuckDuckGo VPN.app */; @@ -9079,6 +9086,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4B41EDA82B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, 3706FA7B293F65D500E42796 /* FaviconUserScript.swift in Sources */, 3706FA7E293F65D500E42796 /* LottieAnimationCache.swift in Sources */, 3706FA7F293F65D500E42796 /* TabIndex.swift in Sources */, @@ -9350,6 +9358,7 @@ B603971B29BA084C00902A34 /* JSAlertController.swift in Sources */, 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */, 3706FB6B293F65D500E42796 /* HistoryEntry.swift in Sources */, + 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */, 3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */, 377D801F2AB48191002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */, @@ -9550,7 +9559,6 @@ B60C6F7829B0E286007BFAA8 /* SearchNonexistentDomainNavigationResponder.swift in Sources */, 3707C720294B5D2900682A9F /* WKWebsiteDataStoreExtension.swift in Sources */, 3706FC03293F65D500E42796 /* TabPreviewViewController.swift in Sources */, - B6F92BA62A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */, 4B9754EC2984300100D7B834 /* EmailManagerExtension.swift in Sources */, 3706FC04293F65D500E42796 /* PreferencesPrivacyView.swift in Sources */, 3706FC05293F65D500E42796 /* NSPasteboardExtension.swift in Sources */, @@ -10040,7 +10048,6 @@ B65DA5F42A77D3FA00CBEE8D /* BundleExtension.swift in Sources */, 4B2D062D2A11C12300DE1F49 /* Logging.swift in Sources */, 7B2E52252A5FEC09000C6D39 /* NetworkProtectionAgentNotificationsPresenter.swift in Sources */, - B6F92BA82A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */, B602E8232A1E260E006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, EEAD7A7B2A1D3E20002A24E7 /* AppLauncher.swift in Sources */, 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */, @@ -10048,6 +10055,7 @@ 4B2D06322A11C1D300DE1F49 /* NSApplicationExtension.swift in Sources */, 4B2D06332A11C1E300DE1F49 /* OptionalExtension.swift in Sources */, 4BF0E50B2AD2552200FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, + 4B41EDA12B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, 4B2537782A11C00F00610219 /* NetworkProtectionExtensionMachService.swift in Sources */, B65DA5F32A77D3C700CBEE8D /* UserDefaultsWrapper.swift in Sources */, 4B2537722A11BF8B00610219 /* main.swift in Sources */, @@ -10077,7 +10085,6 @@ 7BA7CC4A2AD11EA00042E5CE /* NetworkProtectionTunnelController.swift in Sources */, 7BD1688E2AD4A4C400D24876 /* NetworkExtensionController.swift in Sources */, 7BA7CC3E2AD11E380042E5CE /* TunnelControllerIPCService.swift in Sources */, - B6F92BAA2A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */, 7BA7CC402AD11E3D0042E5CE /* AppLauncher+DefaultInitializer.swift in Sources */, EEC589DB2A4F1CE700BCD60C /* AppLauncher.swift in Sources */, B65DA5EF2A77CC3A00CBEE8D /* Bundle+NetworkProtectionExtensions.swift in Sources */, @@ -10097,7 +10104,6 @@ B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */, 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, B6F92BAD2A6937B5002ABA6B /* OptionalExtension.swift in Sources */, - B6F92BAB2A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */, 7BA7CC5A2AD120640042E5CE /* NetworkProtection+ConvenienceInitializers.swift in Sources */, 7BA7CC3B2AD11E330042E5CE /* Bundle+Configuration.swift in Sources */, EEC589DC2A4F1CE800BCD60C /* AppLauncher.swift in Sources */, @@ -10132,7 +10138,6 @@ B602E8222A1E2603006D261F /* Bundle+NetworkProtectionExtensions.swift in Sources */, B602E81A2A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, 4B4BEC402A11B5B5001D9AC5 /* NetworkProtectionExtensionMachService.swift in Sources */, - B6F92BA92A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */, EEAD7A7C2A1D3E20002A24E7 /* AppLauncher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -10141,6 +10146,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4B41EDA02B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, 4B4D609F2A0B2C7300BCD287 /* Logging.swift in Sources */, 4B4D60A12A0B2D6100BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, B602E8182A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, @@ -10152,7 +10158,6 @@ EEF12E6E2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */, 4BF0E50C2AD2552300FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, 4B4D60AC2A0C804B00BCD287 /* OptionalExtension.swift in Sources */, - B6F92BA72A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */, B65DA5F22A77D3C600CBEE8D /* UserDefaultsWrapper.swift in Sources */, EEAD7A7A2A1D3E20002A24E7 /* AppLauncher.swift in Sources */, ); @@ -10261,7 +10266,6 @@ 4B9579A32AC7AE700062CA31 /* PopUpButton.swift in Sources */, 4B9579A42AC7AE700062CA31 /* NetworkProtectionInviteDialog.swift in Sources */, 4B9579A52AC7AE700062CA31 /* SuggestionViewController.swift in Sources */, - 4B9579A62AC7AE700062CA31 /* NetworkProtectionUserDefaultsConstants.swift in Sources */, 4B9579A72AC7AE700062CA31 /* AddEditFavoriteWindow.swift in Sources */, 4B9579A82AC7AE700062CA31 /* BWKeyStorage.swift in Sources */, 4B9579A92AC7AE700062CA31 /* VisitViewModel.swift in Sources */, @@ -10494,6 +10498,7 @@ 4B957A7F2AC7AE700062CA31 /* NSApplicationExtension.swift in Sources */, 4B957A802AC7AE700062CA31 /* NSWindowExtension.swift in Sources */, 4B957A812AC7AE700062CA31 /* KeychainType+ClientDefault.swift in Sources */, + 4B41EDA52B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, 4B957A822AC7AE700062CA31 /* SyncDebugMenu.swift in Sources */, 4B957A832AC7AE700062CA31 /* BookmarkPopover.swift in Sources */, 4B957A842AC7AE700062CA31 /* PreferencesDownloadsView.swift in Sources */, @@ -10601,6 +10606,7 @@ 4B957AE82AC7AE700062CA31 /* Permissions.xcdatamodeld in Sources */, 4B957AE92AC7AE700062CA31 /* JSAlertController.swift in Sources */, 4B957AEA2AC7AE700062CA31 /* NotificationService.swift in Sources */, + 4B41EDA92B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, 4B957AEB2AC7AE700062CA31 /* SyncPreferences.swift in Sources */, 4B957AEC2AC7AE700062CA31 /* FaviconNullStore.swift in Sources */, 4B957AED2AC7AE700062CA31 /* PaddedImageButton.swift in Sources */, @@ -10990,7 +10996,6 @@ 4BBE0AA727B9B027003B37A8 /* PopUpButton.swift in Sources */, 4B4D60CF2A0C849600BCD287 /* NetworkProtectionInviteDialog.swift in Sources */, AABEE6A524AA0A7F0043105B /* SuggestionViewController.swift in Sources */, - B6F92BA52A691A44002ABA6B /* NetworkProtectionUserDefaultsConstants.swift in Sources */, 85589E8027BBB8630038AD11 /* AddEditFavoriteWindow.swift in Sources */, 1D6216B229069BBF00386B2C /* BWKeyStorage.swift in Sources */, AA7E919F287872EA00AB6B62 /* VisitViewModel.swift in Sources */, @@ -11302,6 +11307,7 @@ B693954B26F04BEB0015B914 /* MouseOverView.swift in Sources */, AAE7527C263B056C00B973F8 /* HistoryStore.swift in Sources */, AAE246F32709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift in Sources */, + 4B41EDA32B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, AA61C0D22727F59B00E6B681 /* ArrayExtension.swift in Sources */, 4B4D60CC2A0C849600BCD287 /* NetworkProtectionInviteCodeViewModel.swift in Sources */, AAC30A2C268F1ECD00D2D9CD /* CrashReportSender.swift in Sources */, @@ -11421,6 +11427,7 @@ 4B379C1527BD91E3008A968E /* QuartzIdleStateProvider.swift in Sources */, 37F19A6728E1B43200740DC6 /* DuckPlayerPreferences.swift in Sources */, B6C0B22E26E61CE70031CB7F /* DownloadViewModel.swift in Sources */, + 4B41EDA72B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */, 373A1AA8283ED1B900586521 /* BookmarkHTMLReader.swift in Sources */, B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */, 85378DA0274E6F42007C5CBF /* NSNotificationName+EmailManager.swift in Sources */, @@ -12726,7 +12733,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 85.1.1; + version = 86.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { @@ -12911,6 +12918,10 @@ isa = XCSwiftPackageProductDependency; productName = NetworkProtectionUI; }; + 4B41EDAA2B1544B2001EEDF4 /* LoginItems */ = { + isa = XCSwiftPackageProductDependency; + productName = LoginItems; + }; 4B4BEC492A11B627001D9AC5 /* NetworkProtection */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtection; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index df57efa2d0..b413c9f26b 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "543e1d7ed9b5743d4e6b0ebe18a5fbf8f1441f02", - "version" : "85.1.1" + "revision" : "1331652ad0dc21c23b495b4a9a42e2a0eb44859d", + "version" : "86.0.0" } }, { diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index 3e2ea6eaa9..f777ec90cd 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -121,6 +121,8 @@ final class URLEventHandler { Task { await WindowControllersManager.shared.showNetworkProtectionStatus() } + case AppLaunchCommand.showSettings.launchURL: + WindowControllersManager.shared.showPreferencesTab(withSelectedPane: .vpn) default: return } diff --git a/DuckDuckGo/Assets.xcassets/Images/InfoSubtle-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/InfoSubtle-16.imageset/Contents.json new file mode 100644 index 0000000000..d006c9eda6 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/InfoSubtle-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Info-Subtle-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/InfoSubtle-16.imageset/Info-Subtle-16.pdf b/DuckDuckGo/Assets.xcassets/Images/InfoSubtle-16.imageset/Info-Subtle-16.pdf new file mode 100644 index 0000000000..734d44d694 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/InfoSubtle-16.imageset/Info-Subtle-16.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/VPN.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/VPN.imageset/Contents.json new file mode 100644 index 0000000000..2986a1a8ee --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/VPN.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "VPN-Multicolor-16 1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/VPN.imageset/VPN-Multicolor-16 1.pdf b/DuckDuckGo/Assets.xcassets/Images/VPN.imageset/VPN-Multicolor-16 1.pdf new file mode 100644 index 0000000000..12bce6ad40 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/VPN.imageset/VPN-Multicolor-16 1.pdf differ diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 2b1825c342..0c2cb38812 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -38,6 +38,7 @@ extension UserText { // MARK: - Navigation Bar Status View static let networkProtectionNavBarStatusViewShareFeedback = NSLocalizedString("network.protection.navbar.status.view.share.feedback", value: "Share Feedback...", comment: "Menu item for 'Share Feedback' in the Network Protection status view that's shown in the navigation bar") + static let networkProtectionNavBarStatusMenuVPNSettings = NSLocalizedString("network.protection.status.menu.vpn.settings", value: "VPN Settings...", comment: "The status menu 'VPN Settings' menu item") // MARK: - System Extension Installation Messages diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 34f64bf530..616a6b83e6 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -37,6 +37,7 @@ struct UserText { static let deleteBookmark = NSLocalizedString("delete-bookmark", value: "Delete Bookmark", comment: "Delete Bookmark button") static let removeFavorite = NSLocalizedString("remove-favorite", value: "Remove Favorite", comment: "Remove Favorite button") static let quit = NSLocalizedString("quit", value: "Quit", comment: "Quit button") + static let uninstall = NSLocalizedString("uninstall", value: "Uninstall", comment: "Uninstall button") static let dontQuit = NSLocalizedString("dont.quit", value: "Don’t Quit", comment: "Don’t Quit button") static let next = NSLocalizedString("next", value: "Next", comment: "Next button") static let pasteAndGo = NSLocalizedString("paste.and.go", value: "Paste & Go", comment: "Paste & Go button") @@ -299,6 +300,53 @@ struct UserText { static let autoconsentCheckboxTitle = NSLocalizedString("autoconsent.checkbox.title", value: "Automatically handle cookie pop-ups", comment: "Autoconsent settings checkbox title") static let autoconsentExplanation = NSLocalizedString("autoconsent.explanation", value: "DuckDuckGo will try to select the most private settings available and hide these pop-ups for you.", comment: "Autoconsent feature explanation in settings") + // VPN Setting Titles + + static let vpnGeneralTitle = NSLocalizedString("vpn.general.title", value: "General", comment: "General section title in VPN settings") + static let vpnNotificationsSettingsTitle = NSLocalizedString("vpn.notifications.settings.title", value: "Notifications", comment: "Notifications section title in VPN settings") + static let vpnAdvancedSettingsTitle = NSLocalizedString("vpn.advanced.settings.title", value: "Advanced", comment: "VPN Advanced section title in VPN settings") + + // VPN Settings + + static let vpnConnectOnLoginSettingTitle = NSLocalizedString( + "vpn.setting.title.connect.on.login", + value: "Connect on login", + comment: "Connect on Login setting title") + static let vpnShowInMenuBarSettingTitle = NSLocalizedString( + "vpn.setting.title.connect.on.login", + value: "Show VPN in menu bar", + comment: "Display VPN status in the menu bar.") + static let vpnAlwaysOnSettingDescription = NSLocalizedString( + "vpn.setting.description.always.on", + value: "Automatically restores the VPN connection after interruption. For your security, this setting cannot be disabled.", + comment: "Always ON setting description") + static let vpnExcludeLocalNetworksSettingTitle = NSLocalizedString( + "vpn.setting.title.exclude.local.networks", + value: "Exclude local networks", + comment: "Exclude Local Networks setting title") + static let vpnExcludeLocalNetworksSettingDescription = NSLocalizedString( + "vpn.setting.description.exclude.local.networks", + value: "Bypass the VPN for local network connections, like to a printer.", + comment: "Exclude Local Networks setting description") + static let vpnSecureDNSSettingDescription = NSLocalizedString( + "vpn.setting.description.secure.dns", + value: "Our VPN uses Secure DNS to keep your online activity private, so that your Internet provider can't see what websites you visit.", + comment: "Secure DNS setting description") + static let uninstallVPNButtonTitle = NSLocalizedString( + "vpn.button.title.uninstall.vpn", + value: "Uninstall DuckDuckGo VPN...", + comment: "Uninstall VPN button title") + + // VPN Settings Alerts + + static let uninstallVPNAlertTitle = NSLocalizedString("vpn.uninstall.alert.title", value: "Are you sure you want to uninstall the VPN?", comment: "Alert title when the user selects to uninstall our VPN") + static let uninstallVPNInformativeText = NSLocalizedString( + "vpn.uninstall.alert.informative.text", + value: "Uninstalling the DuckDuckGo VPN will disconnect the VPN and remove it from your device.", + comment: "Informative text for the alert that comes up when the user decides to uninstall our VPN") + + // Misc + static let duckPlayerSettingsTitle = NSLocalizedString("duck-player.title", value: "Duck Player", comment: "Private YouTube Player settings title") static let duckPlayerAlwaysOpenInPlayer = NSLocalizedString("duck-player.always-open-in-player", value: "Always open YouTube videos in Duck Player", comment: "Private YouTube Player option") static let duckPlayerShowPlayerButtons = NSLocalizedString("duck-player.show-buttons", value: "Show option to use Duck Player over YouTube previews on hover", comment: "Private YouTube Player option") @@ -434,6 +482,7 @@ struct UserText { static let defaultBrowser = NSLocalizedString("preferences.default-browser", value: "Default Browser", comment: "Show default browser preferences") static let appearance = NSLocalizedString("preferences.appearance", value: "Appearance", comment: "Show appearance preferences") static let privacy = NSLocalizedString("preferences.privacy", value: "Privacy", comment: "Show privacy browser preferences") + static let vpn = NSLocalizedString("preferences.vpn", value: "VPN", comment: "Show VPN preferences") static let duckPlayer = NSLocalizedString("preferences.duck-player", value: "Duck Player", comment: "Show Duck Player browser preferences") static let about = NSLocalizedString("preferences.about", value: "About", comment: "Show about screen") diff --git a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift index 3f16d2c144..50413b3acf 100644 --- a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift +++ b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift @@ -22,7 +22,7 @@ import LoginItems extension LoginItem { - static let dbpBackgroundAgent = LoginItem(bundleId: Bundle.main.dbpBackgroundAgentBundleId, log: .dbp) + static let dbpBackgroundAgent = LoginItem(bundleId: Bundle.main.dbpBackgroundAgentBundleId, defaults: .dbp, log: .dbp) } diff --git a/DuckDuckGo/MessageViews/PopoverMessageViewController.swift b/DuckDuckGo/MessageViews/PopoverMessageViewController.swift index 2efe639f1d..0609d8425a 100644 --- a/DuckDuckGo/MessageViews/PopoverMessageViewController.swift +++ b/DuckDuckGo/MessageViews/PopoverMessageViewController.swift @@ -63,6 +63,17 @@ final class PopoverMessageViewController: NSHostingController { + // It's important to subscribe to the publisher for the raw value, since this + // is the way to get KVO when the UserDefaults are modified by another process. publisher(for: \.networkProtectionOnboardingStatusRawValue).map { value in OnboardingStatus(rawValue: value) ?? .default }.eraseToAnyPublisher() diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift index 5b150f6ddb..78883281fa 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift @@ -48,8 +48,7 @@ extension EventMapping where Event == NetworkProtectionError { domainEvent = .networkProtectionKeychainDeleteError(status: status) case .noAuthTokenFound: domainEvent = .networkProtectionNoAuthTokenFoundError - case - .noServerRegistrationInfo, + case .noServerRegistrationInfo, .couldNotSelectClosestServer, .couldNotGetPeerPublicKey, .couldNotGetPeerHostName, diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift index ee34e73d09..38caf2dc7c 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift @@ -21,9 +21,9 @@ import LoginItems extension LoginItem { - static let vpnMenu = LoginItem(bundleId: Bundle.main.vpnMenuAgentBundleId, log: .networkProtection) + static let vpnMenu = LoginItem(bundleId: Bundle.main.vpnMenuAgentBundleId, defaults: .netP, log: .networkProtection) #if NETP_SYSTEM_EXTENSION - static let notificationsAgent = LoginItem(bundleId: Bundle.main.notificationsAgentBundleId, log: .networkProtection) + static let notificationsAgent = LoginItem(bundleId: Bundle.main.notificationsAgentBundleId, defaults: .netP, log: .networkProtection) #endif } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index 944cb41131..0838bb0c2d 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -25,7 +25,7 @@ import Common extension NetworkProtectionDeviceManager { static func create() -> NetworkProtectionDeviceManager { - let settings = TunnelSettings(defaults: .netP) + let settings = VPNSettings(defaults: .netP) let keyStore = NetworkProtectionKeychainKeyStore() let tokenStore = NetworkProtectionKeychainTokenStore() return NetworkProtectionDeviceManager(environment: settings.selectedEnvironment, tokenStore: tokenStore, keyStore: keyStore, errorEvents: .networkProtectionAppDebugEvents) @@ -34,7 +34,7 @@ extension NetworkProtectionDeviceManager { extension NetworkProtectionCodeRedemptionCoordinator { convenience init() { - let settings = TunnelSettings(defaults: .netP) + let settings = VPNSettings(defaults: .netP) self.init(environment: settings.selectedEnvironment, tokenStore: NetworkProtectionKeychainTokenStore(), errorEvents: .networkProtectionAppDebugEvents) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index 23e3ffd757..fc494b066e 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -169,7 +169,7 @@ final class NetworkProtectionAppEvents { // MARK: - Legacy Login Item and Extension private func removeLegacyLoginItemAndVPNConfiguration() async { - LoginItem(bundleId: legacyAgentBundleID).forceStop() + LoginItem(bundleId: legacyAgentBundleID, defaults: .netP).forceStop() let tunnels = try? await NETunnelProviderManager.loadAllFromPreferences() let tunnel = tunnels?.first { diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 3762781729..cc6b0871ed 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -37,6 +37,8 @@ final class NetworkProtectionDebugMenu: NSMenu { private let registrationKeyValidityMenu: NSMenu private let registrationKeyValidityAutomaticItem = NSMenuItem(title: "Automatic", action: #selector(NetworkProtectionDebugMenu.setRegistrationKeyValidity)) + private let resetToDefaults = NSMenuItem(title: "Reset Settings to defaults", action: #selector(NetworkProtectionDebugMenu.resetSettings)) + private let exclusionsMenu = NSMenu() private let shouldEnforceRoutesMenuItem = NSMenuItem(title: "Kill Switch (enforceRoutes)", action: #selector(NetworkProtectionDebugMenu.toggleEnforceRoutesAction)) @@ -63,17 +65,23 @@ final class NetworkProtectionDebugMenu: NSMenu { super.init(title: "Network Protection") buildItems { - NSMenuItem(title: "Reset All State Keeping Invite", action: #selector(NetworkProtectionDebugMenu.resetAllKeepingInvite)) - .targetting(self) + NSMenuItem(title: "Reset") { + NSMenuItem(title: "Reset All State Keeping Invite", action: #selector(NetworkProtectionDebugMenu.resetAllKeepingInvite)) + .targetting(self) - NSMenuItem(title: "Reset All State", action: #selector(NetworkProtectionDebugMenu.resetAllState)) - .targetting(self) + NSMenuItem(title: "Reset All State", action: #selector(NetworkProtectionDebugMenu.resetAllState)) + .targetting(self) - NSMenuItem(title: "Remove System Extension and Login Items", action: #selector(NetworkProtectionDebugMenu.removeSystemExtensionAndAgents)) - .targetting(self) + resetToDefaults + .targetting(self) + + NSMenuItem(title: "Remove System Extension and Login Items", action: #selector(NetworkProtectionDebugMenu.removeSystemExtensionAndAgents)) + .targetting(self) + + NSMenuItem(title: "Reset Remote Messages", action: #selector(NetworkProtectionDebugMenu.resetNetworkProtectionRemoteMessages)) + .targetting(self) + } - NSMenuItem(title: "Reset Remote Messages", action: #selector(NetworkProtectionDebugMenu.resetNetworkProtectionRemoteMessages)) - .targetting(self) NSMenuItem.separator() connectOnLogInMenuItem @@ -139,6 +147,7 @@ final class NetworkProtectionDebugMenu: NSMenu { NSMenuItem(title: "NetP Waitlist Feature Flag Overrides") .submenu(NetworkProtectionWaitlistFeatureFlagOverridesMenu()) + NSMenuItem.separator() NSMenuItem(title: "Kill Switch (alternative approach)") { @@ -168,7 +177,7 @@ final class NetworkProtectionDebugMenu: NSMenu { // MARK: - Tunnel Settings - private let settings = TunnelSettings(defaults: .netP) + private let settings = VPNSettings(defaults: .netP) // MARK: - Debug Logic @@ -181,12 +190,7 @@ final class NetworkProtectionDebugMenu: NSMenu { @objc func resetAllState(_ sender: Any?) { Task { @MainActor in guard case .alertFirstButtonReturn = await NSAlert.resetNetworkProtectionAlert().runModal() else { return } - - do { - try await debugUtilities.resetAllState(keepAuthToken: false) - } catch { - await NSAlert(error: error).runModal() - } + await debugUtilities.resetAllState(keepAuthToken: false) } } @@ -195,15 +199,14 @@ final class NetworkProtectionDebugMenu: NSMenu { @objc func resetAllKeepingInvite(_ sender: Any?) { Task { @MainActor in guard case .alertFirstButtonReturn = await NSAlert.resetNetworkProtectionAlert().runModal() else { return } - - do { - try await debugUtilities.resetAllState(keepAuthToken: true) - } catch { - await NSAlert(error: error).runModal() - } + await debugUtilities.resetAllState(keepAuthToken: true) } } + @objc func resetSettings(_ sender: Any?) { + settings.resetToDefaults() + } + /// Removes the system extension and agents for Network Protection. /// @objc func removeSystemExtensionAndAgents(_ sender: Any?) { @@ -234,7 +237,7 @@ final class NetworkProtectionDebugMenu: NSMenu { /// @objc func setSelectedServer(_ menuItem: NSMenuItem) { let title = menuItem.title - let selectedServer: TunnelSettings.SelectedServer + let selectedServer: VPNSettings.SelectedServer if title == "Automatic" { selectedServer = .automatic @@ -250,7 +253,7 @@ final class NetworkProtectionDebugMenu: NSMenu { /// @objc func expireRegistrationKeyNow(_ sender: Any?) { Task { - await debugUtilities.expireRegistrationKeyNow() + try? await debugUtilities.expireRegistrationKeyNow() } } @@ -369,14 +372,14 @@ final class NetworkProtectionDebugMenu: NSMenu { private func populateExclusionsMenuItems() { exclusionsMenu.removeAllItems() - for item in settings.exclusionList { + for item in settings.excludedRoutes { let menuItem: NSMenuItem switch item { case .section(let title): menuItem = NSMenuItem(title: title, action: nil, target: nil) menuItem.isEnabled = false - case .exclusion(range: let range, description: let description, default: _): + case .range(let range, let description): menuItem = NSMenuItem(title: "\(range)\(description != nil ? " (\(description!))" : "")", action: #selector(toggleExclusionAction), target: self, @@ -564,7 +567,7 @@ final class NetworkProtectionDebugMenu: NSMenu { // MARK: Environment @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { let title = menuItem.title - let selectedEnvironment: TunnelSettings.SelectedEnvironment + let selectedEnvironment: VPNSettings.SelectedEnvironment if title == "Staging" { selectedEnvironment = .staging diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift index 5dca24962f..c723da5658 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugUtilities.swift @@ -38,10 +38,15 @@ final class NetworkProtectionDebugUtilities { private let loginItemsManager: LoginItemsManager + // MARK: - Settings + + private let settings: VPNSettings + // MARK: - Initializers - init(loginItemsManager: LoginItemsManager = .init()) { + init(loginItemsManager: LoginItemsManager = .init(), settings: VPNSettings = .init(defaults: .netP)) { self.loginItemsManager = loginItemsManager + self.settings = settings let ipcClient = TunnelControllerIPCClient(machServiceName: Bundle.main.vpnMenuAgentBundleId) @@ -51,8 +56,14 @@ final class NetworkProtectionDebugUtilities { // MARK: - Debug commands for the extension - func resetAllState(keepAuthToken: Bool) async throws { - networkProtectionFeatureDisabler.disable(keepAuthToken: keepAuthToken, uninstallSystemExtension: true) + func resetAllState(keepAuthToken: Bool) async { + let uninstalledSuccessfully = await networkProtectionFeatureDisabler.disable(keepAuthToken: keepAuthToken, uninstallSystemExtension: true) + + guard uninstalledSuccessfully else { + return + } + + settings.resetToDefaults() NetworkProtectionWaitlist().waitlistStorage.deleteWaitlistState() DefaultWaitlistActivationDateStore().removeDates() @@ -63,16 +74,16 @@ final class NetworkProtectionDebugUtilities { } func removeSystemExtensionAndAgents() async throws { - await networkProtectionFeatureDisabler.removeSystemExtension() + try await networkProtectionFeatureDisabler.removeSystemExtension() networkProtectionFeatureDisabler.disableLoginItems() } func sendTestNotificationRequest() async throws { - await ipcClient.debugCommand(.sendTestNotification) + try await ipcClient.debugCommand(.sendTestNotification) } - func expireRegistrationKeyNow() async { - await ipcClient.debugCommand(.expireRegistrationKey) + func expireRegistrationKeyNow() async throws { + try await ipcClient.debugCommand(.expireRegistrationKey) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift index a24bc8ee15..d9473f1efe 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarButtonModel.swift @@ -78,7 +78,6 @@ final class NetworkProtectionNavBarButtonModel: NSObject, ObservableObject { statusReporter: NetworkProtectionStatusReporter? = nil, iconProvider: IconProvider = NavigationBarIconProvider()) { - let vpnBundleID = Bundle.main.vpnMenuAgentBundleId self.popoverManager = popoverManager let ipcClient = popoverManager.ipcClient diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 0e961514ef..64b5642fe3 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -58,18 +58,26 @@ final class NetworkProtectionNavBarPopoverManager { controllerErrorMessageObserver: ControllerErrorMesssageObserverThroughDistributedNotifications() ) - let menuItems = [ - NetworkProtectionStatusView.Model.MenuItem( - name: UserText.networkProtectionNavBarStatusViewShareFeedback, - action: { - let appLauncher = AppLauncher(appBundleURL: Bundle.main.bundleURL) - await appLauncher.launchApp(withCommand: .shareFeedback) - }) - ] - let onboardingStatusPublisher = UserDefaults.netP.networkProtectionOnboardingStatusPublisher - - let popover = NetworkProtectionPopover(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, menuItems: menuItems) + _ = VPNSettings(defaults: .netP) + + let popover = NetworkProtectionPopover(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter) { + let menuItems = [ + NetworkProtectionStatusView.Model.MenuItem( + name: UserText.networkProtectionNavBarStatusMenuVPNSettings, action: { + let appLauncher = AppLauncher(appBundleURL: Bundle.main.bundleURL) + await appLauncher.launchApp(withCommand: .showSettings) + }), + NetworkProtectionStatusView.Model.MenuItem( + name: UserText.networkProtectionNavBarStatusViewShareFeedback, + action: { + let appLauncher = AppLauncher(appBundleURL: Bundle.main.bundleURL) + await appLauncher.launchApp(withCommand: .shareFeedback) + }) + ] + + return menuItems + } popover.delegate = delegate networkProtectionPopover = popover diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionOnboardingMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionOnboardingMenu.swift index 6c5eace433..d3668a838e 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionOnboardingMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionOnboardingMenu.swift @@ -33,7 +33,7 @@ final class NetworkProtectionOnboardingMenu: NSMenu { action: #selector(NetworkProtectionOnboardingMenu.reset)) private let setStatusCompletedMenuItem = NSMenuItem(title: "Onboarding Completed", action: #selector(NetworkProtectionOnboardingMenu.setStatusCompleted)) - private let setStatusAllowSystemExtensionMenuItem = NSMenuItem(title: "Allow System Extension", + private let setStatusAllowSystemExtensionMenuItem = NSMenuItem(title: "Install VPN System Extension", action: #selector(NetworkProtectionOnboardingMenu.setStatusAllowSystemExtension)) private let setStatusAllowVPNConfigurationMenuItem = NSMenuItem(title: "Allow VPN Configuration", action: #selector(NetworkProtectionOnboardingMenu.setStatusAllowVPNConfiguration)) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index b7166ec949..374f1c99a9 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -35,7 +35,7 @@ typealias NetworkProtectionConfigChangeHandler = () -> Void final class NetworkProtectionTunnelController: NetworkProtection.TunnelController { - let settings: TunnelSettings + let settings: VPNSettings // MARK: - Combine Cancellables @@ -69,10 +69,12 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle // MARK: - User Defaults + /* Temporarily disabled - https://app.asana.com/0/0/1205766100762904/f /// Test setting to exclude duckduckgo route from VPN @MainActor @UserDefaultsWrapper(key: .networkProtectionExcludedRoutes, defaultValue: [:]) private(set) var excludedRoutesPreferences: [String: Bool] + */ @UserDefaultsWrapper(key: .networkProtectionOnboardingStatusRawValue, defaultValue: OnboardingStatus.default.rawValue, defaults: .netP) private(set) var onboardingStatusRawValue: OnboardingStatus.RawValue @@ -117,7 +119,7 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle /// init(networkExtensionBundleID: String, networkExtensionController: NetworkExtensionController, - settings: TunnelSettings, + settings: VPNSettings, notificationCenter: NotificationCenter = .default, tokenStore: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), logger: NetworkProtectionLogger = DefaultNetworkProtectionLogger()) { @@ -150,11 +152,11 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle .store(in: &cancellables) } - /// This is where the tunnel has a chance to handle the settings change locally. + /// This is where the tunnel owner has a chance to handle the settings change locally. /// /// The extension can also handle these changes so not everything needs to be handled here. /// - private func handleSettingsChange(_ change: TunnelSettings.Change) async throws { + private func handleSettingsChange(_ change: VPNSettings.Change) async throws { switch change { case .setIncludeAllNetworks(let includeAllNetworks): try await handleSetIncludeAllNetworks(includeAllNetworks) @@ -162,11 +164,14 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle try await handleSetEnforceRoutes(enforceRoutes) case .setExcludeLocalNetworks(let excludeLocalNetworks): try await handleSetExcludeLocalNetworks(excludeLocalNetworks) - case .setRegistrationKeyValidity, + case .setConnectOnLogin, + .setNotifyStatusChanges, + .setRegistrationKeyValidity, .setSelectedServer, + .setSelectedEnvironment, .setSelectedLocation, - .setSelectedEnvironment: - // Intentional no-op as this is handled by the extension + .setShowInMenuBar: + // Intentional no-op as this is handled by the extension or the agent's app delegate break } } @@ -196,10 +201,9 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle } try await setupAndSave(tunnelManager) - updateRoutes() } - private func relaySettingsChange(_ change: TunnelSettings.Change) async throws { + private func relaySettingsChange(_ change: VPNSettings.Change) async throws { guard await isConnected, let activeSession = try await ConnectionSessionUtilities.activeSession(networkExtensionBundleID: networkExtensionBundleID) else { return } @@ -229,7 +233,6 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle protocolConfiguration.providerBundleIdentifier = NetworkProtectionBundle.extensionBundle().bundleIdentifier protocolConfiguration.providerConfiguration = [ NetworkProtectionOptionKey.defaultPixelHeaders: APIRequest.Headers().httpHeaders, - NetworkProtectionOptionKey.excludedRoutes: excludedRoutes().map(\.stringRepresentation) as NSArray, NetworkProtectionOptionKey.includedRoutes: includedRoutes().map(\.stringRepresentation) as NSArray ] @@ -237,10 +240,15 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle protocolConfiguration.disconnectOnSleep = false // kill switch - protocolConfiguration.enforceRoutes = settings.enforceRoutes + protocolConfiguration.enforceRoutes = true // settings.enforceRoutes // this setting breaks Connection Tester protocolConfiguration.includeAllNetworks = settings.includeAllNetworks - protocolConfiguration.excludeLocalNetworks = settings.excludeLocalNetworks + + // This is intentionally not used but left here for documentation purposes. + // The reason for this is that we want to have full control of the routes that + // are excluded, so instead of using this setting we're just configuring the + // excluded routes through our VPNSettings class, which our extension reads directly. + // protocolConfiguration.excludeLocalNetworks = settings.excludeLocalNetworks return protocolConfiguration }() @@ -387,7 +395,7 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle options[NetworkProtectionOptionKey.activationAttemptId] = UUID().uuidString as NSString options[NetworkProtectionOptionKey.authToken] = try tokenStore.fetchToken() as NSString? - options[NetworkProtectionOptionKey.selectedEnvironment] = settings.selectedEnvironment.rawValue as? NSString + options[NetworkProtectionOptionKey.selectedEnvironment] = settings.selectedEnvironment.rawValue as NSString options[NetworkProtectionOptionKey.selectedServer] = settings.selectedServer.stringValue as? NSString if case .custom(let keyValidity) = settings.registrationKeyValidity { @@ -461,9 +469,10 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle try await tunnelManager.saveToPreferences() } + /* Temporarily disabled until we fix this menu: https://app.asana.com/0/0/1205766100762904/f @MainActor private func excludedRoutes() -> [NetworkProtection.IPAddressRange] { - settings.exclusionList.compactMap { [excludedRoutesPreferences] item -> NetworkProtection.IPAddressRange? in + settings.excludedRoutes.compactMap { [excludedRoutesPreferences] item -> NetworkProtection.IPAddressRange? in guard case .exclusion(range: let range, description: _, default: let defaultValue) = item, excludedRoutesPreferences[range.stringRepresentation, default: defaultValue] == true else { return nil } @@ -476,7 +485,7 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle return range } - } + }*/ /// extra Included Routes appended to 0.0.0.0, ::/0 (peers) and interface.addresses @MainActor @@ -484,10 +493,10 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle [] } + /* Temporarily disabled - https://app.asana.com/0/0/1205766100762904/f @MainActor func setExcludedRoute(_ route: String, enabled: Bool) { excludedRoutesPreferences[route] = enabled - updateRoutes() } @MainActor @@ -507,16 +516,7 @@ final class NetworkProtectionTunnelController: NetworkProtection.TunnelControlle return false } return excludedRoutesPreferences[route, default: defaultValue] - } - - func updateRoutes() { - Task { - guard let activeSession = try await ConnectionSessionUtilities.activeSession() else { return } - - try await activeSession.sendProviderMessage(.setIncludedRoutes(includedRoutes())) - try await activeSession.sendProviderMessage(.setExcludedRoutes(excludedRoutes())) - } - } + }*/ struct TunnelFailureError: LocalizedError { let errorDescription: String? diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 9fadeafef0..9388585fde 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -37,23 +37,6 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { private var cancellables = Set() - // MARK: - User Notifications - - private static func makeNotificationsPresenter() -> NetworkProtectionNotificationsPresenter { -#if NETP_SYSTEM_EXTENSION - return NetworkProtectionAgentNotificationsPresenter(notificationCenter: DistributedNotificationCenter.default()) -#else - let parentBundlePath = "../../../" - let mainAppURL: URL - if #available(macOS 13, *) { - mainAppURL = URL(filePath: parentBundlePath, relativeTo: Bundle.main.bundleURL) - } else { - mainAppURL = URL(fileURLWithPath: parentBundlePath, relativeTo: Bundle.main.bundleURL) - } - return NetworkProtectionUNNotificationsPresenter(appLauncher: AppLauncher(appBundleURL: mainAppURL)) -#endif - } - private let appLauncher: AppLaunching? // MARK: - Error Reporting @@ -181,20 +164,27 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { @objc public init() { self.appLauncher = AppLauncher(appBundleURL: .mainAppBundleURL) +#if NETP_SYSTEM_EXTENSION + let settings = VPNSettings(defaults: .standard) +#else + let settings = VPNSettings(defaults: .shared) +#endif let tunnelHealthStore = NetworkProtectionTunnelHealthStore(notificationCenter: notificationCenter) let controllerErrorStore = NetworkProtectionTunnelErrorStore(notificationCenter: notificationCenter) let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore) let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: NetworkProtectionBundle.keychainType, serviceName: Self.tokenServiceName, errorEvents: debugEvents) + let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings) - super.init(notificationsPresenter: Self.makeNotificationsPresenter(), + super.init(notificationsPresenter: notificationsPresenter, tunnelHealthStore: tunnelHealthStore, controllerErrorStore: controllerErrorStore, keychainType: NetworkProtectionBundle.keychainType, tokenStore: tokenStore, debugEvents: debugEvents, - providerEvents: Self.packetTunnelProviderEvents) + providerEvents: Self.packetTunnelProviderEvents, + settings: settings) observeConnectionStatusChanges() observeServerChanges() diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionNotificationsPresenterFactory.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionNotificationsPresenterFactory.swift new file mode 100644 index 0000000000..23419c8918 --- /dev/null +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionNotificationsPresenterFactory.swift @@ -0,0 +1,47 @@ +// +// NetworkProtectionNotificationsPresenterFactory.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 NetworkProtection + +/// A convenience class for making notification presenters. +/// +struct NetworkProtectionNotificationsPresenterFactory { + func make(settings: VPNSettings) -> NetworkProtectionNotificationsPresenter { + let presenterForBuildType = makePresenterForBuildType() + + return NetworkProtectionNotificationsPresenterTogglableDecorator( + settings: settings, + wrappee: presenterForBuildType) + } + + private func makePresenterForBuildType() -> NetworkProtectionNotificationsPresenter { + #if NETP_SYSTEM_EXTENSION + return NetworkProtectionAgentNotificationsPresenter(notificationCenter: DistributedNotificationCenter.default()) + #else + let parentBundlePath = "../../../" + let mainAppURL: URL + if #available(macOS 13, *) { + mainAppURL = URL(filePath: parentBundlePath, relativeTo: Bundle.main.bundleURL) + } else { + mainAppURL = URL(fileURLWithPath: parentBundlePath, relativeTo: Bundle.main.bundleURL) + } + return NetworkProtectionUNNotificationsPresenter(appLauncher: AppLauncher(appBundleURL: mainAppURL)) + #endif + } +} diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index c0377154d8..5002b5666a 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -24,7 +24,7 @@ struct PreferencesSection: Hashable, Identifiable { let panes: [PreferencePaneIdentifier] @MainActor - static func defaultSections(includingDuckPlayer: Bool) -> [PreferencesSection] { + static func defaultSections(includingDuckPlayer: Bool, includingVPN: Bool) -> [PreferencesSection] { let regularPanes: [PreferencePaneIdentifier] = { #if SUBSCRIPTION var panes: [PreferencePaneIdentifier] = [.privacy, .subscription, .general, .appearance, .autofill, .downloads] @@ -45,6 +45,12 @@ struct PreferencesSection: Hashable, Identifiable { panes.append(.duckPlayer) } +#if NETWORK_PROTECTION + if includingVPN { + panes.append(.vpn) + } +#endif + return panes }() @@ -65,6 +71,9 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { case sync case appearance case privacy +#if NETWORK_PROTECTION + case vpn +#endif #if SUBSCRIPTION case subscription #endif @@ -98,6 +107,10 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { return UserText.appearance case .privacy: return UserText.privacy +#if NETWORK_PROTECTION + case .vpn: + return UserText.vpn +#endif #if SUBSCRIPTION case .subscription: return UserText.subscription @@ -123,6 +136,10 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { return "Appearance" case .privacy: return "Privacy" +#if NETWORK_PROTECTION + case .vpn: + return "VPN" +#endif #if SUBSCRIPTION case .subscription: return "Privacy" diff --git a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift index f206797b00..363378641a 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSidebarModel.swift @@ -28,6 +28,8 @@ final class PreferencesSidebarModel: ObservableObject { @Published var selectedTabIndex: Int = 0 @Published private(set) var selectedPane: PreferencePaneIdentifier = .general + // MARK: - Initializers + init( loadSections: @escaping () -> [PreferencesSection], tabSwitcherTabs: [Tab.TabContent], @@ -35,10 +37,11 @@ final class PreferencesSidebarModel: ObservableObject { ) { self.loadSections = loadSections self.tabSwitcherTabs = tabSwitcherTabs + resetTabSelectionIfNeeded() refreshSections() - privacyConfigCancellable = privacyConfigurationManager.updatesPublisher + privacyConfigurationManager.updatesPublisher .map { [weak privacyConfigurationManager] in privacyConfigurationManager?.privacyConfig.isEnabled(featureKey: .duckPlayer) == true } @@ -48,6 +51,11 @@ final class PreferencesSidebarModel: ObservableObject { .sink { [weak self] in self?.refreshSections() } + .store(in: &cancellables) + +#if NETWORK_PROTECTION + setupVPNPaneVisibility() +#endif } @MainActor @@ -56,11 +64,42 @@ final class PreferencesSidebarModel: ObservableObject { privacyConfigurationManager: PrivacyConfigurationManaging = ContentBlocking.shared.privacyConfigurationManager, includeDuckPlayer: Bool ) { - self.init(loadSections: { PreferencesSection.defaultSections(includingDuckPlayer: includeDuckPlayer) }, + let loadSections = { +#if NETWORK_PROTECTION + let includingVPN = DefaultNetworkProtectionVisibility().isOnboarded +#else + let includingVPN = false +#endif + + return PreferencesSection.defaultSections(includingDuckPlayer: includeDuckPlayer, includingVPN: includingVPN) + } + + self.init(loadSections: loadSections, tabSwitcherTabs: tabSwitcherTabs, privacyConfigurationManager: privacyConfigurationManager) } + // MARK: - Setup + +#if NETWORK_PROTECTION + private func setupVPNPaneVisibility() { + DefaultNetworkProtectionVisibility().onboardStatusPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] onboardingStatus in + guard let self else { return } + + if onboardingStatus != .completed && self.selectedPane == .vpn { + self.selectedPane = .general + } + + self.refreshSections() + } + .store(in: &cancellables) + } +#endif + + // MARK: - Refreshing logic + func refreshSections() { sections = loadSections() if !sections.flatMap(\.panes).contains(selectedPane), let firstPane = sections.first?.panes.first { @@ -83,5 +122,5 @@ final class PreferencesSidebarModel: ObservableObject { } private let loadSections: () -> [PreferencesSection] - private var privacyConfigCancellable: AnyCancellable? + private var cancellables = Set() } diff --git a/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift new file mode 100644 index 0000000000..453a505449 --- /dev/null +++ b/DuckDuckGo/Preferences/Model/VPNPreferencesModel.swift @@ -0,0 +1,118 @@ +// +// PrivacyPreferencesModel.swift +// +// Copyright © 2022 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 NETWORK_PROTECTION + +import AppKit +import Combine +import Foundation +import NetworkProtection +import NetworkProtectionUI + +final class VPNPreferencesModel: ObservableObject { + + @Published var alwaysON = true + + @Published var connectOnLogin: Bool { + didSet { + settings.connectOnLogin = connectOnLogin + } + } + + @Published var excludeLocalNetworks: Bool { + didSet { + settings.excludeLocalNetworks = excludeLocalNetworks + } + } + + @Published var secureDNS: Bool = true + + @Published var showInMenuBar: Bool { + didSet { + settings.showInMenuBar = showInMenuBar + } + } + + @Published var notifyStatusChanges: Bool { + didSet { + settings.notifyStatusChanges = notifyStatusChanges + } + } + + @Published var showUninstallVPN: Bool + + private var onboardingStatus: OnboardingStatus { + didSet { + showUninstallVPN = onboardingStatus != .default + } + } + + private let settings: VPNSettings + private var cancellables = Set() + + init(settings: VPNSettings = .init(defaults: .netP), + defaults: UserDefaults = .netP) { + self.settings = settings + + connectOnLogin = settings.connectOnLogin + excludeLocalNetworks = settings.excludeLocalNetworks + notifyStatusChanges = settings.notifyStatusChanges + showInMenuBar = settings.showInMenuBar + showUninstallVPN = defaults.networkProtectionOnboardingStatus != .default + onboardingStatus = defaults.networkProtectionOnboardingStatus + + subscribeToOnboardingStatusChanges(defaults: defaults) + } + + func subscribeToOnboardingStatusChanges(defaults: UserDefaults) { + defaults.networkProtectionOnboardingStatusPublisher + .assign(to: \.onboardingStatus, onWeaklyHeld: self) + .store(in: &cancellables) + } + + @MainActor + func uninstallVPN() async { + let response = await uninstallVPNConfirmationAlert().runModal() + + switch response { + case .OK: + await NetworkProtectionFeatureDisabler().disable(keepAuthToken: true, uninstallSystemExtension: true) + default: + // intentional no-op + break + } + } + + @MainActor + func uninstallVPNConfirmationAlert() -> NSAlert { + let alert = NSAlert() + alert.messageText = UserText.uninstallVPNAlertTitle + alert.informativeText = UserText.uninstallVPNInformativeText + let uninstallButton = alert.addButton(withTitle: UserText.uninstall) + uninstallButton.tag = NSApplication.ModalResponse.OK.rawValue + uninstallButton.keyEquivalent = "" + + let cancelButton = alert.addButton(withTitle: UserText.cancel) + cancelButton.tag = NSApplication.ModalResponse.cancel.rawValue + cancelButton.keyEquivalent = "\r" + + return alert + } +} + +#endif diff --git a/DuckDuckGo/Preferences/View/Preferences.swift b/DuckDuckGo/Preferences/View/Preferences.swift index 739c3eec11..ebb1438b71 100644 --- a/DuckDuckGo/Preferences/View/Preferences.swift +++ b/DuckDuckGo/Preferences/View/Preferences.swift @@ -30,6 +30,10 @@ enum Preferences { } }() + enum Spacing { + static let groupedCheckboxesSeparation: CGFloat = 10 + } + enum Fonts { static let popUpButton: NSFont = .preferredFont(forTextStyle: .title1, options: [:]) static let sideBarItem: Font = .body @@ -79,4 +83,31 @@ enum Preferences { } } + struct ToggleMenuItemWithDescription: View { + let title: String + let description: String + let isOn: Binding + let spacing: CGFloat + + var body: some View { + Toggle(isOn: isOn) { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .fixMultilineScrollableText() + + TextMenuItemCaption(text: description) + } + }.toggleStyle(.checkbox) + } + } + + struct SpacedCheckbox: View where Content: View { + @ViewBuilder let content: () -> Content + + var body: some View { + VStack(alignment: .leading) { + content() + }.padding(.bottom, Const.Spacing.groupedCheckboxesSeparation) + } + } } diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index 570ef2efe0..8b0c2fdbd4 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -59,6 +59,12 @@ extension Preferences { AppearanceView(model: .shared) case .privacy: PrivacyView(model: PrivacyPreferencesModel()) + +#if NETWORK_PROTECTION + case .vpn: + VPNView(model: VPNPreferencesModel()) +#endif + #if SUBSCRIPTION case .subscription: makeSubscriptionView() diff --git a/DuckDuckGo/Preferences/View/PreferencesVPNView.swift b/DuckDuckGo/Preferences/View/PreferencesVPNView.swift new file mode 100644 index 0000000000..b30d3b8358 --- /dev/null +++ b/DuckDuckGo/Preferences/View/PreferencesVPNView.swift @@ -0,0 +1,107 @@ +// +// PreferencesPrivacyView.swift +// +// Copyright © 2022 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 NETWORK_PROTECTION + +import SwiftUI +import SwiftUIExtensions + +extension Preferences { + + struct VPNView: View { + @ObservedObject var model: VPNPreferencesModel + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + + // TITLE + + TextMenuTitle(text: UserText.vpn) + + // SECTION: Manage VPN + + PreferencePaneSection { + TextMenuItemHeader(text: UserText.vpnGeneralTitle) + + SpacedCheckbox { + ToggleMenuItem(title: UserText.vpnConnectOnLoginSettingTitle, isOn: $model.connectOnLogin) + } + + SpacedCheckbox { + ToggleMenuItem(title: UserText.vpnShowInMenuBarSettingTitle, isOn: $model.showInMenuBar) + } + + SpacedCheckbox { + ToggleMenuItemWithDescription(title: UserText.vpnExcludeLocalNetworksSettingTitle, + description: UserText.vpnExcludeLocalNetworksSettingDescription, + isOn: $model.excludeLocalNetworks, + spacing: 12) + } + + VStack(alignment: .leading) { + HStack(spacing: 10) { + Image("InfoSubtle-16") + + VStack { + HStack { + Text(UserText.vpnSecureDNSSettingDescription) + .padding(0) + .font(.system(size: 11)) + .foregroundColor(Color("BlackWhite60")) + .multilineTextAlignment(.leading) + .fixMultilineScrollableText() + + Spacer() + } + } + .frame(idealWidth: .infinity, maxWidth: .infinity) + + Spacer() + } + }.frame(alignment: .topLeading) + .frame(idealWidth: .infinity, maxWidth: .infinity) + .padding(10) + .background(Color("BlackWhite1")) + .roundedBorder() + } + + // SECTION: VPN Notifications + + PreferencePaneSection { + TextMenuItemHeader(text: UserText.vpnNotificationsSettingsTitle) + + ToggleMenuItem(title: "VPN connection drops or status changes", isOn: $model.notifyStatusChanges) + } + + // SECTION: Uninstall + + if model.showUninstallVPN { + PreferencePaneSection { + Button(UserText.uninstallVPNButtonTitle) { + Task { @MainActor in + await model.uninstallVPN() + } + } + } + } + } + } + } +} + +#endif diff --git a/DuckDuckGo/Preferences/View/PreferencesViewController.swift b/DuckDuckGo/Preferences/View/PreferencesViewController.swift index f7920d8f60..0289ac6afc 100644 --- a/DuckDuckGo/Preferences/View/PreferencesViewController.swift +++ b/DuckDuckGo/Preferences/View/PreferencesViewController.swift @@ -20,6 +20,10 @@ import AppKit import SwiftUI import Combine +#if NETWORK_PROTECTION +import NetworkProtection +#endif + final class PreferencesViewController: NSViewController { weak var delegate: BrowserTabSelectionDelegate? diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift index 0afc000b19..fe00557e56 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureDisabler.swift @@ -27,21 +27,25 @@ import NetworkProtectionUI import SystemExtensions protocol NetworkProtectionFeatureDisabling { - func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) + /// - Returns: `true` if the uninstallation was completed. `false` if it was cancelled by the user or an error. + /// + func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) async -> Bool } 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 - private let settings: TunnelSettings + private let settings: VPNSettings private let userDefaults: UserDefaults private let ipcClient: TunnelControllerIPCClient init(loginItemsManager: LoginItemsManager = LoginItemsManager(), pinningManager: LocalPinningManager = .shared, userDefaults: UserDefaults = .netP, - settings: TunnelSettings = .init(defaults: .netP), + settings: VPNSettings = .init(defaults: .netP), ipcClient: TunnelControllerIPCClient = TunnelControllerIPCClient(machServiceName: Bundle.main.vpnMenuAgentBundleId), log: OSLog = .networkProtection) { @@ -59,34 +63,36 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling /// - keepAuthToken: If `true`, the auth token will not be removed. /// - includeSystemExtension: Whether this method should uninstall the system extension. /// - func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) { - Task { - // To disable NetP we need the login item to be running - // This should be fine though as we'll disable them further down below - enableLoginItems() - - // Allow some time for the login items to fully launch - try? await Task.sleep(interval: 0.5) - - unpinNetworkProtection() - - if uninstallSystemExtension { - await removeSystemExtension() + @discardableResult + func disable(keepAuthToken: Bool, uninstallSystemExtension: Bool) async -> Bool { + // To disable NetP we need the login item to be running + // This should be fine though as we'll disable them further down below + enableLoginItems() + + // Allow some time for the login items to fully launch + try? await Task.sleep(interval: 0.5) + + if uninstallSystemExtension { + do { + try await removeSystemExtension() + } catch { + return false } + } - await removeVPNConfiguration() - - // We want to give some time for the login item to reset state before disabling it - try? await Task.sleep(interval: 0.5) - - disableLoginItems() - - resetUserDefaults() + try? await removeVPNConfiguration() + // We want to give some time for the login item to reset state before disabling it + try? await Task.sleep(interval: 0.5) + disableLoginItems() + resetUserDefaults() - if !keepAuthToken { - try? removeAppAuthToken() - } + if !keepAuthToken { + try? removeAppAuthToken() } + + unpinNetworkProtection() + postVPNUninstalledNotification() + return true } private func enableLoginItems() { @@ -97,12 +103,8 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling loginItemsManager.disableLoginItems(LoginItemsManager.networkProtectionLoginItems) } - func removeSystemExtension() async { - await ipcClient.debugCommand(.removeSystemExtension) - -#if NETP_SYSTEM_EXTENSION - userDefaults.networkProtectionOnboardingStatusRawValue = OnboardingStatus.default.rawValue -#endif + func removeSystemExtension() async throws { + try await ipcClient.debugCommand(.removeSystemExtension) } private func unpinNetworkProtection() { @@ -113,11 +115,12 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling try NetworkProtectionKeychainTokenStore().deleteToken() } - private func removeVPNConfiguration() async { + private func removeVPNConfiguration() async throws { // Remove the agent VPN configuration - await ipcClient.debugCommand(.removeVPNConfiguration) + try await ipcClient.debugCommand(.removeVPNConfiguration) // Remove the legacy (local) configuration + // We don't care if this fails let tunnels = try? await NETunnelProviderManager.loadAllFromPreferences() if let tunnels = tunnels { @@ -130,7 +133,17 @@ final class NetworkProtectionFeatureDisabler: NetworkProtectionFeatureDisabling private func resetUserDefaults() { settings.resetToDefaults() - userDefaults.networkProtectionOnboardingStatusRawValue = OnboardingStatus.default.rawValue + } + + private func postVPNUninstalledNotification() { + Task { @MainActor in + // Wait a bit since the NetP button is likely being hidden + try? await Task.sleep(nanoseconds: 500 * NSEC_PER_MSEC) + + NotificationCenter.default.post( + name: Self.vpnUninstalledNotificationName, + object: nil) + } } } diff --git a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift index e4eb4daebb..370acfc068 100644 --- a/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift +++ b/DuckDuckGo/Waitlist/NetworkProtectionFeatureVisibility.swift @@ -19,6 +19,7 @@ #if NETWORK_PROTECTION import BrowserServicesKit +import Combine import Common import NetworkExtension import NetworkProtection @@ -34,7 +35,9 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { private let featureDisabler: NetworkProtectionFeatureDisabling private let featureOverrides: WaitlistBetaOverriding private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation + private let networkProtectionWaitlist = NetworkProtectionWaitlist() private let privacyConfigurationManager: PrivacyConfigurationManaging + private let defaults: UserDefaults var waitlistIsOngoing: Bool { isWaitlistEnabled && isWaitlistBetaActive @@ -44,12 +47,14 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), featureOverrides: WaitlistBetaOverriding = DefaultWaitlistBetaOverrides(), featureDisabler: NetworkProtectionFeatureDisabling = NetworkProtectionFeatureDisabler(), + defaults: UserDefaults = .netP, log: OSLog = .networkProtection) { self.privacyConfigurationManager = privacyConfigurationManager self.networkProtectionFeatureActivation = networkProtectionFeatureActivation self.featureDisabler = featureDisabler self.featureOverrides = featureOverrides + self.defaults = defaults } /// Calculates whether Network Protection is visible. @@ -63,6 +68,18 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { isEasterEggUser || waitlistIsOngoing } + /// Whether the user is fully onboarded + /// + var isOnboarded: Bool { + defaults.networkProtectionOnboardingStatus == .completed + } + + /// A publisher for the onboarding status + /// + var onboardStatusPublisher: AnyPublisher { + defaults.networkProtectionOnboardingStatusPublisher + } + /// Easter egg users can be identified by them being internal users and having an auth token (NetP being activated). /// private var isEasterEggUser: Bool { @@ -77,7 +94,13 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { /// Waitlist users are users that have the waitlist enabled and active /// private var isWaitlistUser: Bool { - NetworkProtectionWaitlist().waitlistStorage.isWaitlistUser + networkProtectionWaitlist.waitlistStorage.isWaitlistUser + } + + /// Waitlist users are users that have the waitlist enabled and active and are invited + /// + private var isInvitedWaitlistUser: Bool { + networkProtectionWaitlist.waitlistStorage.isWaitlistUser && networkProtectionWaitlist.waitlistStorage.isInvited } private var isWaitlistBetaActive: Bool { @@ -107,7 +130,9 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { } func disableForAllUsers() { - featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) + Task { + await featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) + } } func disableForWaitlistUsers() { @@ -115,7 +140,9 @@ struct DefaultNetworkProtectionVisibility: NetworkProtectionFeatureVisibility { return } - featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) + Task { + await featureDisabler.disable(keepAuthToken: false, uninstallSystemExtension: false) + } } } diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index 2f7c530c3d..5a2beeadfe 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -186,7 +186,12 @@ extension WindowControllersManager { } func showTab(with content: Tab.TabContent) { - guard let windowController = self.mainWindowController else { return } + guard let windowController = self.mainWindowController else { + let tabCollection = TabCollection(tabs: [Tab(content: content)]) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + WindowsManager.openNewWindow(with: tabCollectionViewModel) + return + } let viewController = windowController.mainViewController let tabCollectionViewModel = viewController.tabCollectionViewModel diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index f0e3f0a2f3..4fb762d46c 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -19,6 +19,7 @@ import Cocoa import Combine import Common +import LoginItems import Networking import NetworkExtension import NetworkProtection @@ -57,6 +58,8 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private let appLauncher = AppLauncher() private let bouncer = NetworkProtectionBouncer() + private var cancellables = Set() + var networkExtensionBundleID: String { Bundle.main.networkExtensionBundleID } @@ -65,10 +68,12 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { private lazy var networkExtensionController = NetworkExtensionController(extensionBundleID: networkExtensionBundleID) #endif + private lazy var tunnelSettings = VPNSettings(defaults: .netP) + private lazy var tunnelController = NetworkProtectionTunnelController( networkExtensionBundleID: networkExtensionBundleID, networkExtensionController: networkExtensionController, - settings: .init(defaults: .netP)) + settings: tunnelSettings) /// An IPC server that provides access to the tunnel controller. /// @@ -111,6 +116,11 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { /// @MainActor private lazy var networkProtectionMenu: StatusBarMenu = { + makeStatusBarMenu() + }() + + @MainActor + private func makeStatusBarMenu() -> StatusBarMenu { #if DEBUG let iconProvider = DebugMenuIconProvider() #elseif REVIEW @@ -119,32 +129,35 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { let iconProvider = MenuIconProvider() #endif - let menuItems = [ - StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuShareFeedback, action: { [weak self] in - await self?.appLauncher.launchApp(withCommand: .shareFeedback) - }), - StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuOpenDuckDuckGo, action: { [weak self] in - await self?.appLauncher.launchApp(withCommand: .justOpen) - }) - ] - let onboardingStatusPublisher = UserDefaults.netP.publisher(for: \.networkProtectionOnboardingStatusRawValue).map { rawValue in OnboardingStatus(rawValue: rawValue) ?? .default }.eraseToAnyPublisher() + let tunnelSettings = self.tunnelSettings + return StatusBarMenu( onboardingStatusPublisher: onboardingStatusPublisher, statusReporter: statusReporter, controller: tunnelController, - iconProvider: iconProvider, - menuItems: menuItems) - }() + iconProvider: iconProvider) { + [ + StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuVPNSettings, action: { [weak self] in + await self?.appLauncher.launchApp(withCommand: .showSettings) + }), + StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuShareFeedback, action: { [weak self] in + await self?.appLauncher.launchApp(withCommand: .shareFeedback) + }) + ] + } + } + @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { APIRequest.Headers.setUserAgent(UserAgent.duckDuckGoUserAgent()) os_log("DuckDuckGoVPN started", log: .networkProtectionLoginItemLog, type: .info) - networkProtectionMenu.show() + + setupMenuVisibility() bouncer.requireAuthTokenOrKillApp() @@ -162,6 +175,39 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { onComplete(error) } } + + let launchInformation = LoginItemLaunchInformation(agentBundleID: Bundle.main.bundleIdentifier!, defaults: .netP) + let launchedOnStartup = launchInformation.wasLaunchedByStartup + launchInformation.update() + + if launchedOnStartup { + Task { + let isConnected = await tunnelController.isConnected + + if !isConnected && tunnelSettings.connectOnLogin { + await tunnelController.start() + } + } + } + } + + @MainActor + private func setupMenuVisibility() { + if tunnelSettings.showInMenuBar { + networkProtectionMenu.show() + } else { + networkProtectionMenu.hide() + } + + tunnelSettings.showInMenuBarPublisher.sink { [weak self] showInMenuBar in + Task { @MainActor in + if showInMenuBar { + self?.networkProtectionMenu.show() + } else { + self?.networkProtectionMenu.hide() + } + } + }.store(in: &cancellables) } } diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index 3210cf2be2..79687fd9e2 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -20,6 +20,7 @@ import Combine import Foundation import NetworkProtection import NetworkProtectionIPC +import NetworkProtectionUI /// Takes care of handling incoming IPC requests from clients that need to be relayed to the tunnel, and handling state /// changes that need to be relayed back to IPC clients. @@ -33,15 +34,18 @@ final class TunnelControllerIPCService { private let server: NetworkProtectionIPC.TunnelControllerIPCServer private let statusReporter: NetworkProtectionStatusReporter private var cancellables = Set() + private let defaults: UserDefaults init(tunnelController: TunnelController, networkExtensionController: NetworkExtensionController, - statusReporter: NetworkProtectionStatusReporter) { + statusReporter: NetworkProtectionStatusReporter, + defaults: UserDefaults = .netP) { self.tunnelController = tunnelController self.networkExtensionController = networkExtensionController server = .init(machServiceName: Bundle.main.bundleIdentifier!) self.statusReporter = statusReporter + self.defaults = defaults subscribeToErrorChanges() subscribeToStatusUpdates() @@ -107,18 +111,15 @@ extension TunnelControllerIPCService: IPCServerInterface { try? await networkExtensionController.deactivateSystemExtension() } - func debugCommand(_ command: DebugCommand) async { - if let activeSession = try? await ConnectionSessionUtilities.activeSession(networkExtensionBundleID: Bundle.main.networkExtensionBundleID) { - - // First give a chance to the extension to process the command, since some commands - // may remove the VPN configuration or deactivate the extension. - try? await activeSession.sendProviderRequest(.debugCommand(command)) - } + func debugCommand(_ command: DebugCommand) async throws { + _ = try await ConnectionSessionUtilities.activeSession(networkExtensionBundleID: Bundle.main.networkExtensionBundleID) switch command { case .removeSystemExtension: - await VPNConfigurationManager().removeVPNConfiguration() - try? await networkExtensionController.deactivateSystemExtension() +#if NETP_SYSTEM_EXTENSION + try await networkExtensionController.deactivateSystemExtension() + defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowExtension) +#endif case .expireRegistrationKey: // Intentional no-op: handled by the extension break @@ -127,6 +128,10 @@ extension TunnelControllerIPCService: IPCServerInterface { break case .removeVPNConfiguration: await VPNConfigurationManager().removeVPNConfiguration() + + if defaults.networkProtectionOnboardingStatus == .completed { + defaults.networkProtectionOnboardingStatus = .isOnboarding(step: .userNeedsToAllowVPNConfiguration) + } } } } diff --git a/DuckDuckGoVPN/UserText.swift b/DuckDuckGoVPN/UserText.swift index 227f211608..050d601727 100644 --- a/DuckDuckGoVPN/UserText.swift +++ b/DuckDuckGoVPN/UserText.swift @@ -22,5 +22,5 @@ final class UserText { // MARK: - Status Menu static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share Feedback...", comment: "The status menu 'Share Feedback' menu item") - static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.open.duckduckgo", value: "Open DuckDuckGo...", comment: "The status menu 'Open DuckDuckGo' menu item") + static let networkProtectionStatusMenuVPNSettings = NSLocalizedString("network.protection.status.menu.vpn.settings", value: "VPN Settings...", comment: "The status menu 'VPN Settings' menu item") } diff --git a/LocalPackages/Account/Package.swift b/LocalPackages/Account/Package.swift index c93ca48150..1f44709b52 100644 --- a/LocalPackages/Account/Package.swift +++ b/LocalPackages/Account/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["Account"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "85.1.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "86.0.0"), .package(path: "../Purchase") ], targets: [ diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index ee121cf018..e27f0fb5fd 100644 --- a/LocalPackages/DataBrokerProtection/Package.swift +++ b/LocalPackages/DataBrokerProtection/Package.swift @@ -29,7 +29,7 @@ let package = Package( targets: ["DataBrokerProtection"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "85.1.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "86.0.0"), .package(path: "../PixelKit"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper") diff --git a/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift b/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift index a826b70904..9e9d53a824 100644 --- a/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift +++ b/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift @@ -26,6 +26,8 @@ import ServiceManagement public struct LoginItem: Equatable, Hashable { let agentBundleID: String + private let launchInformation: LoginItemLaunchInformation + private let defaults: UserDefaults private let log: OSLog public var isRunning: Bool { @@ -70,8 +72,10 @@ public struct LoginItem: Equatable, Hashable { return Status(SMAppService.loginItem(identifier: agentBundleID).status) } - public init(bundleId: String, log: OSLog = .disabled) { + public init(bundleId: String, defaults: UserDefaults, log: OSLog = .disabled) { self.agentBundleID = bundleId + self.defaults = defaults + self.launchInformation = LoginItemLaunchInformation(agentBundleID: bundleId, defaults: defaults) self.log = log } @@ -83,6 +87,8 @@ public struct LoginItem: Equatable, Hashable { } else { SMLoginItemSetEnabled(agentBundleID as CFString, true) } + + launchInformation.updateLastEnabledTimestamp() } public func disable() throws { diff --git a/LocalPackages/LoginItems/Sources/LoginItems/LoginItemLaunchInformation.swift b/LocalPackages/LoginItems/Sources/LoginItems/LoginItemLaunchInformation.swift new file mode 100644 index 0000000000..f6e559f430 --- /dev/null +++ b/LocalPackages/LoginItems/Sources/LoginItems/LoginItemLaunchInformation.swift @@ -0,0 +1,99 @@ +// +// LoginItemLaunchInformation.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 AppKit +// swiftlint:disable:next enforce_os_log_wrapper +import os.log + +public struct LoginItemLaunchInformation: Equatable, Hashable { + + private let agentBundleID: String + private let defaults: UserDefaults + private let launchRecencyThreshold = 5.0 + + public init(agentBundleID: String, defaults: UserDefaults) { + self.agentBundleID = agentBundleID + self.defaults = defaults + } + + // MARK: - Launch Information + + private func systemBootTime() -> Date { + var tv = timeval() + var tvSize = MemoryLayout.size + let err = sysctlbyname("kern.boottime", &tv, &tvSize, nil, 0) + guard err == 0, tvSize == MemoryLayout.size else { + return Date(timeIntervalSince1970: 0) + } + return Date(timeIntervalSince1970: Double(tv.tv_sec) + Double(tv.tv_usec) / 1_000_000.0) + } + + /// Lets the login item app know if it was launched by the computer startup. + /// + public var wasLaunchedByStartup: Bool { + let lastSystemBootTime = self.systemBootTime() + let lastRunTime = Date(timeIntervalSince1970: lastRunTimestamp) + let lastEnabledTime = Date(timeIntervalSince1970: lastEnabledTimestamp) + + return lastSystemBootTime > lastRunTime + && lastSystemBootTime > lastEnabledTime + } + + /// The login item app should call this after checking `wasLaunchedByStartup`. + /// + public func update() { + updateLastRunTimestamp() + } + + // MARK: - Last Enabled + + private static let loginItemLastEnabledTimestampKey = "loginItemLastEnabledTimestampKey" + + private func lastEnabledKey(forAgentBundleID agentBundleID: String) -> String { + Self.loginItemLastEnabledTimestampKey + "_" + agentBundleID + } + + var lastEnabledTimestamp: TimeInterval { + defaults.double(forKey: lastEnabledKey(forAgentBundleID: agentBundleID)) + } + + func updateLastEnabledTimestamp() { + defaults.set( + Date().timeIntervalSince1970, + forKey: lastEnabledKey(forAgentBundleID: agentBundleID)) + } + + // MARK: - Last Run + + private static let loginItemLastRunTimestampKey = "loginItemLastRunTimestampKey" + + private func lastRunKey(forAgentBundleID agentBundleID: String) -> String { + Self.loginItemLastRunTimestampKey + "_" + agentBundleID + } + + var lastRunTimestamp: TimeInterval { + defaults.double(forKey: lastRunKey(forAgentBundleID: agentBundleID)) + } + + func updateLastRunTimestamp() { + defaults.set( + Date().timeIntervalSince1970, + forKey: lastRunKey(forAgentBundleID: agentBundleID)) + } +} diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index a9655152d1..ea40c43fe6 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -30,7 +30,7 @@ let package = Package( .library(name: "NetworkProtectionUI", targets: ["NetworkProtectionUI"]) ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "85.1.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "86.0.0"), .package(path: "../XPCHelper"), .package(path: "../SwiftUIExtensions") ], diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift index dd34206618..92ec6e6425 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCClient.swift @@ -95,20 +95,24 @@ extension TunnelControllerIPCClient: IPCServerInterface { }) } - public func debugCommand(_ command: DebugCommand) async { + public func debugCommand(_ command: DebugCommand) async throws { guard let payload = try? JSONEncoder().encode(command) else { return } - await withCheckedContinuation { continuation in + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in xpc.execute(call: { server in - server.debugCommand(payload) { - continuation.resume() + server.debugCommand(payload) { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } } - }, xpcReplyErrorHandler: { _ in + }, xpcReplyErrorHandler: { error in // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! - continuation.resume() + continuation.resume(throwing: error) }) } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift index 8ef52836e8..3fe62c74f3 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionIPC/TunnelControllerIPCServer.swift @@ -39,7 +39,7 @@ public protocol IPCServerInterface: AnyObject { /// Debug commands /// - func debugCommand(_ command: DebugCommand) async + func debugCommand(_ command: DebugCommand) async throws } /// This protocol describes the server-side XPC interface. @@ -65,12 +65,16 @@ protocol XPCServerInterface { /// Debug commands /// - func debugCommand(_ payload: Data, completion: @escaping () -> Void) + func debugCommand(_ payload: Data, completion: @escaping (Error?) -> Void) } public final class TunnelControllerIPCServer { let xpc: XPCServer + enum IPCError: Error { + case cannotDecodeDebugCommand + } + /// The delegate. /// public weak var serverDelegate: IPCServerInterface? @@ -148,15 +152,19 @@ extension TunnelControllerIPCServer: XPCServerInterface { serverDelegate?.stop() } - func debugCommand(_ payload: Data, completion: @escaping () -> Void) { + func debugCommand(_ payload: Data, completion: @escaping (Error?) -> Void) { guard let command = try? JSONDecoder().decode(DebugCommand.self, from: payload) else { - completion() + completion(IPCError.cannotDecodeDebugCommand) return } Task { - await serverDelegate?.debugCommand(command) - completion() + do { + try await serverDelegate?.debugCommand(command) + completion(nil) + } catch { + completion(error) + } } } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift index 5a35ae2b4d..a73363a9c8 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift @@ -30,7 +30,7 @@ final class UserText { // MARK: - Onboarding - static let networkProtectionOnboardingAllowExtensionTitle = NSLocalizedString("network.protection.onboarding.allow.extension.title", value: "Allow System Extension", comment: "Title for the onboarding allow-extension step") + static let networkProtectionOnboardingInstallExtensionTitle = NSLocalizedString("network.protection.onboarding.install.extension.title", value: "Install VPN System Extension", comment: "Title for the onboarding install-vpn-extension step") static let networkProtectionOnboardingAllowExtensionDescPrefix = NSLocalizedString("network.protection.onboarding.allow.extension.desc.prefix", value: "Open System Settings to Privacy & Security. Scroll and select ", comment: "Non-bold prefix for the onboarding allow-extension description") static let networkProtectionOnboardingAllowExtensionDescAllow = NSLocalizedString("network.protection.onboarding.allow.extension.desc.allow", value: "Allow", comment: "'Allow' word between the prefix and suffix for the onboarding allow-extension description") static let networkProtectionOnboardingAllowExtensionDescSuffix = NSLocalizedString("network.protection.onboarding.allow.extension.desc.suffix", value: " for DuckDuckGo software.", comment: "Non-bold suffix for the onboarding allow-extension description") diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/NetworkProtectionStatusBarMenu.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/NetworkProtectionStatusBarMenu.swift index c7fe0059cc..890d1bee8f 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/NetworkProtectionStatusBarMenu.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Menu/NetworkProtectionStatusBarMenu.swift @@ -48,7 +48,7 @@ public final class StatusBarMenu { statusReporter: NetworkProtectionStatusReporter, controller: TunnelController, iconProvider: IconProvider, - menuItems: [MenuItem]) { + menuItems: @escaping () -> [MenuItem]) { self.statusItem = statusItem ?? NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) self.iconPublisher = NetworkProtectionIconPublisher(statusReporter: statusReporter, iconProvider: iconProvider) diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift index 47f25e7192..a4668c3484 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/NetworkProtectionPopover.swift @@ -48,7 +48,7 @@ public final class NetworkProtectionPopover: NSPopover { public required init(controller: TunnelController, onboardingStatusPublisher: OnboardingStatusPublisher, statusReporter: NetworkProtectionStatusReporter, - menuItems: [MenuItem]) { + menuItems: @escaping () -> [MenuItem]) { self.statusReporter = statusReporter @@ -67,7 +67,7 @@ public final class NetworkProtectionPopover: NSPopover { private func setupContentController(controller: TunnelController, onboardingStatusPublisher: OnboardingStatusPublisher, statusReporter: NetworkProtectionStatusReporter, - menuItems: [MenuItem]) { + menuItems: @escaping () -> [MenuItem]) { let model = NetworkProtectionStatusView.Model(controller: controller, onboardingStatusPublisher: onboardingStatusPublisher, diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/OnboardingStepView/OnboardingStepViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/OnboardingStepView/OnboardingStepViewModel.swift index de6d796351..72871f1140 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/OnboardingStepView/OnboardingStepViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/OnboardingStepView/OnboardingStepViewModel.swift @@ -54,7 +54,7 @@ extension OnboardingStepView { var title: String { switch step { case .userNeedsToAllowExtension: - return UserText.networkProtectionOnboardingAllowExtensionTitle + return UserText.networkProtectionOnboardingInstallExtensionTitle case .userNeedsToAllowVPNConfiguration: return UserText.networkProtectionOnboardingAllowVPNTitle } diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift index a138e4544c..ab6d1fcd26 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusView.swift @@ -97,7 +97,7 @@ public struct NetworkProtectionStatusView: View { private func bottomMenuView() -> some View { VStack(spacing: 0) { - ForEach(model.menuItems, id: \.name) { menuItem in + ForEach(model.menuItems(), id: \.name) { menuItem in MenuItemButton(menuItem.name, textColor: Color(.defaultText)) { await menuItem.action() dismiss() diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 3d7e8d489c..efef7ab117 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -68,7 +68,7 @@ extension NetworkProtectionStatusView { // MARK: - Extra Menu Items - public let menuItems: [MenuItem] + public let menuItems: () -> [MenuItem] // MARK: - Misc @@ -90,7 +90,7 @@ extension NetworkProtectionStatusView { public init(controller: TunnelController, onboardingStatusPublisher: OnboardingStatusPublisher, statusReporter: NetworkProtectionStatusReporter, - menuItems: [MenuItem], + menuItems: @escaping () -> [MenuItem], runLoopMode: RunLoop.Mode? = nil) { self.tunnelController = controller diff --git a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift index 39236a194e..56842e070e 100644 --- a/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift +++ b/LocalPackages/NetworkProtectionMac/Tests/NetworkProtectionUITests/NetworkProtectionStatusBarMenuTests.swift @@ -49,7 +49,7 @@ final class StatusBarMenuTests: XCTestCase { statusReporter: MockNetworkProtectionStatusReporter(), controller: TestTunnelController(), iconProvider: MenuIconProvider(), - menuItems: []) + menuItems: { [] }) menu.show() @@ -65,7 +65,7 @@ final class StatusBarMenuTests: XCTestCase { statusReporter: MockNetworkProtectionStatusReporter(), controller: TestTunnelController(), iconProvider: MenuIconProvider(), - menuItems: []) + menuItems: { [] }) menu.hide() diff --git a/UnitTests/Preferences/PreferencesSidebarModelTests.swift b/UnitTests/Preferences/PreferencesSidebarModelTests.swift index 61e59bf439..f601cb90f4 100644 --- a/UnitTests/Preferences/PreferencesSidebarModelTests.swift +++ b/UnitTests/Preferences/PreferencesSidebarModelTests.swift @@ -31,7 +31,7 @@ final class PreferencesSidebarModelTests: XCTestCase { } private func PreferencesSidebarModel(loadSections: [PreferencesSection]? = nil, tabSwitcherTabs: [Tab.TabContent] = Tab.TabContent.displayableTabTypes) -> DuckDuckGo_Privacy_Browser.PreferencesSidebarModel { - return DuckDuckGo_Privacy_Browser.PreferencesSidebarModel(loadSections: { loadSections ?? PreferencesSection.defaultSections(includingDuckPlayer: false) }, tabSwitcherTabs: tabSwitcherTabs, privacyConfigurationManager: MockPrivacyConfigurationManager()) + return DuckDuckGo_Privacy_Browser.PreferencesSidebarModel(loadSections: { loadSections ?? PreferencesSection.defaultSections(includingDuckPlayer: false, includingVPN: false) }, tabSwitcherTabs: tabSwitcherTabs, privacyConfigurationManager: MockPrivacyConfigurationManager()) } func testWhenInitializedThenFirstPaneInFirstSectionIsSelected() throws {