From 04d229aad4681a54ac4d9560152f6e11e5b970bc Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:08:26 -0400 Subject: [PATCH 01/14] Unified feedback form for Privacy Pro (#3058) Task/Issue URL: https://app.asana.com/0/1205922231854923/1207701941801991/f Tech Design URL: CC: **Description**: **Steps to test this PR**: 1. **Definition of Done**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 162 ++++++--- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../PPro-Feedback.imageset/Contents.json | 16 + .../Privacy-Pro-16D.svg | 11 + .../UserText+NetworkProtection.swift | 78 ++++- DuckDuckGo/Common/Localizables/UserText.swift | 1 + DuckDuckGo/Localizable.xcstrings | 14 +- DuckDuckGo/Menus/MainMenuActions.swift | 5 + .../NavigationBar/View/MoreOptionsMenu.swift | 24 +- .../View/NavigationBarViewController.swift | 9 + ...etworkProtectionNavBarPopoverManager.swift | 4 +- .../BothAppTargets/VPNURLEventHandler.swift | 2 +- .../View/PreferencesRootView.swift | 5 +- DuckDuckGo/Statistics/GeneralPixel.swift | 55 ++++ .../FeedbackCategoryProviding.swift | 198 +++++++++++ .../EmptyMetadataCollector.swift | 32 ++ .../UnifiedMetadataCollector.swift | 42 +++ .../UnifiedFeedbackFormView.swift | 310 ++++++++++++++++++ .../UnifiedFeedbackFormViewController.swift | 129 ++++++++ .../UnifiedFeedbackFormViewModel.swift | 238 ++++++++++++++ .../UnifiedFeedbackSender.swift | 118 +++++++ .../VPNFeedbackForm/VPNFeedbackFormView.swift | 2 +- .../VPNFeedbackFormViewController.swift | 8 +- .../VPNFeedbackFormViewModel.swift | 2 +- .../VPNMetadataCollector.swift | 36 +- .../View/WindowControllersManager.swift | 20 +- DuckDuckGoDBPBackgroundAgent/UserText.swift | 2 +- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 2 +- DuckDuckGoVPN/UserText.swift | 1 + .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- .../UserText+NetworkProtectionUI.swift | 2 +- .../NetworkProtectionStatusViewModel.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 3 +- .../PreferencesSubscriptionModel.swift | 6 + .../PreferencesSubscriptionView.swift | 23 +- .../Sources/SubscriptionUI/UserText.swift | 3 + ...okerProtectionFeatureGatekeeperTests.swift | 3 + UnitTests/Menus/MoreOptionsMenuTests.swift | 3 +- .../VPNFeedbackFormViewModelTests.swift | 2 +- .../SubscriptionFeatureAvailabilityMock.swift | 4 +- .../UnifiedFeedbackFormViewModelTests.swift | 195 +++++++++++ 42 files changed, 1686 insertions(+), 94 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/PPro-Feedback.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/PPro-Feedback.imageset/Privacy-Pro-16D.svg create mode 100644 DuckDuckGo/UnifiedFeedbackForm/FeedbackCategoryProviding.swift create mode 100644 DuckDuckGo/UnifiedFeedbackForm/MetadataCollectors/EmptyMetadataCollector.swift create mode 100644 DuckDuckGo/UnifiedFeedbackForm/MetadataCollectors/UnifiedMetadataCollector.swift create mode 100644 DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormView.swift create mode 100644 DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewController.swift create mode 100644 DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewModel.swift create mode 100644 DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackSender.swift rename UnitTests/{VPNFeedbackForm => NetworkProtection}/VPNFeedbackFormViewModelTests.swift (99%) create mode 100644 UnitTests/UnifiedFeedbackForm/UnifiedFeedbackFormViewModelTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 074739e6fe..df3f38cb87 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1117,9 +1117,7 @@ 4B0511CA262CAA5A00F6079C /* FireproofDomainsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511B4262CAA5A00F6079C /* FireproofDomainsViewController.swift */; }; 4B0511E1262CAA8600F6079C /* NSOpenPanelExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */; }; 4B0511E2262CAA8600F6079C /* NSViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511E0262CAA8600F6079C /* NSViewControllerExtension.swift */; }; - 4B05265E2B1AE5C70054955A /* VPNMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B05265D2B1AE5C70054955A /* VPNMetadataCollector.swift */; }; - 4B0526612B1D55320054955A /* VPNFeedbackSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526602B1D55320054955A /* VPNFeedbackSender.swift */; }; - 4B0526642B1D55D80054955A /* VPNFeedbackCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526632B1D55D80054955A /* VPNFeedbackCategory.swift */; }; + 4B0526612B1D55320054955A /* UnifiedFeedbackSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526602B1D55320054955A /* UnifiedFeedbackSender.swift */; }; 4B0A63E8289DB58E00378EF7 /* FirefoxFaviconsReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0A63E7289DB58E00378EF7 /* FirefoxFaviconsReader.swift */; }; 4B0AACAC28BC63ED001038AC /* ChromiumFaviconsReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0AACAB28BC63ED001038AC /* ChromiumFaviconsReader.swift */; }; 4B0AACAE28BC6FD0001038AC /* SafariFaviconsReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0AACAD28BC6FD0001038AC /* SafariFaviconsReader.swift */; }; @@ -1174,12 +1172,10 @@ 4B41EDA72B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDA62B1543C9001EEDF4 /* PreferencesVPNView.swift */; }; 4B41EDA82B1543C9001EEDF4 /* PreferencesVPNView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDA62B1543C9001EEDF4 /* PreferencesVPNView.swift */; }; 4B41EDAB2B1544B2001EEDF4 /* LoginItems in Frameworks */ = {isa = PBXBuildFile; productRef = 4B41EDAA2B1544B2001EEDF4 /* LoginItems */; }; - 4B41EDAE2B168AFF001EEDF4 /* VPNFeedbackFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDAD2B168AFF001EEDF4 /* VPNFeedbackFormViewController.swift */; }; - 4B41EDAF2B168AFF001EEDF4 /* VPNFeedbackFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDAD2B168AFF001EEDF4 /* VPNFeedbackFormViewController.swift */; }; - 4B41EDB12B168B1E001EEDF4 /* VPNFeedbackFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDB02B168B1E001EEDF4 /* VPNFeedbackFormView.swift */; }; - 4B41EDB22B168B1E001EEDF4 /* VPNFeedbackFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDB02B168B1E001EEDF4 /* VPNFeedbackFormView.swift */; }; - 4B41EDB42B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDB32B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift */; }; - 4B41EDB52B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDB32B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift */; }; + 4B41EDAE2B168AFF001EEDF4 /* UnifiedFeedbackFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDAD2B168AFF001EEDF4 /* UnifiedFeedbackFormViewController.swift */; }; + 4B41EDAF2B168AFF001EEDF4 /* UnifiedFeedbackFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDAD2B168AFF001EEDF4 /* UnifiedFeedbackFormViewController.swift */; }; + 4B41EDB42B168C55001EEDF4 /* UnifiedFeedbackFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDB32B168C55001EEDF4 /* UnifiedFeedbackFormViewModel.swift */; }; + 4B41EDB52B168C55001EEDF4 /* UnifiedFeedbackFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41EDB32B168C55001EEDF4 /* UnifiedFeedbackFormViewModel.swift */; }; 4B434690285ED7A100177407 /* BookmarksBarViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B43468F285ED7A100177407 /* BookmarksBarViewModelTests.swift */; }; 4B43469528655D1400177407 /* FirefoxDataImporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B43469428655D1400177407 /* FirefoxDataImporterTests.swift */; }; 4B44FEF32B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B44FEF22B1FEF5A000619D8 /* FocusableTextEditor.swift */; }; @@ -1381,8 +1377,8 @@ 4BD18F01283F0BC500058124 /* BookmarksBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD18EFF283F0BC500058124 /* BookmarksBarViewController.swift */; }; 4BD18F05283F151F00058124 /* BookmarksBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4BD18F04283F151F00058124 /* BookmarksBar.storyboard */; }; 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */; }; - 4BE344EE2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; - 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; + 4BE344EE2B2376DF003FC223 /* UnifiedFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* UnifiedFeedbackFormViewModelTests.swift */; }; + 4BE344EF2B23786F003FC223 /* UnifiedFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* UnifiedFeedbackFormViewModelTests.swift */; }; 4BE3A6C12C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE3A6C02C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift */; }; 4BE3A6C22C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE3A6C02C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift */; }; 4BE4005327CF3DC3007D3161 /* SavePaymentMethodPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */; }; @@ -1424,9 +1420,7 @@ 4BF97AD72B43C53D00EB4240 /* NetworkProtectionIPCTunnelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BA7CC4D2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift */; }; 4BF97AD82B43C5B300EB4240 /* NetworkProtectionAppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2DDCF72A93A8BB0039D884 /* NetworkProtectionAppEvents.swift */; }; 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; - 4BF97ADA2B43C5DC00EB4240 /* VPNFeedbackCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526632B1D55D80054955A /* VPNFeedbackCategory.swift */; }; - 4BF97ADB2B43C5E000EB4240 /* VPNMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B05265D2B1AE5C70054955A /* VPNMetadataCollector.swift */; }; - 4BF97ADC2B43C5E200EB4240 /* VPNFeedbackSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526602B1D55320054955A /* VPNFeedbackSender.swift */; }; + 4BF97ADC2B43C5E200EB4240 /* UnifiedFeedbackSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0526602B1D55320054955A /* UnifiedFeedbackSender.swift */; }; 4BF97ADD2B43C5FC00EB4240 /* KeychainType+ClientDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3AF5C2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift */; }; 5601FECD29B7973D00068905 /* TabBarViewItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5601FECC29B7973D00068905 /* TabBarViewItemTests.swift */; }; 5603D90629B7B746007F9F01 /* MockTabViewItemDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603D90529B7B746007F9F01 /* MockTabViewItemDelegate.swift */; }; @@ -2555,6 +2549,14 @@ BD384ACA2BBC821A00EF3735 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; BD384ACB2BBC821B00EF3735 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; BD384ACC2BBC821B00EF3735 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; + BD7090CF2C5182FB009EED82 /* UnifiedFeedbackFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090CE2C5182FB009EED82 /* UnifiedFeedbackFormView.swift */; }; + BD7090D02C5182FB009EED82 /* UnifiedFeedbackFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090CE2C5182FB009EED82 /* UnifiedFeedbackFormView.swift */; }; + BD7090D22C52ECFE009EED82 /* UnifiedMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090D12C52ECFE009EED82 /* UnifiedMetadataCollector.swift */; }; + BD7090D32C52ECFE009EED82 /* UnifiedMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090D12C52ECFE009EED82 /* UnifiedMetadataCollector.swift */; }; + BD7090D62C540D5D009EED82 /* EmptyMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090D52C540D5D009EED82 /* EmptyMetadataCollector.swift */; }; + BD7090D72C540D5D009EED82 /* EmptyMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7090D52C540D5D009EED82 /* EmptyMetadataCollector.swift */; }; + BD88A83E2C4F3E4300460A26 /* FeedbackCategoryProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD88A83D2C4F3E4300460A26 /* FeedbackCategoryProviding.swift */; }; + BD88A83F2C4F3E4300460A26 /* FeedbackCategoryProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD88A83D2C4F3E4300460A26 /* FeedbackCategoryProviding.swift */; }; BDA7647C2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; BDA7647D2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; BDA7647F2BC4998900D0400C /* DefaultVPNLocationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */; }; @@ -2569,6 +2571,20 @@ BDADBDCB2BD2BC2800421B9B /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = BDADBDCA2BD2BC2800421B9B /* Lottie */; }; BDADBDCC2BD2BC4D00421B9B /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; BDADBDCD2BD2BC5700421B9B /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; + BDBA85902C5D252A00BC54F5 /* VPNFeedbackSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA858F2C5D252A00BC54F5 /* VPNFeedbackSender.swift */; }; + BDBA85912C5D252A00BC54F5 /* VPNFeedbackSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA858F2C5D252A00BC54F5 /* VPNFeedbackSender.swift */; }; + BDBA85932C5D255200BC54F5 /* VPNFeedbackCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA85922C5D255200BC54F5 /* VPNFeedbackCategory.swift */; }; + BDBA85942C5D255200BC54F5 /* VPNFeedbackCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA85922C5D255200BC54F5 /* VPNFeedbackCategory.swift */; }; + BDBA85962C5D256C00BC54F5 /* VPNFeedbackFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA85952C5D256C00BC54F5 /* VPNFeedbackFormView.swift */; }; + BDBA85972C5D256C00BC54F5 /* VPNFeedbackFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA85952C5D256C00BC54F5 /* VPNFeedbackFormView.swift */; }; + BDBA85992C5D258100BC54F5 /* VPNFeedbackFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA85982C5D258100BC54F5 /* VPNFeedbackFormViewController.swift */; }; + BDBA859A2C5D258100BC54F5 /* VPNFeedbackFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA85982C5D258100BC54F5 /* VPNFeedbackFormViewController.swift */; }; + BDBA859C2C5D25A300BC54F5 /* VPNFeedbackFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA859B2C5D25A300BC54F5 /* VPNFeedbackFormViewModel.swift */; }; + BDBA859D2C5D25A300BC54F5 /* VPNFeedbackFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA859B2C5D25A300BC54F5 /* VPNFeedbackFormViewModel.swift */; }; + BDBA859F2C5D25B700BC54F5 /* VPNMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA859E2C5D25B700BC54F5 /* VPNMetadataCollector.swift */; }; + BDBA85A02C5D25B700BC54F5 /* VPNMetadataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBA859E2C5D25B700BC54F5 /* VPNMetadataCollector.swift */; }; + BDCB66D82C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCB66D72C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift */; }; + BDCB66D92C7CE1A700E8ABC9 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCB66D72C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift */; }; BDE981D92BBD10D600645880 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; }; BDE981DA2BBD10D600645880 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; }; C1372EF42BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; }; @@ -3229,9 +3245,7 @@ 4B0511B4262CAA5A00F6079C /* FireproofDomainsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireproofDomainsViewController.swift; sourceTree = ""; }; 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSOpenPanelExtensions.swift; sourceTree = ""; }; 4B0511E0262CAA8600F6079C /* NSViewControllerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSViewControllerExtension.swift; sourceTree = ""; }; - 4B05265D2B1AE5C70054955A /* VPNMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNMetadataCollector.swift; sourceTree = ""; }; - 4B0526602B1D55320054955A /* VPNFeedbackSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackSender.swift; sourceTree = ""; }; - 4B0526632B1D55D80054955A /* VPNFeedbackCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackCategory.swift; sourceTree = ""; }; + 4B0526602B1D55320054955A /* UnifiedFeedbackSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedFeedbackSender.swift; sourceTree = ""; }; 4B0A63E7289DB58E00378EF7 /* FirefoxFaviconsReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirefoxFaviconsReader.swift; sourceTree = ""; }; 4B0AACAB28BC63ED001038AC /* ChromiumFaviconsReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChromiumFaviconsReader.swift; sourceTree = ""; }; 4B0AACAD28BC6FD0001038AC /* SafariFaviconsReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariFaviconsReader.swift; sourceTree = ""; }; @@ -3271,9 +3285,8 @@ 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 = ""; }; - 4B41EDAD2B168AFF001EEDF4 /* VPNFeedbackFormViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewController.swift; sourceTree = ""; }; - 4B41EDB02B168B1E001EEDF4 /* VPNFeedbackFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormView.swift; sourceTree = ""; }; - 4B41EDB32B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModel.swift; sourceTree = ""; }; + 4B41EDAD2B168AFF001EEDF4 /* UnifiedFeedbackFormViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedFeedbackFormViewController.swift; sourceTree = ""; }; + 4B41EDB32B168C55001EEDF4 /* UnifiedFeedbackFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedFeedbackFormViewModel.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 = ""; }; 4B44FEF22B1FEF5A000619D8 /* FocusableTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextEditor.swift; sourceTree = ""; }; @@ -3441,7 +3454,7 @@ 4BD18EFF283F0BC500058124 /* BookmarksBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksBarViewController.swift; sourceTree = ""; }; 4BD18F04283F151F00058124 /* BookmarksBar.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BookmarksBar.storyboard; sourceTree = ""; }; 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableScrollView.swift; sourceTree = ""; }; - 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.swift; sourceTree = ""; }; + 4BE344ED2B2376DF003FC223 /* UnifiedFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedFeedbackFormViewModelTests.swift; sourceTree = ""; }; 4BE3A6C02C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNRedditSessionWorkaround.swift; sourceTree = ""; }; 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodPopover.swift; sourceTree = ""; }; 4BE4005427CF3F19007D3161 /* SavePaymentMethodViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodViewController.swift; sourceTree = ""; }; @@ -4244,9 +4257,20 @@ BBFF355C2C4AF26200DA3289 /* BookmarksSortModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksSortModeTests.swift; sourceTree = ""; }; BD384AC72BBC821100EF3735 /* vpn-light-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-light-mode.json"; sourceTree = ""; }; BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "vpn-dark-mode.json"; sourceTree = ""; }; + BD7090CE2C5182FB009EED82 /* UnifiedFeedbackFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedFeedbackFormView.swift; sourceTree = ""; }; + BD7090D12C52ECFE009EED82 /* UnifiedMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnifiedMetadataCollector.swift; sourceTree = ""; }; + BD7090D52C540D5D009EED82 /* EmptyMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyMetadataCollector.swift; sourceTree = ""; }; + BD88A83D2C4F3E4300460A26 /* FeedbackCategoryProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackCategoryProviding.swift; sourceTree = ""; }; BDA7647B2BC497BE00D0400C /* DefaultVPNLocationFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatter.swift; sourceTree = ""; }; BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultVPNLocationFormatterTests.swift; sourceTree = ""; }; BDA764902BC4E57200D0400C /* MockVPNLocationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockVPNLocationFormatter.swift; sourceTree = ""; }; + BDBA858F2C5D252A00BC54F5 /* VPNFeedbackSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackSender.swift; sourceTree = ""; }; + BDBA85922C5D255200BC54F5 /* VPNFeedbackCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackCategory.swift; sourceTree = ""; }; + BDBA85952C5D256C00BC54F5 /* VPNFeedbackFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormView.swift; sourceTree = ""; }; + BDBA85982C5D258100BC54F5 /* VPNFeedbackFormViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewController.swift; sourceTree = ""; }; + BDBA859B2C5D25A300BC54F5 /* VPNFeedbackFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModel.swift; sourceTree = ""; }; + BDBA859E2C5D25B700BC54F5 /* VPNMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNMetadataCollector.swift; sourceTree = ""; }; + BDCB66D72C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.swift; sourceTree = ""; }; C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = ""; }; C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = ""; }; C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = ""; }; @@ -5325,17 +5349,17 @@ path = Surveys; sourceTree = ""; }; - 4B41EDAC2B168A66001EEDF4 /* VPNFeedbackForm */ = { + 4B41EDAC2B168A66001EEDF4 /* UnifiedFeedbackForm */ = { isa = PBXGroup; children = ( - 4B41EDAD2B168AFF001EEDF4 /* VPNFeedbackFormViewController.swift */, - 4B41EDB02B168B1E001EEDF4 /* VPNFeedbackFormView.swift */, - 4B41EDB32B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift */, - 4B0526632B1D55D80054955A /* VPNFeedbackCategory.swift */, - 4B05265D2B1AE5C70054955A /* VPNMetadataCollector.swift */, - 4B0526602B1D55320054955A /* VPNFeedbackSender.swift */, + BD88A83D2C4F3E4300460A26 /* FeedbackCategoryProviding.swift */, + 4B41EDAD2B168AFF001EEDF4 /* UnifiedFeedbackFormViewController.swift */, + 4B41EDB32B168C55001EEDF4 /* UnifiedFeedbackFormViewModel.swift */, + BD7090D42C540C0D009EED82 /* MetadataCollectors */, + 4B0526602B1D55320054955A /* UnifiedFeedbackSender.swift */, + BD7090CE2C5182FB009EED82 /* UnifiedFeedbackFormView.swift */, ); - path = VPNFeedbackForm; + path = UnifiedFeedbackForm; sourceTree = ""; }; 4B43468D285ED6BD00177407 /* BookmarksBar */ = { @@ -5985,6 +6009,7 @@ 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */, BDA7648C2BC4E4EF00D0400C /* DefaultVPNLocationFormatterTests.swift */, 7B4C5CF42BE51D640007A164 /* VPNUninstallerTests.swift */, + BDCB66D72C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift */, ); path = NetworkProtection; sourceTree = ""; @@ -6004,12 +6029,12 @@ path = View; sourceTree = ""; }; - 4BE344EC2B2376AE003FC223 /* VPNFeedbackForm */ = { + 4BE344EC2B2376AE003FC223 /* UnifiedFeedbackForm */ = { isa = PBXGroup; children = ( - 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */, + 4BE344ED2B2376DF003FC223 /* UnifiedFeedbackFormViewModelTests.swift */, ); - path = VPNFeedbackForm; + path = UnifiedFeedbackForm; sourceTree = ""; }; 4BF6961B28BE90E800D402D4 /* HomePage */ = { @@ -6960,7 +6985,8 @@ B6040859274B8C5200680351 /* UnprotectedDomains */, 1D72D5902BFF361700AEDE36 /* Updates */, AACF6FD426BC35C200CF09F9 /* UserAgent */, - 4B41EDAC2B168A66001EEDF4 /* VPNFeedbackForm */, + BDBA858E2C5D24EF00BC54F5 /* VPNFeedbackForm */, + 4B41EDAC2B168A66001EEDF4 /* UnifiedFeedbackForm */, 4B9DB0062A983B23000927DB /* Waitlist */, AA6EF9AE25066F99004754E6 /* Windows */, 31F28C4B28C8EE9000119F70 /* YoutubePlayer */, @@ -7022,7 +7048,7 @@ 4B9DB04D2A983B55000927DB /* Waitlist */, 3776582B27F7163B009A6B35 /* WebsiteBreakageReport */, 376718FE28E58504003A2A15 /* YoutubePlayer */, - 4BE344EC2B2376AE003FC223 /* VPNFeedbackForm */, + 4BE344EC2B2376AE003FC223 /* UnifiedFeedbackForm */, AA585D96248FD31400E9A3E2 /* Info.plist */, ); path = UnitTests; @@ -8437,6 +8463,15 @@ path = View; sourceTree = ""; }; + BD7090D42C540C0D009EED82 /* MetadataCollectors */ = { + isa = PBXGroup; + children = ( + BD7090D12C52ECFE009EED82 /* UnifiedMetadataCollector.swift */, + BD7090D52C540D5D009EED82 /* EmptyMetadataCollector.swift */, + ); + path = MetadataCollectors; + sourceTree = ""; + }; BDA7648F2BC4E56200D0400C /* Mocks */ = { isa = PBXGroup; children = ( @@ -8445,6 +8480,19 @@ path = Mocks; sourceTree = ""; }; + BDBA858E2C5D24EF00BC54F5 /* VPNFeedbackForm */ = { + isa = PBXGroup; + children = ( + BDBA858F2C5D252A00BC54F5 /* VPNFeedbackSender.swift */, + BDBA85922C5D255200BC54F5 /* VPNFeedbackCategory.swift */, + BDBA85952C5D256C00BC54F5 /* VPNFeedbackFormView.swift */, + BDBA85982C5D258100BC54F5 /* VPNFeedbackFormViewController.swift */, + BDBA859B2C5D25A300BC54F5 /* VPNFeedbackFormViewModel.swift */, + BDBA859E2C5D25B700BC54F5 /* VPNMetadataCollector.swift */, + ); + path = VPNFeedbackForm; + sourceTree = ""; + }; BDE981DB2BBD110800645880 /* Assets */ = { isa = PBXGroup; children = ( @@ -9956,6 +10004,7 @@ 7BFF35742C10D75000F89673 /* IPCServiceLauncher.swift in Sources */, F1D042A22BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, 3706FAA3293F65D500E42796 /* CrashReportPromptViewController.swift in Sources */, + BD88A83F2C4F3E4300460A26 /* FeedbackCategoryProviding.swift in Sources */, 3706FAA4293F65D500E42796 /* ContextMenuManager.swift in Sources */, 31AA6B982B960BA50025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */, 3706FAA5293F65D500E42796 /* GradientView.swift in Sources */, @@ -10096,7 +10145,6 @@ 3706FB05293F65D500E42796 /* Favicons.xcdatamodeld in Sources */, 3706FB07293F65D500E42796 /* Publisher.asVoid.swift in Sources */, 3706FB08293F65D500E42796 /* NavigationButtonMenuDelegate.swift in Sources */, - 4BF97ADB2B43C5E000EB4240 /* VPNMetadataCollector.swift in Sources */, 1DDC84F82B83558F00670238 /* PreferencesPrivateSearchView.swift in Sources */, 3706FB09293F65D500E42796 /* CrashReport.swift in Sources */, 1E0C72072ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */, @@ -10160,6 +10208,7 @@ 3706FB35293F65D500E42796 /* FlatButton.swift in Sources */, 3706FB36293F65D500E42796 /* PinnedTabView.swift in Sources */, 3706FB37293F65D500E42796 /* DataEncryption.swift in Sources */, + BD7090D32C52ECFE009EED82 /* UnifiedMetadataCollector.swift in Sources */, 56BA1E762BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */, 4B9DB0362A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */, 37197EA82942443D00394917 /* BrowserTabViewController.swift in Sources */, @@ -10213,6 +10262,7 @@ 3706FB5D293F65D500E42796 /* VisitMenuItem.swift in Sources */, 3706FB5E293F65D500E42796 /* EncryptionKeyStore.swift in Sources */, F1DA51872BF607D200CF29FA /* SubscriptionAttributionPixelHandler.swift in Sources */, + BDBA85A02C5D25B700BC54F5 /* VPNMetadataCollector.swift in Sources */, 3706FB60293F65D500E42796 /* PasswordManagementIdentityItemView.swift in Sources */, 3706FB61293F65D500E42796 /* ProgressExtension.swift in Sources */, 3706FB62293F65D500E42796 /* CSVParser.swift in Sources */, @@ -10271,6 +10321,7 @@ 3706FB82293F65D500E42796 /* PasswordManagementNoteItemView.swift in Sources */, 4BF97AD82B43C5B300EB4240 /* NetworkProtectionAppEvents.swift in Sources */, 3706FEC5293F6F0600E42796 /* BWInstallationService.swift in Sources */, + BDBA85972C5D256C00BC54F5 /* VPNFeedbackFormView.swift in Sources */, 3775912E29AAC72700E26367 /* SyncPreferences.swift in Sources */, 3706FB83293F65D500E42796 /* NSApplicationExtension.swift in Sources */, 37197EA42942441D00394917 /* NewWindowPolicy.swift in Sources */, @@ -10310,6 +10361,7 @@ B690152D2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */, 1D36F4252A3B85C50052B527 /* TabCleanupPreparer.swift in Sources */, 3706FB97293F65D500E42796 /* ActionSpeech.swift in Sources */, + BD7090D72C540D5D009EED82 /* EmptyMetadataCollector.swift in Sources */, B6AFE6BD29A5D621002FF962 /* HTTPSUpgradeTabExtension.swift in Sources */, 3706FB9A293F65D500E42796 /* FireproofDomainsStore.swift in Sources */, 3706FB9B293F65D500E42796 /* PrivacyDashboardPermissionHandler.swift in Sources */, @@ -10335,7 +10387,7 @@ 3706FBA4293F65D500E42796 /* ContentOverlayPopover.swift in Sources */, 3706FBA5293F65D500E42796 /* TabShadowView.swift in Sources */, 3706FBA7293F65D500E42796 /* EncryptedValueTransformer.swift in Sources */, - 4B41EDAF2B168AFF001EEDF4 /* VPNFeedbackFormViewController.swift in Sources */, + 4B41EDAF2B168AFF001EEDF4 /* UnifiedFeedbackFormViewController.swift in Sources */, 31EF1E802B63FFA800E6DB17 /* DBPHomeViewController.swift in Sources */, 3706FBA8293F65D500E42796 /* PasteboardBookmark.swift in Sources */, 3706FBA9293F65D500E42796 /* PinnedTabsManager.swift in Sources */, @@ -10345,6 +10397,8 @@ 9FBD84532BB3AACB00220859 /* AttributionOriginFileProvider.swift in Sources */, 4BF97AD92B43C5C000EB4240 /* Bundle+VPN.swift in Sources */, 3706FBAE293F65D500E42796 /* DataImport.swift in Sources */, + BDBA859A2C5D258100BC54F5 /* VPNFeedbackFormViewController.swift in Sources */, + BDBA859D2C5D25A300BC54F5 /* VPNFeedbackFormViewModel.swift in Sources */, 3706FBAF293F65D500E42796 /* FireproofDomains.xcdatamodeld in Sources */, B626A7552991413000053070 /* SerpHeadersNavigationResponder.swift in Sources */, B68D21C42ACBC917002DA3C2 /* ContentBlockingMock.swift in Sources */, @@ -10380,7 +10434,7 @@ 3706FBC8293F65D500E42796 /* FirePopoverCollectionViewItem.swift in Sources */, 3706FBC9293F65D500E42796 /* ArrayExtension.swift in Sources */, 3706FBCB293F65D500E42796 /* BookmarkHTMLImporter.swift in Sources */, - 4BF97ADC2B43C5E200EB4240 /* VPNFeedbackSender.swift in Sources */, + 4BF97ADC2B43C5E200EB4240 /* UnifiedFeedbackSender.swift in Sources */, 987799F72999996B005D8EB6 /* BookmarkDatabase.swift in Sources */, F1C5763F2BFF972900C78647 /* SubscriptionUIHandling.swift in Sources */, BBE013EB2C5BFD660025F2C6 /* BookmarksEmptyStateContent.swift in Sources */, @@ -10411,7 +10465,7 @@ 3706FBDB293F65D500E42796 /* DefaultBrowserPreferences.swift in Sources */, 9FEE98662B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 3706FBDC293F65D500E42796 /* Permissions.xcdatamodeld in Sources */, - 4B41EDB52B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift in Sources */, + 4B41EDB52B168C55001EEDF4 /* UnifiedFeedbackFormViewModel.swift in Sources */, 371209302C233D66003ADF3D /* RemoteMessagingDatabase.swift in Sources */, 3706FBDD293F65D500E42796 /* PaddedImageButton.swift in Sources */, 37CEFCAA2A6737A2001EF741 /* CredentialsCleanupErrorHandling.swift in Sources */, @@ -10437,6 +10491,7 @@ 3706FBE8293F65D500E42796 /* SuggestionTableCellView.swift in Sources */, 3706FBE9293F65D500E42796 /* FireViewModel.swift in Sources */, B68D21D02ACBC9FD002DA3C2 /* ContentBlockerRulesManagerMock.swift in Sources */, + BD7090D02C5182FB009EED82 /* UnifiedFeedbackFormView.swift in Sources */, 3706FEC6293F6F0600E42796 /* BWKeyStorage.swift in Sources */, 4B6785482AA8DE69008A5004 /* VPNUninstaller.swift in Sources */, 3706FBEC293F65D500E42796 /* EditableTextView.swift in Sources */, @@ -10460,6 +10515,7 @@ 379E877729E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */, 3706FBFB293F65D500E42796 /* MoreOrLessView.swift in Sources */, 987799FA29999973005D8EB6 /* LocalBookmarkStore.swift in Sources */, + BDBA85942C5D255200BC54F5 /* VPNFeedbackCategory.swift in Sources */, B602E7D02A93A5FF00F12201 /* WKBackForwardListExtension.swift in Sources */, 3706FBFE293F65D500E42796 /* History.xcdatamodeld in Sources */, B68D21C92ACBC96E002DA3C2 /* MockPrivacyConfiguration.swift in Sources */, @@ -10586,6 +10642,7 @@ 3706FC4A293F65D500E42796 /* LocalStatisticsStore.swift in Sources */, 3706FC4B293F65D500E42796 /* BackForwardListItem.swift in Sources */, 4B4D60DD2A0C875E00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, + BDBA85912C5D252A00BC54F5 /* VPNFeedbackSender.swift in Sources */, 31267C6A2B640C4B00FEF811 /* DataBrokerProtectionFeatureDisabler.swift in Sources */, 3707C723294B5D2900682A9F /* URLSessionExtension.swift in Sources */, 3706FC4E293F65D500E42796 /* AtbAndVariantCleanup.swift in Sources */, @@ -10641,7 +10698,6 @@ 3706FC6A293F65D500E42796 /* NSWorkspaceExtension.swift in Sources */, B6C0BB6829AEFF8100AE8E3C /* BookmarkExtension.swift in Sources */, B69A14FB2B4D705D00B9417D /* BookmarkFolderPicker.swift in Sources */, - 4B41EDB22B168B1E001EEDF4 /* VPNFeedbackFormView.swift in Sources */, 3706FC6C293F65D500E42796 /* BookmarkViewModel.swift in Sources */, 3706FC6D293F65D500E42796 /* DaxSpeech.swift in Sources */, 3706FC6E293F65D500E42796 /* DuckURLSchemeHandler.swift in Sources */, @@ -10673,7 +10729,6 @@ 371209312C233D69003ADF3D /* RemoteMessagingStoreErrorHandling.swift in Sources */, 3706FC83293F65D500E42796 /* PopoverMessageViewController.swift in Sources */, 7B5A23702C46A116007213AC /* ExcludedDomainsModel.swift in Sources */, - 4BF97ADA2B43C5DC00EB4240 /* VPNFeedbackCategory.swift in Sources */, 9D9AE86E2AA76D1F0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, 3707C721294B5D2900682A9F /* WKMenuItemIdentifier.swift in Sources */, 3706FEBE293F6EFF00E42796 /* BWMessageIdGenerator.swift in Sources */, @@ -10764,6 +10819,7 @@ 1D1C36E729FB019C001FA40C /* HistoryTabExtensionTests.swift in Sources */, 1D9FDEBB2B9B5E090040B78C /* WebTrackingProtectionPreferencesTests.swift in Sources */, 3706FDF3293F661700E42796 /* ChromiumLoginReaderTests.swift in Sources */, + BDCB66D92C7CE1A700E8ABC9 /* VPNFeedbackFormViewModelTests.swift in Sources */, 3706FDF4293F661700E42796 /* TabCollectionTests.swift in Sources */, 3706FDF5293F661700E42796 /* StartupPreferencesTests.swift in Sources */, 3706FDF6293F661700E42796 /* DuckPlayerTests.swift in Sources */, @@ -10813,7 +10869,7 @@ 1D9FDEC12B9B5FEA0040B78C /* AccessibilityPreferencesTests.swift in Sources */, 3706FE11293F661700E42796 /* URLSuggestedFilenameTests.swift in Sources */, 5681ED472BDBAF6E00F59729 /* SyncErrorHandlerTests.swift in Sources */, - 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */, + 4BE344EF2B23786F003FC223 /* UnifiedFeedbackFormViewModelTests.swift in Sources */, B6F56569299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, 3706FE13293F661700E42796 /* ConfigurationStorageTests.swift in Sources */, 3706FE15293F661700E42796 /* PrivacyIconViewModelTests.swift in Sources */, @@ -11407,7 +11463,7 @@ B693955126F04BEB0015B914 /* GradientView.swift in Sources */, 37AFCE8527DA2D3900471A10 /* PreferencesSidebar.swift in Sources */, B6C00ED5292FB21E009C73A6 /* HoveredLinkTabExtension.swift in Sources */, - 4B0526612B1D55320054955A /* VPNFeedbackSender.swift in Sources */, + 4B0526612B1D55320054955A /* UnifiedFeedbackSender.swift in Sources */, AA5C8F5E2590EEE800748EB7 /* NSPointExtension.swift in Sources */, 4BF0E5122AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, AA6EF9AD25066F42004754E6 /* WindowsManager.swift in Sources */, @@ -11435,7 +11491,7 @@ 4BB88B5025B7BA2B006F6B06 /* TabInstrumentation.swift in Sources */, 85D33F1225C82EB3002B91A6 /* ConfigurationManager.swift in Sources */, 31F28C4F28C8EEC500119F70 /* YoutubePlayerUserScript.swift in Sources */, - 4B41EDAE2B168AFF001EEDF4 /* VPNFeedbackFormViewController.swift in Sources */, + 4B41EDAE2B168AFF001EEDF4 /* UnifiedFeedbackFormViewController.swift in Sources */, 7BFF35732C10D75000F89673 /* IPCServiceLauncher.swift in Sources */, AA5FA697275F90C400DCE9C9 /* FaviconImageCache.swift in Sources */, 1430DFF524D0580F00B8978C /* TabBarViewController.swift in Sources */, @@ -11477,6 +11533,7 @@ 7BEC20452B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift in Sources */, B6C0BB6A29AF1C7000AE8E3C /* BrowserTabView.swift in Sources */, B6B1E88026D5DA9B0062C350 /* DownloadsViewController.swift in Sources */, + BD7090D62C540D5D009EED82 /* EmptyMetadataCollector.swift in Sources */, 85AC3AF725D5DBFD00C7D2AA /* DataExtension.swift in Sources */, 85480FCF25D1AA22009424E3 /* ConfigurationStore.swift in Sources */, AA3D531B27A2F57E00074EC1 /* Feedback.swift in Sources */, @@ -11494,6 +11551,7 @@ EEC4A6712B2C90AB00F7C0AA /* VPNLocationPreferenceItem.swift in Sources */, 85589E8727BBB8F20038AD11 /* HomePageFavoritesModel.swift in Sources */, 4BB88B4A25B7B690006F6B06 /* SequenceExtensions.swift in Sources */, + BDBA85932C5D255200BC54F5 /* VPNFeedbackCategory.swift in Sources */, B602E7CF2A93A5FF00F12201 /* WKBackForwardListExtension.swift in Sources */, 4B59024026B35F3600489384 /* ChromiumDataImporter.swift in Sources */, B62B48562ADE730D000DECE5 /* FileImportView.swift in Sources */, @@ -11619,6 +11677,7 @@ 4BB99CFE26FE191E001E4761 /* FirefoxBookmarksReader.swift in Sources */, F1DA518C2BF607D200CF29FA /* SubscriptionRedirectManager.swift in Sources */, 4BBC16A227C485BC00E00A38 /* DeviceIdleStateDetector.swift in Sources */, + BDBA859C2C5D25A300BC54F5 /* VPNFeedbackFormViewModel.swift in Sources */, 4B379C2427BDE1B0008A968E /* FlatButton.swift in Sources */, 37054FC92873301700033B6F /* PinnedTabView.swift in Sources */, 4BA1A6A0258B079600F6F690 /* DataEncryption.swift in Sources */, @@ -11695,8 +11754,8 @@ 4B9DB0352A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */, AAB8203C26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift in Sources */, 4B6785472AA8DE68008A5004 /* VPNUninstaller.swift in Sources */, - 4B0526642B1D55D80054955A /* VPNFeedbackCategory.swift in Sources */, 4B9292D42667123700AD2C21 /* BookmarkListViewController.swift in Sources */, + BD88A83E2C4F3E4300460A26 /* FeedbackCategoryProviding.swift in Sources */, 9F56CFB12B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, F1FD5B672C2B0AAA0040FA0D /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */, 4B723E0D26B0006100E14D75 /* SecureVaultLoginImporter.swift in Sources */, @@ -11704,6 +11763,7 @@ 9D9AE86B2AA76CF90026E7DC /* LoginItemsManager.swift in Sources */, 857E5AF52A79045800FC0FB4 /* PixelExperiment.swift in Sources */, B6C416A7294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift in Sources */, + BDBA859F2C5D25B700BC54F5 /* VPNMetadataCollector.swift in Sources */, AA5C1DD5285C780C0089850C /* RecentlyClosedCoordinator.swift in Sources */, AA88D14B252A557100980B4E /* URLRequestExtension.swift in Sources */, AA6197C6276B3168008396F0 /* FaviconHostReference.swift in Sources */, @@ -11812,6 +11872,8 @@ 9F872D982B8DA9F800138637 /* Bookmarks+Tab.swift in Sources */, 85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */, 983DFB2528B67036006B7E34 /* UserContentUpdating.swift in Sources */, + BD7090D22C52ECFE009EED82 /* UnifiedMetadataCollector.swift in Sources */, + BD7090CF2C5182FB009EED82 /* UnifiedFeedbackFormView.swift in Sources */, 1D9A4E5A2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, 316913262BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */, 4B7A57CF279A4EF300B1C70E /* ChromiumPreferences.swift in Sources */, @@ -11828,7 +11890,6 @@ F188267C2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */, 4B723E1026B0006700E14D75 /* CSVImporter.swift in Sources */, 37A4CEBA282E992F00D75B89 /* StartupPreferences.swift in Sources */, - 4B41EDB12B168B1E001EEDF4 /* VPNFeedbackFormView.swift in Sources */, 1D72D59C2BFF61B200AEDE36 /* UpdateNotificationPresenter.swift in Sources */, AA4BBA3B25C58FA200C4FB0F /* MainMenu.swift in Sources */, AA585D84248FD31100E9A3E2 /* BrowserTabViewController.swift in Sources */, @@ -11905,7 +11966,6 @@ 9F9C49FD2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */, 4B9292A226670D2A00AD2C21 /* PseudoFolder.swift in Sources */, 1DDD3EC42B84F96B004CBF2B /* CookiePopupProtectionPreferences.swift in Sources */, - 4B05265E2B1AE5C70054955A /* VPNMetadataCollector.swift in Sources */, 4B9DB0292A983B24000927DB /* WaitlistStorage.swift in Sources */, AA2CB1352587C29500AA6FBE /* TabBarFooter.swift in Sources */, EEC111E6294D06290086524F /* JSAlertViewModel.swift in Sources */, @@ -11941,7 +12001,7 @@ 85C48CCC278D808F00D3263E /* NSAttributedStringExtension.swift in Sources */, 7B5A236F2C46A116007213AC /* ExcludedDomainsModel.swift in Sources */, 1D710F4B2C48F1F200C3975F /* UpdateDialogHelper.swift in Sources */, - 4B41EDB42B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift in Sources */, + 4B41EDB42B168C55001EEDF4 /* UnifiedFeedbackFormViewModel.swift in Sources */, AA7EB6E527E7D6DC00036718 /* AnimationView.swift in Sources */, 8562599A269CA0A600EE44BC /* NSRectExtension.swift in Sources */, 4B37EE5F2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift in Sources */, @@ -11980,6 +12040,7 @@ 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, 859F30642A72A7BB00C20372 /* BookmarksBarPromptPopover.swift in Sources */, 7B6545ED2C0778BB00115BEA /* VPNControllerUDSClient+ConvenienceInitializers.swift in Sources */, + BDBA85902C5D252A00BC54F5 /* VPNFeedbackSender.swift in Sources */, B693955426F04BEC0015B914 /* ColorView.swift in Sources */, AA5C1DD3285A217F0089850C /* RecentlyClosedCacheItem.swift in Sources */, B6BBF17427475B15004F850E /* PopupBlockedPopover.swift in Sources */, @@ -11996,6 +12057,7 @@ B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */, 85378DA0274E6F42007C5CBF /* NSNotificationName+EmailManager.swift in Sources */, 1DDC84FF2B835BC000670238 /* SearchPreferences.swift in Sources */, + BDBA85992C5D258100BC54F5 /* VPNFeedbackFormViewController.swift in Sources */, B693955726F04BEC0015B914 /* MouseOverButton.swift in Sources */, AA61C0D02722159B00E6B681 /* FireInfoViewController.swift in Sources */, 9D9AE8692AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, @@ -12164,6 +12226,7 @@ 9F514F912B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 9FA173E32B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, 37534CA8281198CD002621E7 /* AdjacentItemEnumerator.swift in Sources */, + BDBA85962C5D256C00BC54F5 /* VPNFeedbackFormView.swift in Sources */, 987799F62999996B005D8EB6 /* BookmarkDatabase.swift in Sources */, 7BB4BC6A2C5CD96200E06FC8 /* ActiveDomainPublisher.swift in Sources */, 4BE53374286E39F10019DBFD /* ChromiumKeychainPrompt.swift in Sources */, @@ -12301,7 +12364,7 @@ B6A5A2A025B96E8300AA7ADA /* AppStateChangePublisherTests.swift in Sources */, B63ED0E326B3E7FA00A9DAD1 /* CLLocationManagerMock.swift in Sources */, 37CD54BB27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift in Sources */, - 4BE344EE2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */, + 4BE344EE2B2376DF003FC223 /* UnifiedFeedbackFormViewModelTests.swift in Sources */, 9F872D9D2B9058D000138637 /* Bookmarks+TabTests.swift in Sources */, BBFF355D2C4AF26200DA3289 /* BookmarksSortModeTests.swift in Sources */, 9F3910622B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */, @@ -12331,6 +12394,7 @@ 4B98D27A28D95F1A003C2B6F /* ChromiumFaviconsReaderTests.swift in Sources */, 9F0660732BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, 56A054302C2043C8007D8FAB /* OnboardingTabExtensionTests.swift in Sources */, + BDCB66D82C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift in Sources */, 986189E62A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift in Sources */, AA652CD325DDA6E9009059CC /* LocalBookmarkManagerTests.swift in Sources */, 1D232E992C7870D90043840D /* BinaryOwnershipCheckerTests.swift in Sources */, @@ -13473,7 +13537,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 186.0.0; + version = 186.1.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 151e8e8b96..2b8ba57fc4 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "4a55217003ad7b2d44a1ac616d47596c0bda69dc", - "version" : "186.0.0" + "revision" : "606ccf9e86f5cad3ae83132f46241357feecf152", + "version" : "186.1.0" } }, { diff --git a/DuckDuckGo/Assets.xcassets/Images/PPro-Feedback.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/PPro-Feedback.imageset/Contents.json new file mode 100644 index 0000000000..b0631f4c9b --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/PPro-Feedback.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "Privacy-Pro-16D.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/PPro-Feedback.imageset/Privacy-Pro-16D.svg b/DuckDuckGo/Assets.xcassets/Images/PPro-Feedback.imageset/Privacy-Pro-16D.svg new file mode 100644 index 0000000000..c64970b038 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/PPro-Feedback.imageset/Privacy-Pro-16D.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift index 26fabdff43..bec5c7538e 100644 --- a/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift +++ b/DuckDuckGo/Common/Localizables/UserText+NetworkProtection.swift @@ -42,8 +42,8 @@ extension UserText { static let networkProtectionInviteSuccessMessage = "DuckDuckGo's VPN secures all of your device's Internet traffic anytime, anywhere." // MARK: - Navigation Bar Status View - // "network.protection.navbar.status.view.share.feedback" - Menu item for 'Share VPN Feedback' in the VPN status view that's shown in the navigation bar - static let networkProtectionNavBarStatusViewShareFeedback = "Share VPN Feedback…" + // "network.protection.navbar.status.view.send.feedback" - Menu item for 'Send Feedback' in the VPN status view that's shown in the navigation bar + static let networkProtectionNavBarStatusViewSendFeedback = "Send Feedback…" // "network.protection.status.menu.vpn.settings" - The status menu 'VPN Settings' menu item static let networkProtectionNavBarStatusMenuVPNSettings = "VPN Settings…" // "network.protection.status.menu.faq" - The status menu 'FAQ' menu item @@ -73,6 +73,24 @@ extension UserText { extension UserText { // MARK: - Feedback Form + // "feedback-form.title" - Title for each screen of the feedback form + static let feedbackFormTitle = "Help Improve Privacy Pro" + // "general.feedback-form.category.select-feature" - Title for the feature selection state of the general feedback form + static let generalFeedbackFormCategorySelect = "Select a category" + // "general.feedback-form.category.ppro" - Description for the feedback form when the issue is related to subscription and payments + static let generalFeedbackFormCategoryPPro = "Subscription and Payments" + // "general.feedback-form.category.vpn" - Description for the feedback form when the issue is related to VPN + static let generalFeedbackFormCategoryVPN = "VPN" + // "general.feedback-form.category.pir" - Description for the feedback form when the issue is related to Personal Info Removal (PIR) + static let generalFeedbackFormCategoryPIR = "Personal Info Removal" + // "general.feedback-form.category.itr" - Description for the feedback form when the issue is related to Identity Theft Restoration (ITR) + static let generalFeedbackFormCategoryITR = "Identity Theft Restoration" + // "ppro.feedback-form.category.select-category" - Title for the category selection state of the feedback form + static let pproFeedbackFormCategorySelect = "Select a category" + // "ppro.feedback-form.category.otp" - Description for the feedback form when there is an issue with the one-time password + static let pproFeedbackFormCategoryOTP = "Issue with one-time password" + // "ppro.feedback-form.category.something-else" - Description for the feedback form when the user has an issue not categorized in other options + static let pproFeedbackFormCategoryOther = "Something else" // "vpn.feedback-form.title" - Title for each screen of the VPN feedback form static let vpnFeedbackFormTitle = "Help Improve the DuckDuckGo VPN" // "vpn.feedback-form.category.select-category" - Title for the category selection state of the VPN feedback form @@ -93,7 +111,63 @@ extension UserText { static let vpnFeedbackFormCategoryFeatureRequest = "VPN feature request" // "vpn.feedback-form.category.other" - Title for the 'other VPN feedback' category of the VPN feedback form static let vpnFeedbackFormCategoryOther = "Other VPN feedback" + // "pir.feedback-form.category.select-category" - Title for the category selection state of the PIR feedback form + static let pirFeedbackFormCategorySelect = "Select a category" + // "pir.feedback-form.category.no-info-on-specific-site" - Description for the feedback form when the scan didn't find user's info on a specific site + static let pirFeedbackFormCategoryNothingOnSpecificSite = "The scan didn't find my info on a specific site" + // "pir.feedback-form.category.not-me" - Description for the feedback form when the scan found records that don’t belong to the user + static let pirFeedbackFormCategoryNotMe = "The scan found records which aren't me" + // "pir.feedback-form.category.scan-stuck" - Description for the feedback form when the scan process is stuck + static let pirFeedbackFormCategoryScanStuck = "The scan for records is stuck" + // "pir.feedback-form.category.removal-stuck" - Description for the feedback form when the removal process is stuck + static let pirFeedbackFormCategoryRemovalStuck = "The removal process is stuck" + // "itr.feedback-form.category.select-category" - Title for the category selection state of the ITR feedback form + static let itrFeedbackFormCategorySelect = "Select a category" + // "itr.feedback-form.category.access-code" - Description for the feedback form when there is an issue with the access code + static let itrFeedbackFormCategoryAccessCode = "Issue with access code" + // "itr.feedback-form.category.contact-advisor" - Description for the feedback form when the user is unable to contact an advisor + static let itrFeedbackFormCategoryCantContactAdvisor = "Unable to contact advisor" + // "itr.feedback-form.category.unhelpful" - Description for the feedback form when the call to an advisor was unhelpful + static let itrFeedbackFormCategoryUnhelpful = "Call to Advisor was unhelpful" + // "itr.feedback-form.category.something-else" - Description for the feedback form when the user has an issue not categorized in other options + static let itrFeedbackFormCategorySomethingElse = "Something else" + // "ppro.feedback-form.text-1" - Text for the body of the PPro feedback form + static let pproFeedbackFormText1 = "Found an issue not cover in our [help center](duck://)? We definitely want to know about it.\n\nTell us what's going on:" + // "ppro.feedback-form.text-2" - Text for the body of the PPro feedback form + static let pproFeedbackFormText2 = "In addition to the details entered above, we send some anonymized info with your feedback:" + // "ppro.feedback-form.text-3" - Bullet text for the body of the PPro feedback form + static let pproFeedbackFormText3 = "• Whether specific browser features are active" + // "ppro.feedback-form.text-4" - Bullet text for the body of the PPro feedback form + static let pproFeedbackFormText4 = "• Aggregate app diagnostics (e.g., error codes)" + // "ppro.feedback-form.text-5" - Text for the body of the PPro feedback form + static let pproFeedbackFormText5 = "By clicking \"Submit\" you agree that DuckDuckGo may use information submitted to improve the app." + // "ppro.feedback-form.disclaimer" - Text for the disclaimer of the PPro feedback form + static let pproFeedbackFormDisclaimer = "Reports are anonymous and sent to DuckDuckGo to help improve our service" + + // "ppro.feedback-form.sending-confirmation.title" - Title for the feedback sent view title of the feedback form + static let pproFeedbackFormSendingConfirmationTitle = "Thank you!" + // "ppro.feedback-form.sending-confirmation.description" - Title for the feedback sent view description of the feedback form + static let pproFeedbackFormSendingConfirmationDescription = "Your Feedback will help us improve Privacy Pro." + // "ppro.feedback-form.sending-confirmation.error" - Title for the feedback sending error text of the feedback form + static let pproFeedbackFormSendingConfirmationError = "We couldn't send your feedback right now, please try again." + + // "ppro.feedback-form.button.done" - Title for the Done button of the PPro feedback form + static let pproFeedbackFormButtonDone = "Done" + // "ppro.feedback-form.button.cancel" - Title for the Cancel button of the PPro feedback form + static let pproFeedbackFormButtonCancel = "Cancel" + // "ppro.feedback-form.button.submit" - Title for the Submit button of the PPro feedback form + static let pproFeedbackFormButtonSubmit = "Submit" + // "ppro.feedback-form.button.submitting" - Title for the Submitting state of the PPro feedback form + static let pproFeedbackFormButtonSubmitting = "Submitting…" + + // "ppro.feedback-form.general-feedback.placeholder" - Placeholder for the General Feedback step in the Privacy Pro feedback form + static let pproFeedbackFormGeneralFeedbackPlaceholder = "Please give us your feedback:" + // "ppro.feedback-form.request-feature.placeholder" - Placeholder for the Feature Request step in the Privacy Pro feedback form + static let pproFeedbackFormRequestFeaturePlaceholder = "What feature would you like to see?" + + // "pir.feedback-form.category.other" - Description for the feedback form when the user has an issue not categorized in other options + static let pirFeedbackFormCategoryOther = "Something else" // "vpn.feedback-form.text-1" - Text for the body of the VPN feedback form static let vpnFeedbackFormText1 = "Please describe what's happening, what you expected to happen, and the steps that led to the issue:" // "vpn.feedback-form.text-2" - Text for the body of the VPN feedback form diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index a922574c2f..b2dfd2c47c 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -492,6 +492,7 @@ struct UserText { static let bookmarkImportedFromFolder = NSLocalizedString("bookmarks.imported.from.folder", value: "Imported from", comment: "Name of the folder the imported bookmarks are saved into") // MARK: Feedback + static let sendPProFeedback = NSLocalizedString("send.ppro.feedback", value: "Send Privacy Pro Feedback", comment: "Menu with feedback commands") static let reportBrokenSite = NSLocalizedString("report.broken.site", value: "Report Broken Site", comment: "Menu with feedback commands") static let browserFeedback = NSLocalizedString("send.browser.feedback", value: "Send Browser Feedback", comment: "Menu with feedback commands") static let browserFeedbackTitle = NSLocalizedString("send.browser.feedback.title", value: "Help Improve the DuckDuckGo Browser", comment: "Title of the interface to send feedback on the browser") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 90c59a3d9d..ccd1671697 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -51960,6 +51960,18 @@ } } }, + "send.ppro.feedback" : { + "comment" : "Menu with feedback commands", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Send Privacy Pro Feedback" + } + } + } + }, "settings" : { "comment" : "Menu item for opening settings", "extractionState" : "extracted_with_value", @@ -58457,4 +58469,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 824d544a71..8204d364dd 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -190,6 +190,11 @@ extension AppDelegate { } } + @MainActor + @objc func openPProFeedback(_ sender: Any?) { + WindowControllersManager.shared.showShareFeedbackModal(source: .settings) + } + #endif @objc func navigateToBookmark(_ sender: Any?) { diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 085e84e1fa..2daca8b0b9 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -108,7 +108,10 @@ final class MoreOptionsMenu: NSMenu { let feedbackMenuItem = NSMenuItem(title: feedbackString, action: nil, keyEquivalent: "") .withImage(.sendFeedback) - feedbackMenuItem.submenu = FeedbackSubMenu(targetting: self, tabCollectionViewModel: tabCollectionViewModel) + feedbackMenuItem.submenu = FeedbackSubMenu(targetting: self, + tabCollectionViewModel: tabCollectionViewModel, + subscriptionFeatureAvailability: subscriptionFeatureAvailability, + accountManager: accountManager) addItem(feedbackMenuItem) addItem(NSMenuItem.separator()) @@ -488,8 +491,15 @@ final class EmailOptionsButtonSubMenu: NSMenu { @MainActor final class FeedbackSubMenu: NSMenu { + private let subscriptionFeatureAvailability: SubscriptionFeatureAvailability + private let accountManager: AccountManager - init(targetting target: AnyObject, tabCollectionViewModel: TabCollectionViewModel) { + init(targetting target: AnyObject, + tabCollectionViewModel: TabCollectionViewModel, + subscriptionFeatureAvailability: SubscriptionFeatureAvailability, + accountManager: AccountManager) { + self.subscriptionFeatureAvailability = subscriptionFeatureAvailability + self.accountManager = accountManager super.init(title: UserText.sendFeedback) updateMenuItems(with: tabCollectionViewModel, targetting: target) } @@ -512,6 +522,16 @@ final class FeedbackSubMenu: NSMenu { keyEquivalent: "") .withImage(.siteBreakage) addItem(reportBrokenSiteItem) + + if subscriptionFeatureAvailability.usesUnifiedFeedbackForm, accountManager.isUserAuthenticated { + addItem(.separator()) + + let sendPProFeedbackItem = NSMenuItem(title: UserText.sendPProFeedback, + action: #selector(AppDelegate.openPProFeedback(_:)), + keyEquivalent: "") + .withImage(.pProFeedback) + addItem(sendPProFeedbackItem) + } } } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index ed07f0746a..6cf1768160 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -95,6 +95,7 @@ final class NavigationBarViewController: NSViewController { private var selectedTabViewModelCancellable: AnyCancellable? private var credentialsToSaveCancellable: AnyCancellable? private var vpnToggleCancellable: AnyCancellable? + private var feedbackFormCancellable: AnyCancellable? private var passwordManagerNotificationCancellable: AnyCancellable? private var pinnedViewsNotificationCancellable: AnyCancellable? private var navigationButtonsCancellables = Set() @@ -151,6 +152,7 @@ final class NavigationBarViewController: NSViewController { listenToPasswordManagerNotifications() listenToPinningManagerNotifications() listenToMessageNotifications() + listenToFeedbackFormNotifications() subscribeToDownloads() addContextMenu() @@ -404,6 +406,12 @@ final class NavigationBarViewController: NSViewController { .store(in: &cancellables) } + func listenToFeedbackFormNotifications() { + feedbackFormCancellable = NotificationCenter.default.publisher(for: .OpenUnifiedFeedbackForm).receive(on: DispatchQueue.main).sink { _ in + WindowControllersManager.shared.showShareFeedbackModal(source: .ppro) + } + } + @objc private func showVPNUninstalledFeedback() { // Only show the popover if we aren't already presenting one: guard view.window?.isKeyWindow == true, (self.presentedViewControllers ?? []).isEmpty else { return } @@ -1169,4 +1177,5 @@ extension NavigationBarViewController { extension Notification.Name { static let ToggleNetworkProtectionInMainWindow = Notification.Name("com.duckduckgo.vpn.toggle-popover-in-main-window") + static let OpenUnifiedFeedbackForm = Notification.Name("com.duckduckgo.subscription.open-unified-feedback-form") } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift index 6573e3a550..531fbea107 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionNavBarPopoverManager.swift @@ -128,7 +128,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) }), NetworkProtectionStatusView.Model.MenuItem( - name: UserText.networkProtectionNavBarStatusViewShareFeedback, + name: UserText.networkProtectionNavBarStatusViewSendFeedback, action: { try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) }) @@ -140,7 +140,7 @@ final class NetworkProtectionNavBarPopoverManager: NetPPopoverManager { try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) }), NetworkProtectionStatusView.Model.MenuItem( - name: UserText.networkProtectionNavBarStatusViewShareFeedback, + name: UserText.networkProtectionNavBarStatusViewSendFeedback, action: { try? await appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) }) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift index 54368bc09e..3ca5bb8fef 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNURLEventHandler.swift @@ -63,7 +63,7 @@ final class VPNURLEventHandler { } func showShareFeedback() { - windowControllerManager.showShareFeedbackModal() + windowControllerManager.showShareFeedbackModal(source: .vpn) } func showMainWindow() { diff --git a/DuckDuckGo/Preferences/View/PreferencesRootView.swift b/DuckDuckGo/Preferences/View/PreferencesRootView.swift index ab2a539db6..b559d4a6da 100644 --- a/DuckDuckGo/Preferences/View/PreferencesRootView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesRootView.swift @@ -104,7 +104,8 @@ enum Preferences { case .vpn: VPNView(model: VPNPreferencesModel(), status: model.vpnProtectionStatus()) case .subscription: - SubscriptionUI.PreferencesSubscriptionView(model: subscriptionModel!) + SubscriptionUI.PreferencesSubscriptionView(model: subscriptionModel!, + subscriptionFeatureAvailability: DefaultSubscriptionFeatureAvailability()) case .autofill: AutofillView(model: AutofillPreferencesModel()) case .accessibility: @@ -136,6 +137,8 @@ enum Preferences { case .openVPN: PixelKit.fire(PrivacyProPixel.privacyProVPNSettings) NotificationCenter.default.post(name: .ToggleNetworkProtectionInMainWindow, object: self, userInfo: nil) + case .openFeedback: + NotificationCenter.default.post(name: .OpenUnifiedFeedbackForm, object: self, userInfo: nil) case .openDB: PixelKit.fire(PrivacyProPixel.privacyProPersonalInformationRemovalSettings) WindowControllersManager.shared.showTab(with: .dataBrokerProtection) diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 0fd4143a3a..78b911cb8b 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -140,6 +140,15 @@ enum GeneralPixel: PixelKitEventV2 { // VPN case vpnBreakageReport(category: String, description: String, metadata: String) + // Unified Feedback + case pproFeedbackFeatureRequest(description: String, source: String) + case pproFeedbackGeneralFeedback(description: String, source: String) + case pproFeedbackReportIssue(source: String, category: String, subcategory: String, description: String, metadata: String) + + case pproFeedbackFormShow + case pproFeedbackSubmitScreenShow(source: String, reportType: String, category: String, subcategory: String) + case pproFeedbackSubmitScreenFAQClick(source: String, reportType: String, category: String, subcategory: String) + case networkProtectionEnabledOnSearch case networkProtectionGeoswitchingOpened case networkProtectionGeoswitchingSetNearest @@ -625,6 +634,19 @@ enum GeneralPixel: PixelKitEventV2 { case .vpnBreakageReport: return "m_mac_vpn_breakage_report" + case .pproFeedbackFeatureRequest: + return "m_mac_ppro_feedback_feature-request" + case .pproFeedbackGeneralFeedback: + return "m_mac_ppro_feedback_general-feedback" + case .pproFeedbackReportIssue: + return "m_mac_ppro_feedback_report-issue" + case .pproFeedbackFormShow: + return "m_mac_ppro_feedback_general-screen_show" + case .pproFeedbackSubmitScreenShow: + return "m_mac_ppro_feedback_submit-screen_show" + case .pproFeedbackSubmitScreenFAQClick: + return "m_mac_ppro_feedback_submit-screen-faq_click" + case .networkProtectionEnabledOnSearch: return "m_mac_netp_ev_enabled_on_search" @@ -1078,6 +1100,39 @@ enum GeneralPixel: PixelKitEventV2 { PixelKit.Parameters.vpnBreakageMetadata: metadata ] + case .pproFeedbackFeatureRequest(let description, let source): + return [ + PixelKit.Parameters.pproIssueDescription: description, + PixelKit.Parameters.pproIssueSource: source, + ] + case .pproFeedbackGeneralFeedback(let description, let source): + return [ + PixelKit.Parameters.pproIssueDescription: description, + PixelKit.Parameters.pproIssueSource: source, + ] + case .pproFeedbackReportIssue(let source, let category, let subcategory, let description, let metadata): + return [ + PixelKit.Parameters.pproIssueSource: source, + PixelKit.Parameters.pproIssueCategory: category, + PixelKit.Parameters.pproIssueSubcategory: subcategory, + PixelKit.Parameters.pproIssueDescription: description, + PixelKit.Parameters.pproIssueMetadata: metadata, + ] + case .pproFeedbackSubmitScreenShow(let source, let reportType, let category, let subcategory): + return [ + PixelKit.Parameters.pproIssueSource: source, + PixelKit.Parameters.pproIssueReportType: reportType, + PixelKit.Parameters.pproIssueCategory: category, + PixelKit.Parameters.pproIssueSubcategory: subcategory, + ] + case .pproFeedbackSubmitScreenFAQClick(let source, let reportType, let category, let subcategory): + return [ + PixelKit.Parameters.pproIssueSource: source, + PixelKit.Parameters.pproIssueReportType: reportType, + PixelKit.Parameters.pproIssueCategory: category, + PixelKit.Parameters.pproIssueSubcategory: subcategory, + ] + case .onboardingCohortAssigned(let cohort): return [PixelKit.Parameters.experimentCohort: cohort] case .onboardingHomeButtonEnabled(let cohort): diff --git a/DuckDuckGo/UnifiedFeedbackForm/FeedbackCategoryProviding.swift b/DuckDuckGo/UnifiedFeedbackForm/FeedbackCategoryProviding.swift new file mode 100644 index 0000000000..da53250652 --- /dev/null +++ b/DuckDuckGo/UnifiedFeedbackForm/FeedbackCategoryProviding.swift @@ -0,0 +1,198 @@ +// +// FeedbackCategoryProviding.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol FeedbackCategoryProviding: Hashable, CaseIterable, Identifiable, RawRepresentable { + static var prompt: Self { get } + var displayName: String { get } +} + +protocol FeedbackFAQProviding { + var url: URL? { get } +} + +extension FeedbackCategoryProviding where RawValue == String { + var id: String { + rawValue + } +} + +enum UnifiedFeedbackReportType: String, FeedbackCategoryProviding { + case selectReportType + case reportIssue + case requestFeature + case general + + static var prompt = UnifiedFeedbackReportType.selectReportType + + var displayName: String { + switch self { + case .selectReportType: return UserText.browserFeedbackSelectCategory + case .reportIssue: return UserText.browserFeedbackReportProblem + case .requestFeature: return UserText.browserFeedbackRequestFeature + case .general: return UserText.browserFeedbackGeneralFeedback + } + } +} + +enum UnifiedFeedbackCategory: String, FeedbackCategoryProviding { + case selectFeature + case subscription + case vpn + case pir + case itr + + static var prompt = UnifiedFeedbackCategory.selectFeature + + var displayName: String { + switch self { + case .selectFeature: return UserText.generalFeedbackFormCategorySelect + case .subscription: return UserText.generalFeedbackFormCategoryPPro + case .vpn: return UserText.generalFeedbackFormCategoryVPN + case .pir: return UserText.generalFeedbackFormCategoryPIR + case .itr: return UserText.generalFeedbackFormCategoryITR + } + } +} + +enum PrivacyProFeedbackSubcategory: String, FeedbackCategoryProviding, FeedbackFAQProviding { + case selectSubcategory + case otp + case somethingElse + + static var prompt = PrivacyProFeedbackSubcategory.selectSubcategory + + var displayName: String { + switch self { + case .selectSubcategory: return UserText.pproFeedbackFormCategorySelect + case .otp: return UserText.pproFeedbackFormCategoryOTP + case .somethingElse: return UserText.pproFeedbackFormCategoryOther + } + } + + var url: URL? { + switch self { + case .selectSubcategory: return nil + case .otp: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/payments/")! + case .somethingElse: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/payments/")! + } + } +} + +enum VPNFeedbackSubcategory: String, FeedbackCategoryProviding, FeedbackFAQProviding { + case selectSubcategory + case unableToInstall + case failsToConnect + case tooSlow + case issueWithAppOrWebsite + case appCrashesOrFreezes + case cantConnectToLocalDevice + case somethingElse + + static var prompt = VPNFeedbackSubcategory.selectSubcategory + + var displayName: String { + switch self { + case .selectSubcategory: return UserText.vpnFeedbackFormCategorySelect + case .unableToInstall: return UserText.vpnFeedbackFormCategoryUnableToInstall + case .failsToConnect: return UserText.vpnFeedbackFormCategoryFailsToConnect + case .tooSlow: return UserText.vpnFeedbackFormCategoryTooSlow + case .issueWithAppOrWebsite: return UserText.vpnFeedbackFormCategoryIssuesWithApps + case .appCrashesOrFreezes: return UserText.vpnFeedbackFormCategoryBrowserCrashOrFreeze + case .cantConnectToLocalDevice: return UserText.vpnFeedbackFormCategoryLocalDeviceConnectivity + case .somethingElse: return UserText.vpnFeedbackFormCategoryOther + } + } + + var url: URL? { + switch self { + case .selectSubcategory: return nil + case .unableToInstall: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/troubleshooting/")! + case .failsToConnect: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/troubleshooting/")! + case .tooSlow: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/troubleshooting/")! + case .issueWithAppOrWebsite: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/troubleshooting/")! + case .appCrashesOrFreezes: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/troubleshooting/")! + case .cantConnectToLocalDevice: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/troubleshooting/")! + case .somethingElse: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/vpn/")! + } + } +} + +enum PIRFeedbackSubcategory: String, FeedbackCategoryProviding, FeedbackFAQProviding { + case selectSubcategory + case nothingOnSpecificSite + case notMe + case scanStuck + case removalStuck + case somethingElse + + static var prompt = PIRFeedbackSubcategory.selectSubcategory + + var displayName: String { + switch self { + case .selectSubcategory: return UserText.pirFeedbackFormCategorySelect + case .nothingOnSpecificSite: return UserText.pirFeedbackFormCategoryNothingOnSpecificSite + case .notMe: return UserText.pirFeedbackFormCategoryNotMe + case .scanStuck: return UserText.pirFeedbackFormCategoryScanStuck + case .removalStuck: return UserText.pirFeedbackFormCategoryRemovalStuck + case .somethingElse: return UserText.pirFeedbackFormCategoryOther + } + } + + var url: URL? { + switch self { + case .selectSubcategory: return nil + case .nothingOnSpecificSite: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/personal-information-removal/removal-process/")! + case .notMe: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/personal-information-removal/removal-process/")! + case .scanStuck: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/personal-information-removal/removal-process/")! + case .removalStuck: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/personal-information-removal/removal-process/")! + case .somethingElse: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/personal-information-removal/")! + } + } +} + +enum ITRFeedbackSubcategory: String, FeedbackCategoryProviding, FeedbackFAQProviding { + case selectSubcategory + case accessCode + case cantContactAdvisor + case advisorUnhelpful + case somethingElse + + static var prompt = ITRFeedbackSubcategory.selectSubcategory + + var displayName: String { + switch self { + case .selectSubcategory: return UserText.itrFeedbackFormCategorySelect + case .accessCode: return UserText.itrFeedbackFormCategoryAccessCode + case .cantContactAdvisor: return UserText.itrFeedbackFormCategoryCantContactAdvisor + case .advisorUnhelpful: return UserText.itrFeedbackFormCategoryUnhelpful + case .somethingElse: return UserText.itrFeedbackFormCategorySomethingElse + } + } + + var url: URL? { + switch self { + case .selectSubcategory: return nil + case .accessCode: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/identity-theft-restoration/")! + case .cantContactAdvisor: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/identity-theft-restoration/iris/")! + case .advisorUnhelpful: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/identity-theft-restoration/")! + case .somethingElse: return URL(string: "https://duckduckgo.com/duckduckgo-help-pages/privacy-pro/identity-theft-restoration/")! + } + } +} diff --git a/DuckDuckGo/UnifiedFeedbackForm/MetadataCollectors/EmptyMetadataCollector.swift b/DuckDuckGo/UnifiedFeedbackForm/MetadataCollectors/EmptyMetadataCollector.swift new file mode 100644 index 0000000000..b8af990ffa --- /dev/null +++ b/DuckDuckGo/UnifiedFeedbackForm/MetadataCollectors/EmptyMetadataCollector.swift @@ -0,0 +1,32 @@ +// +// EmptyMetadataCollector.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct EmptyFeedbackMetadata: UnifiedFeedbackMetadata { + +} + +/// Default implementation for Privacy Pro metadata collector +/// Intentionally left blank as we currently don't collect any metadata for PIR and ITR +/// See `DefaultVPNMetadataCollector` for a reference implementation +final class EmptyMetadataCollector: UnifiedMetadataCollector { + func collectMetadata() async -> EmptyFeedbackMetadata { + EmptyFeedbackMetadata() + } +} diff --git a/DuckDuckGo/UnifiedFeedbackForm/MetadataCollectors/UnifiedMetadataCollector.swift b/DuckDuckGo/UnifiedFeedbackForm/MetadataCollectors/UnifiedMetadataCollector.swift new file mode 100644 index 0000000000..a720ac8bdc --- /dev/null +++ b/DuckDuckGo/UnifiedFeedbackForm/MetadataCollectors/UnifiedMetadataCollector.swift @@ -0,0 +1,42 @@ +// +// UnifiedMetadataCollector.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol UnifiedMetadataCollector { + associatedtype Metadata: UnifiedFeedbackMetadata + + func collectMetadata() async -> Metadata +} + +protocol UnifiedFeedbackMetadata: Encodable { + func toBase64() -> String +} + +extension UnifiedFeedbackMetadata { + func toBase64() -> String { + let encoder = JSONEncoder() + + do { + let encodedMetadata = try encoder.encode(self) + return encodedMetadata.base64EncodedString() + } catch { + return "Failed to encode metadata to JSON, error message: \(error.localizedDescription)" + } + } +} diff --git a/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormView.swift b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormView.swift new file mode 100644 index 0000000000..011f00efc9 --- /dev/null +++ b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormView.swift @@ -0,0 +1,310 @@ +// +// UnifiedFeedbackFormView.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct UnifiedFeedbackFormView: View { + + @EnvironmentObject var viewModel: UnifiedFeedbackFormViewModel + + var body: some View { + VStack(spacing: 0) { + Group { + Text(UserText.feedbackFormTitle) + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(.secondary) + } + .frame(height: 70) + .frame(maxWidth: .infinity) + .background(Color.secondary.opacity(0.1)) + + Divider() + + switch viewModel.viewState { + case .feedbackPending, .feedbackSending, .feedbackSendingFailed: + FeedbackFormBodyView() + .padding([.top, .leading, .trailing], 20) + + if viewModel.viewState == .feedbackSendingFailed { + Text(UserText.vpnFeedbackFormSendingConfirmationError) + .foregroundColor(.red) + .padding(.top, 15) + } + case .feedbackSent: + FeedbackFormSentView() + .padding([.top, .leading, .trailing], 20) + } + + Spacer(minLength: 0) + + FeedbackFormButtons() + .padding(20) + } + .onChange(of: viewModel.needsSubmitShowReport) { needsSubmitShowReport in + if needsSubmitShowReport { + Task { + await viewModel.process(action: .reportSubmitShow) + } + } + } + .task { + await viewModel.process(action: .reportShow) + } + } + +} + +private struct FeedbackFormBodyView: View { + + @EnvironmentObject var viewModel: UnifiedFeedbackFormViewModel + + var body: some View { + CategoryPicker(sources: UnifiedFeedbackReportType.self, selection: $viewModel.selectedReportType) { + switch UnifiedFeedbackReportType(rawValue: viewModel.selectedReportType) { + case .selectReportType, nil: + EmptyView() + case .general: + FeedbackFormIssueDescriptionView { + Text(UserText.pproFeedbackFormGeneralFeedbackPlaceholder) + } + case .requestFeature: + FeedbackFormIssueDescriptionView { + Text(UserText.pproFeedbackFormRequestFeaturePlaceholder) + } + case .reportIssue: + reportProblemView() + } + } + } + + @ViewBuilder + func reportProblemView() -> some View { + CategoryPicker(sources: UnifiedFeedbackCategory.self, selection: $viewModel.selectedCategory) { + switch UnifiedFeedbackCategory(rawValue: viewModel.selectedCategory) { + case .selectFeature, nil: + EmptyView() + case .subscription: + CategoryPicker(sources: PrivacyProFeedbackSubcategory.self, selection: $viewModel.selectedSubcategory) { + issueDescriptionView() + } + case .vpn: + CategoryPicker(sources: VPNFeedbackSubcategory.self, selection: $viewModel.selectedSubcategory) { + issueDescriptionView() + } + case .pir: + CategoryPicker(sources: PIRFeedbackSubcategory.self, selection: $viewModel.selectedSubcategory) { + issueDescriptionView() + } + case .itr: + CategoryPicker(sources: ITRFeedbackSubcategory.self, selection: $viewModel.selectedSubcategory) { + issueDescriptionView() + } + } + } + } + + @ViewBuilder + func issueDescriptionView() -> some View { + FeedbackFormIssueDescriptionView { + Text(LocalizedStringKey(UserText.pproFeedbackFormText1)) + .onURLTap { _ in + Task { + await viewModel.process(action: .reportFAQClick) + await viewModel.process(action: .faqClick) + } + } + } footer: { + Text(UserText.pproFeedbackFormText2) + VStack(alignment: .leading) { + Text(UserText.pproFeedbackFormText3) + Text(UserText.pproFeedbackFormText4) + } + Text(UserText.pproFeedbackFormText5) + } + } +} + +private struct CategoryPicker: View where Category.AllCases == [Category], Category.RawValue == String { + let sources: Category.Type + let selection: Binding + let content: () -> Content + + init(sources: Category.Type, + selection: Binding, + @ViewBuilder content: @escaping () -> Content) { + self.sources = sources + self.selection = selection + self.content = content + } + + var body: some View { + Group { + Picker(selection: selection, content: { + ForEach(sources.allCases) { option in + Text(option.displayName).tag(option.rawValue) + } + }, label: {}) + .controlSize(.large) + .padding(.bottom, 0) + + if Category(rawValue: selection.wrappedValue) == .prompt { + Spacer() + .frame(height: 50) + } else { + content() + } + } + } +} + +private struct FeedbackFormIssueDescriptionView: View { + @EnvironmentObject var viewModel: UnifiedFeedbackFormViewModel + + let label: () -> Label + let content: () -> Content + let footer: () -> Footer + + init(@ViewBuilder label: @escaping () -> Label, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder footer: @escaping () -> Footer) { + self.label = label + self.content = content + self.footer = footer + } + + init(@ViewBuilder label: @escaping () -> Label, + @ViewBuilder footer: @escaping () -> Footer) where Content == EmptyView { + self.init { + label() + } content: { + EmptyView() + } footer: { + footer() + } + } + + init(@ViewBuilder label: @escaping () -> Label) where Content == EmptyView, Footer == Text { + self.init { + label() + } content: { + EmptyView() + } footer: { + Text(UserText.pproFeedbackFormDisclaimer) + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + label() + .multilineTextAlignment(.leading) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + textEditor() + content() + footer() + .multilineTextAlignment(.leading) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.secondary) + } + } + + @ViewBuilder + func textEditor() -> some View { +#if APPSTORE + FocusableTextEditor(text: $viewModel.feedbackFormText, characterLimit: 1000) +#else + if #available(macOS 12, *) { + FocusableTextEditor(text: $viewModel.feedbackFormText, characterLimit: 1000) + } else { + TextEditor(text: $viewModel.feedbackFormText) + .frame(height: 197.0) + .font(.body) + .foregroundColor(.primary) + .onChange(of: viewModel.feedbackFormText) { + viewModel.feedbackFormText = String($0.prefix(1000)) + } + .padding(EdgeInsets(top: 3.0, leading: 6.0, bottom: 5.0, trailing: 0.0)) + .clipShape(RoundedRectangle(cornerRadius: 8.0, style: .continuous)) + .background( + ZStack { + RoundedRectangle(cornerRadius: 8.0) + .stroke(Color(.textEditorBorder), lineWidth: 0.4) + RoundedRectangle(cornerRadius: 8.0) + .fill(Color(.textEditorBackground)) + } + ) + } +#endif + } +} + +private struct FeedbackFormSentView: View { + + var body: some View { + VStack(spacing: 0) { + Image(.vpnFeedbackSent) + .padding(.top, 20) + + Text(UserText.pproFeedbackFormSendingConfirmationTitle) + .font(.system(size: 18, weight: .medium)) + .padding(.top, 30) + + Text(UserText.pproFeedbackFormSendingConfirmationDescription) + .multilineTextAlignment(.center) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 10) + } + } + +} + +private struct FeedbackFormButtons: View { + + @EnvironmentObject var viewModel: UnifiedFeedbackFormViewModel + + var body: some View { + HStack { + if viewModel.viewState == .feedbackSent { + button(text: UserText.pproFeedbackFormButtonDone, action: .cancel) + .keyboardShortcut(.defaultAction) + } else { + button(text: UserText.pproFeedbackFormButtonCancel, action: .cancel) + button(text: viewModel.viewState == .feedbackSending ? UserText.pproFeedbackFormButtonSubmitting : UserText.pproFeedbackFormButtonSubmit, action: .submit) + .keyboardShortcut(.defaultAction) + .disabled(!viewModel.submitButtonEnabled) + } + } + } + + @ViewBuilder + func button(text: String, action: UnifiedFeedbackFormViewModel.ViewAction) -> some View { + Button(action: { + Task { + await viewModel.process(action: action) + } + }, label: { + Text(text) + .frame(maxWidth: .infinity) + }) + .controlSize(.large) + .frame(maxWidth: .infinity) + } + +} diff --git a/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewController.swift b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewController.swift new file mode 100644 index 0000000000..8fa9d63fdc --- /dev/null +++ b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewController.swift @@ -0,0 +1,129 @@ +// +// UnifiedFeedbackFormViewController.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 +import SwiftUI +import Combine +import PixelKit + +final class UnifiedFeedbackFormViewController: NSViewController { + // Using a dynamic height in the form was causing layout problems and couldn't be completed in time for the release that needed this form. + // As a temporary measure, the heights of each form state are hardcoded. + // This should be cleaned up later, and eventually use the `sizingOptions` property of NSHostingController. + enum Constants { + static let landingPageHeight = 260.0 + static let feedbackFormCompactHeight = 430.0 + static let feedbackFormHeight = 650.0 + static let feedbackSentHeight = 350.0 + static let feedbackErrorHeight = 560.0 + } + + private let defaultSize = CGSize(width: 480, height: Constants.landingPageHeight) + + private let feedbackSender: UnifiedFeedbackSender + private let viewModel: UnifiedFeedbackFormViewModel + + private var heightConstraint: NSLayoutConstraint? + private var cancellables = Set() + + init(feedbackSender: UnifiedFeedbackSender = DefaultFeedbackSender(), + source: UnifiedFeedbackSource = .default) { + self.feedbackSender = feedbackSender + self.viewModel = UnifiedFeedbackFormViewModel( + vpnMetadataCollector: DefaultVPNMetadataCollector(accountManager: Application.appDelegate.subscriptionManager.accountManager), + feedbackSender: feedbackSender, + source: source + ) + super.init(nibName: nil, bundle: nil) + self.viewModel.delegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NSView(frame: NSRect(origin: CGPoint.zero, size: defaultSize)) + } + + override func viewDidLoad() { + super.viewDidLoad() + + let feedbackFormView = UnifiedFeedbackFormView() + let hostingView = NSHostingView(rootView: feedbackFormView.environmentObject(self.viewModel)) + hostingView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(hostingView) + + let heightConstraint = hostingView.heightAnchor.constraint(equalToConstant: defaultSize.height) + self.heightConstraint = heightConstraint + + NSLayoutConstraint.activate([ + heightConstraint, + hostingView.widthAnchor.constraint(equalToConstant: defaultSize.width), + hostingView.topAnchor.constraint(equalTo: view.topAnchor), + hostingView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + hostingView.leftAnchor.constraint(equalTo: view.leftAnchor), + hostingView.rightAnchor.constraint(equalTo: view.rightAnchor) + ]) + + subscribeToViewModelChanges() + } + + func subscribeToViewModelChanges() { + viewModel.$viewState + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateViewHeight() + } + .store(in: &cancellables) + + viewModel.$selectedReportType + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateViewHeight() + } + .store(in: &cancellables) + } + + private func updateViewHeight() { + switch viewModel.viewState { + case .feedbackPending: + if UnifiedFeedbackReportType(rawValue: viewModel.selectedReportType) == .prompt { + heightConstraint?.constant = Constants.landingPageHeight + } else { + heightConstraint?.constant = viewModel.usesCompactForm ? Constants.feedbackFormCompactHeight : Constants.feedbackFormHeight + } + case .feedbackSending: + heightConstraint?.constant = viewModel.usesCompactForm ? Constants.feedbackFormCompactHeight : Constants.feedbackFormHeight + case .feedbackSent: + heightConstraint?.constant = Constants.feedbackSentHeight + case .feedbackSendingFailed: + heightConstraint?.constant = Constants.feedbackErrorHeight + } + } + +} + +extension UnifiedFeedbackFormViewController: UnifiedFeedbackFormViewModelDelegate { + + func feedbackViewModelDismissedView(_ viewModel: UnifiedFeedbackFormViewModel) { + dismiss() + } + +} diff --git a/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewModel.swift b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewModel.swift new file mode 100644 index 0000000000..5d89a79282 --- /dev/null +++ b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackFormViewModel.swift @@ -0,0 +1,238 @@ +// +// UnifiedFeedbackFormViewModel.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 Combine +import SwiftUI +import PixelKit + +protocol UnifiedFeedbackFormViewModelDelegate: AnyObject { + func feedbackViewModelDismissedView(_ viewModel: UnifiedFeedbackFormViewModel) +} + +final class UnifiedFeedbackFormViewModel: ObservableObject { + enum ViewState { + case feedbackPending + case feedbackSending + case feedbackSendingFailed + case feedbackSent + + var canSubmit: Bool { + switch self { + case .feedbackPending: return true + case .feedbackSending: return false + case .feedbackSendingFailed: return true + case .feedbackSent: return false + } + } + } + + enum ViewAction { + case cancel + case submit + case faqClick + case reportShow + case reportSubmitShow + case reportFAQClick + } + + @Published var viewState: ViewState { + didSet { + updateSubmitButtonStatus() + } + } + + @Published var feedbackFormText: String = "" { + didSet { + updateSubmitButtonStatus() + } + } + + @Published private(set) var submitButtonEnabled: Bool = false + @Published var selectedReportType: String = UnifiedFeedbackReportType.prompt.rawValue { + didSet { + let defaultCategory: UnifiedFeedbackCategory + switch source { + case .ppro: defaultCategory = .subscription + case .vpn: defaultCategory = .vpn + case .pir: defaultCategory = .pir + case .itr: defaultCategory = .itr + default: defaultCategory = .prompt + } + selectedCategory = defaultCategory.rawValue + updateSubmitShowStatus() + } + } + @Published var selectedCategory: String = UnifiedFeedbackCategory.prompt.rawValue { + didSet { + selectedSubcategory = selectedSubcategoryPrompt + updateSubmitShowStatus() + } + } + @Published var selectedSubcategory = "" { + didSet { + updateSubmitShowStatus() + } + } + + private var selectedSubcategoryPrompt: String { + switch UnifiedFeedbackCategory(rawValue: selectedCategory) { + case .selectFeature, nil: return "" + case .subscription: return PrivacyProFeedbackSubcategory.prompt.rawValue + case .vpn: return VPNFeedbackSubcategory.prompt.rawValue + case .pir: return PIRFeedbackSubcategory.prompt.rawValue + case .itr: return ITRFeedbackSubcategory.prompt.rawValue + } + } + + @Published var needsSubmitShowReport = false + + var usesCompactForm: Bool { + switch UnifiedFeedbackReportType(rawValue: selectedReportType) { + case .reportIssue: + return false + default: + return true + } + } + + weak var delegate: UnifiedFeedbackFormViewModelDelegate? + + private let vpnMetadataCollector: any UnifiedMetadataCollector + private let defaultMetadataCollector: any UnifiedMetadataCollector + private let feedbackSender: any UnifiedFeedbackSender + + let source: UnifiedFeedbackSource + + init(vpnMetadataCollector: any UnifiedMetadataCollector, + defaultMetadataCollector: any UnifiedMetadataCollector = EmptyMetadataCollector(), + feedbackSender: any UnifiedFeedbackSender = DefaultFeedbackSender(), + source: UnifiedFeedbackSource = .default) { + self.viewState = .feedbackPending + + self.vpnMetadataCollector = vpnMetadataCollector + self.defaultMetadataCollector = defaultMetadataCollector + self.feedbackSender = feedbackSender + self.source = source + } + + @MainActor + func process(action: ViewAction) async { + switch action { + case .cancel: + delegate?.feedbackViewModelDismissedView(self) + case .submit: + self.viewState = .feedbackSending + + do { + try await sendFeedback() + self.viewState = .feedbackSent + } catch { + self.viewState = .feedbackSendingFailed + } + case .faqClick: + await openFAQ() + case .reportShow: + feedbackSender.sendFormShowPixel() + case .reportSubmitShow: + feedbackSender.sendSubmitScreenShowPixel(source: source, + reportType: selectedReportType, + category: selectedCategory, + subcategory: selectedSubcategory) + needsSubmitShowReport = false + case .reportFAQClick: + feedbackSender.sendSubmitScreenFAQClickPixel(source: source, + reportType: selectedReportType, + category: selectedCategory, + subcategory: selectedSubcategory) + } + } + + private func openFAQ() async { + guard !selectedReportType.isEmpty, UnifiedFeedbackReportType(rawValue: selectedReportType) == .reportIssue, + !selectedCategory.isEmpty, let category = UnifiedFeedbackCategory(rawValue: selectedCategory), + !selectedSubcategory.isEmpty else { + return + } + + let url: URL? = { + switch category { + case .selectFeature: return nil + case .subscription: return PrivacyProFeedbackSubcategory(rawValue: selectedSubcategory)?.url + case .vpn: return VPNFeedbackSubcategory(rawValue: selectedSubcategory)?.url + case .pir: return PIRFeedbackSubcategory(rawValue: selectedSubcategory)?.url + case .itr: return ITRFeedbackSubcategory(rawValue: selectedSubcategory)?.url + } + }() + + if let url { + NSWorkspace.shared.open(url) + } + } + + private func sendFeedback() async throws { + switch UnifiedFeedbackReportType(rawValue: selectedReportType) { + case .selectReportType, nil: + return + case .requestFeature: + try await feedbackSender.sendFeatureRequestPixel(description: feedbackFormText, + source: source) + case .general: + try await feedbackSender.sendGeneralFeedbackPixel(description: feedbackFormText, + source: source) + case .reportIssue: + try await reportProblem() + } + } + + private func reportProblem() async throws { + switch UnifiedFeedbackCategory(rawValue: selectedCategory) { + case .vpn: + let metadata = await vpnMetadataCollector.collectMetadata() + try await feedbackSender.sendReportIssuePixel(source: source, + category: selectedCategory, + subcategory: selectedSubcategory, + description: feedbackFormText, + metadata: metadata as? VPNMetadata) + default: + let metadata = await defaultMetadataCollector.collectMetadata() + try await feedbackSender.sendReportIssuePixel(source: source, + category: selectedCategory, + subcategory: selectedSubcategory, + description: feedbackFormText, + metadata: metadata as? EmptyFeedbackMetadata) + } + } + + private func updateSubmitButtonStatus() { + self.submitButtonEnabled = viewState.canSubmit && !feedbackFormText.isEmpty + } + + private func updateSubmitShowStatus() { + needsSubmitShowReport = { + switch UnifiedFeedbackReportType(rawValue: selectedReportType) { + case .selectReportType, nil: + return false + case .requestFeature, .general: + return true + case .reportIssue: + return selectedCategory != UnifiedFeedbackCategory.prompt.rawValue && selectedSubcategory != selectedSubcategoryPrompt + } + }() + } +} diff --git a/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackSender.swift b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackSender.swift new file mode 100644 index 0000000000..edbb839d04 --- /dev/null +++ b/DuckDuckGo/UnifiedFeedbackForm/UnifiedFeedbackSender.swift @@ -0,0 +1,118 @@ +// +// UnifiedFeedbackSender.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 PixelKit + +enum UnifiedFeedbackSource: String, StringRepresentable { + case settings, ppro, vpn, pir, itr, unknown + static var `default` = UnifiedFeedbackSource.unknown +} + +protocol UnifiedFeedbackSender { + func sendFeatureRequestPixel(description: String, source: UnifiedFeedbackSource) async throws + func sendGeneralFeedbackPixel(description: String, source: UnifiedFeedbackSource) async throws + func sendReportIssuePixel(source: UnifiedFeedbackSource, category: String, subcategory: String, description: String, metadata: T?) async throws + + func sendFormShowPixel() + func sendSubmitScreenShowPixel(source: UnifiedFeedbackSource, reportType: String, category: String, subcategory: String) + func sendSubmitScreenFAQClickPixel(source: UnifiedFeedbackSource, reportType: String, category: String, subcategory: String) +} + +extension UnifiedFeedbackSender { + func sendStandardPixel(_ pixel: PixelKitEventV2) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + PixelKit.fire(pixel, frequency: .standard) { _, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } +} + +protocol StringRepresentable: RawRepresentable { + static var `default`: Self { get } +} + +extension StringRepresentable where RawValue == String { + static func from(_ text: String) -> String { + (Self(rawValue: text) ?? .default).rawValue + } +} + +struct DefaultFeedbackSender: UnifiedFeedbackSender { + enum ReportType: String, StringRepresentable { + case general, reportIssue, requestFeature + static var `default` = ReportType.general + } + + enum Category: String, StringRepresentable { + case subscription, vpn, pir, itr, unknown + static var `default` = Category.unknown + } + + enum Subcategory: String, StringRepresentable { + case otp + case unableToInstall, failsToConnect, tooSlow, issueWithAppOrWebsite, appCrashesOrFreezes, cantConnectToLocalDevice + case nothingOnSpecificSite, notMe, scanStuck, removalStuck + case accessCode, cantContactAdvisor, advisorUnhelpful + case somethingElse + static var `default` = Subcategory.somethingElse + } + + func sendFeatureRequestPixel(description: String, source: UnifiedFeedbackSource) async throws { + try await sendStandardPixel(GeneralPixel.pproFeedbackFeatureRequest(description: description, + source: source.rawValue)) + } + + func sendGeneralFeedbackPixel(description: String, source: UnifiedFeedbackSource) async throws { + try await sendStandardPixel(GeneralPixel.pproFeedbackGeneralFeedback(description: description, + source: source.rawValue)) + } + + func sendReportIssuePixel(source: UnifiedFeedbackSource, category: String, subcategory: String, description: String, metadata: T?) async throws { + try await sendStandardPixel(GeneralPixel.pproFeedbackReportIssue(source: source.rawValue, + category: Category.from(category), + subcategory: Subcategory.from(subcategory), + description: description, + metadata: metadata?.toBase64() ?? "")) + } + + func sendFormShowPixel() { + PixelKit.fire(GeneralPixel.pproFeedbackFormShow, frequency: .dailyAndCount) + } + + func sendSubmitScreenShowPixel(source: UnifiedFeedbackSource, reportType: String, category: String, subcategory: String) { + PixelKit.fire(GeneralPixel.pproFeedbackSubmitScreenShow(source: source.rawValue, + reportType: ReportType.from(reportType), + category: Category.from(category), + subcategory: Subcategory.from(subcategory)), + frequency: .dailyAndCount) + } + + func sendSubmitScreenFAQClickPixel(source: UnifiedFeedbackSource, reportType: String, category: String, subcategory: String) { + PixelKit.fire(GeneralPixel.pproFeedbackSubmitScreenFAQClick(source: source.rawValue, + reportType: ReportType.from(reportType), + category: Category.from(category), + subcategory: Subcategory.from(subcategory)), + frequency: .dailyAndCount) + } +} diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormView.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormView.swift index fb3f6ccf4b..a34ccfcdc1 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormView.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormView.swift @@ -39,7 +39,7 @@ struct VPNFeedbackFormView: View { switch viewModel.viewState { case .feedbackPending, .feedbackSending, .feedbackSendingFailed: VPNFeedbackFormBodyView() - .padding([.top, .leading, .trailing], 20) + .padding([.top, .leading, .trailing], 20) if viewModel.viewState == .feedbackSendingFailed { Text(UserText.vpnFeedbackFormSendingConfirmationError) diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift index 590d958513..22e38142a2 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewController.swift @@ -81,15 +81,15 @@ final class VPNFeedbackFormViewController: NSViewController { .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.updateViewHeight() - } - .store(in: &cancellables) + } + .store(in: &cancellables) viewModel.$selectedFeedbackCategory .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.updateViewHeight() - } - .store(in: &cancellables) + } + .store(in: &cancellables) } private func updateViewHeight() { diff --git a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift index 70aeb3e110..3fff69c5d2 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNFeedbackFormViewModel.swift @@ -85,7 +85,7 @@ final class VPNFeedbackFormViewModel: ObservableObject { self.viewState = .feedbackSending do { - let metadata = await self.metadataCollector.collectMetadata() + let metadata = await self.metadataCollector.collectVPNMetadata() try await self.feedbackSender.send(metadata: metadata, category: selectedFeedbackCategory, userText: feedbackFormText) self.viewState = .feedbackSent } catch { diff --git a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift index 1c27d86d1c..778b039358 100644 --- a/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift +++ b/DuckDuckGo/VPNFeedbackForm/VPNMetadataCollector.swift @@ -115,7 +115,7 @@ struct VPNMetadata: Encodable { } protocol VPNMetadataCollector { - func collectMetadata() async -> VPNMetadata + func collectVPNMetadata() async -> VPNMetadata } final class DefaultVPNMetadataCollector: VPNMetadataCollector { @@ -162,7 +162,7 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } @MainActor - func collectMetadata() async -> VPNMetadata { + func collectVPNMetadata() async -> VPNMetadata { let appInfoMetadata = collectAppInfoMetadata() let deviceInfoMetadata = collectDeviceInfoMetadata() let networkInfoMetadata = await collectNetworkInformation() @@ -216,17 +216,17 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } private func getMachineArchitecture() -> String { - #if arch(arm) - return "arm" - #elseif arch(arm64) - return "arm64" - #elseif arch(i386) - return "i386" - #elseif arch(x86_64) - return "x86_64" - #else - return "unknown" - #endif +#if arch(arm) + return "arm" +#elseif arch(arm64) + return "arm64" +#elseif arch(i386) + return "i386" +#elseif arch(x86_64) + return "x86_64" +#else + return "unknown" +#endif } func collectNetworkInformation() async -> VPNMetadata.NetworkInfo { @@ -329,3 +329,13 @@ final class DefaultVPNMetadataCollector: VPNMetadataCollector { } } + +// MARK: - Unified feedback form support + +extension VPNMetadata: UnifiedFeedbackMetadata {} + +extension DefaultVPNMetadataCollector: UnifiedMetadataCollector { + func collectMetadata() async -> VPNMetadata { + await collectVPNMetadata() + } +} diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index 3cbb3630d8..4e9f8548b6 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -19,6 +19,7 @@ import Cocoa import Combine import Common +import BrowserServicesKit @MainActor protocol WindowControllersManagerProtocol { @@ -36,14 +37,18 @@ protocol WindowControllersManagerProtocol { @MainActor final class WindowControllersManager: WindowControllersManagerProtocol { - static let shared = WindowControllersManager(pinnedTabsManager: Application.appDelegate.pinnedTabsManager) + static let shared = WindowControllersManager(pinnedTabsManager: Application.appDelegate.pinnedTabsManager, + subscriptionFeatureAvailability: DefaultSubscriptionFeatureAvailability() + ) var activeViewController: MainViewController? { lastKeyMainWindowController?.mainViewController } - init(pinnedTabsManager: PinnedTabsManager) { + init(pinnedTabsManager: PinnedTabsManager, + subscriptionFeatureAvailability: SubscriptionFeatureAvailability) { self.pinnedTabsManager = pinnedTabsManager + self.subscriptionFeatureAvailability = subscriptionFeatureAvailability } /** @@ -52,6 +57,7 @@ final class WindowControllersManager: WindowControllersManagerProtocol { @Published private(set) var isInInitialState: Bool = true @Published private(set) var mainWindowControllers = [MainWindowController]() private(set) var pinnedTabsManager: PinnedTabsManager + private let subscriptionFeatureAvailability: SubscriptionFeatureAvailability weak var lastKeyMainWindowController: MainWindowController? { didSet { @@ -226,8 +232,14 @@ extension WindowControllersManager { windowController.mainViewController.navigationBarViewController.showNetworkProtectionStatus() } - func showShareFeedbackModal() { - let feedbackFormViewController = VPNFeedbackFormViewController() + func showShareFeedbackModal(source: UnifiedFeedbackSource = .default) { + let feedbackFormViewController: NSViewController = { + if subscriptionFeatureAvailability.usesUnifiedFeedbackForm { + return UnifiedFeedbackFormViewController(source: source) + } else { + return VPNFeedbackFormViewController() + } + }() let feedbackFormWindowController = feedbackFormViewController.wrappedInWindowController() guard let feedbackFormWindow = feedbackFormWindowController.window else { diff --git a/DuckDuckGoDBPBackgroundAgent/UserText.swift b/DuckDuckGoDBPBackgroundAgent/UserText.swift index 608cab9e14..05eb833e12 100644 --- a/DuckDuckGoDBPBackgroundAgent/UserText.swift +++ b/DuckDuckGoDBPBackgroundAgent/UserText.swift @@ -21,6 +21,6 @@ import Foundation final class UserText { // MARK: - Status Menu - static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share VPN Feedback…", comment: "The status menu 'Share VPN Feedback' menu item") + static let networkProtectionStatusMenuSendFeedback = NSLocalizedString("network.protection.status.menu.send.feedback", value: "Send Feedback…", comment: "The status menu 'Send Feedback' menu item") static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.open.duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") } diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index b205ba17d8..1951ee6650 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -331,7 +331,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuFAQ, action: { [weak self] in try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.showFAQ) }), - StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuShareFeedback, action: { [weak self] in + StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuSendFeedback, action: { [weak self] in try? await self?.appLauncher.launchApp(withCommand: VPNAppLaunchCommand.shareFeedback) }), StatusBarMenu.MenuItem(name: UserText.networkProtectionStatusMenuOpenDuckDuckGo, action: { [weak self] in diff --git a/DuckDuckGoVPN/UserText.swift b/DuckDuckGoVPN/UserText.swift index e7fbe48d86..0e7d32e691 100644 --- a/DuckDuckGoVPN/UserText.swift +++ b/DuckDuckGoVPN/UserText.swift @@ -25,4 +25,5 @@ final class UserText { static let networkProtectionStatusMenuFAQ = NSLocalizedString("network.protection.status.menu.faq", value: "FAQs and Support…", comment: "The status menu 'FAQ' menu item") static let networkProtectionStatusMenuOpenDuckDuckGo = NSLocalizedString("network.protection.status.menu.vpn.open-duckduckgo", value: "Open DuckDuckGo…", comment: "The status menu 'Open DuckDuckGo' menu item") static let networkProtectionStatusMenuShareFeedback = NSLocalizedString("network.protection.status.menu.share.feedback", value: "Share VPN Feedback…", comment: "The status menu 'Share VPN Feedback' menu item") + static let networkProtectionStatusMenuSendFeedback = NSLocalizedString("network.protection.status.menu.send.feedback", value: "Send Feedback…", comment: "The status menu 'Send Feedback' menu item") } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 871782e9ca..c95a1baf87 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: "186.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "186.1.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 2c50733ad9..a77a119908 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "186.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "186.1.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift index e418d7ce5a..e86879002c 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Extensions/UserText+NetworkProtectionUI.swift @@ -34,7 +34,7 @@ final class UserText { static let vpnLocationSelected = NSLocalizedString("network.protection.vpn.location.selected", value: "Selected Location", comment: "Description of the location type in the VPN status view") static let vpnDnsServer = NSLocalizedString("network.protection.vpn.dns-server", value: "DNS Server", comment: "Title for the DNS server section in the VPN status view") static let vpnDataVolume = NSLocalizedString("network.protection.vpn.data-volume", value: "Data Volume", comment: "Title for the data volume section in the VPN status view") - static let vpnShareFeedback = NSLocalizedString("network.protection.vpn.share-feedback", value: "Share VPN Feedback…", comment: "Action button title for the Share VPN feedback option") + static let vpnSendFeedback = NSLocalizedString("network.protection.vpn.send-feedback", value: "Send Feedback…", comment: "Action button title for the Send feedback option") static let vpnOperationNotPermittedMessage = NSLocalizedString("network.protection.vpn.failure.operation-not-permitted", value: "Unable to connect due to an unexpected error. Restarting your Mac can usually fix the issue.", comment: "Error message for the Operation not permitted error") static let vpnLoginItemVersionMismatchedMessage = NSLocalizedString("network.protection.vpn.failure.login-item-version-mismatched", value: "Unable to connect due to versioning conflict. If you have multiple versions of the browser installed, remove all but the most recent version of DuckDuckGo and restart your Mac.", comment: "Error message for the Login item version mismatched error") static let vpnRegisteredServerFetchingFailedMessage = NSLocalizedString("network.protection.vpn.failure.registered-server-fetching-failed", value: "Unable to connect. Double check your internet connection. Make sure other software or services aren't blocking DuckDuckGo VPN servers.", comment: "Error message for the Failed to fetch registered server error") diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift index 835078e15b..8385f23f26 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/StatusView/NetworkProtectionStatusViewModel.swift @@ -345,7 +345,7 @@ extension NetworkProtectionStatusView { var warningViewModel: WarningView.Model? { if let warningMessage = warningMessage(for: knownFailure) { return WarningView.Model(message: warningMessage, - actionTitle: UserText.vpnShareFeedback, + actionTitle: UserText.vpnSendFeedback, action: openFeedbackForm) } diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 28af28412f..e488e81990 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,13 +12,14 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "186.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "186.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ .target( name: "SubscriptionUI", dependencies: [ + .product(name: "BrowserServicesKit", package: "BrowserServicesKit"), .product(name: "Subscription", package: "BrowserServicesKit"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions"), .product(name: "PreferencesViews", package: "SwiftUIExtensions"), diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift index bfacdc587e..53dcbb131e 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionModel.swift @@ -57,6 +57,7 @@ public final class PreferencesSubscriptionModel: ObservableObject { case openVPN, openDB, openITR, + openFeedback, iHaveASubscriptionClick, activateAddEmailClick, postSubscriptionAddEmailClick, @@ -282,6 +283,11 @@ public final class PreferencesSubscriptionModel: ObservableObject { openURLHandler(subscriptionManager.url(for: .faq)) } + @MainActor + func openUnifiedFeedbackForm() { + userEventHandler(.openFeedback) + } + @MainActor func refreshSubscriptionPendingState() { if subscriptionManager.currentEnvironment.purchasePlatform == .appStore { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift index 57a37fe5c4..283f5e7f7e 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/Preferences/PreferencesSubscriptionView.swift @@ -19,6 +19,7 @@ import PreferencesViews import SwiftUI import SwiftUIExtensions +import BrowserServicesKit public struct PreferencesSubscriptionView: View { @@ -30,8 +31,12 @@ public struct PreferencesSubscriptionView: View { @State private var manageSubscriptionSheet: ManageSubscriptionSheet? - public init(model: PreferencesSubscriptionModel) { + private let subscriptionFeatureAvailability: SubscriptionFeatureAvailability + + public init(model: PreferencesSubscriptionModel, + subscriptionFeatureAvailability: SubscriptionFeatureAvailability) { self.model = model + self.subscriptionFeatureAvailability = subscriptionFeatureAvailability } public var body: some View { @@ -96,6 +101,11 @@ public struct PreferencesSubscriptionView: View { // Help section helpSection + + // Feedback section + if subscriptionFeatureAvailability.usesUnifiedFeedbackForm, state == .subscriptionActive { + feedbackSection + } } .onAppear(perform: { if model.isUserAuthenticated { @@ -277,6 +287,17 @@ public struct PreferencesSubscriptionView: View { } } + @ViewBuilder + private var feedbackSection: some View { + PreferencePaneSection { + TextMenuItemHeader(UserText.preferencesSubscriptionFeedbackTitle, bottomPadding: 0) + HStack(alignment: .top, spacing: 6) { + TextMenuItemCaption(UserText.preferencesSubscriptionFeedbackCaption) + Button(UserText.preferencesSubscriptionFeedbackButton) { model.openUnifiedFeedbackForm() } + } + } + } + @ViewBuilder private var emailView: some View { VStack { diff --git a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift index 388c2c0131..0a22093283 100644 --- a/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift +++ b/LocalPackages/SubscriptionUI/Sources/SubscriptionUI/UserText.swift @@ -50,6 +50,9 @@ enum UserText { static let preferencesSubscriptionFooterTitle = NSLocalizedString("subscription.preferences.subscription.footer.title", value: "Need help with Privacy Pro?", comment: "Title for the subscription preferences pane footer") static let preferencesSubscriptionFooterCaption = NSLocalizedString("subscription.preferences.subscription.footer.caption", value: "Get answers to frequently asked questions or contact Privacy Pro support from our help pages.", comment: "Caption for the subscription preferences pane footer") static let viewFaqsButton = NSLocalizedString("subscription.preferences.view.faqs.button", value: "FAQs and Support", comment: "Button to open page for FAQs") + static let preferencesSubscriptionFeedbackTitle = NSLocalizedString("subscription.preferences.feedback.title", value: "Send Feedback", comment: "Title for the subscription feedback section") + static let preferencesSubscriptionFeedbackCaption = NSLocalizedString("subscription.preferences.feedback.caption", value: "Help improve Privacy Pro. Your feedback matters to us. Feel free to report any issues or provide general feedback.", comment: "Caption for the subscription feedback section") + static let preferencesSubscriptionFeedbackButton = NSLocalizedString("subscription.preferences.feedback.button", value: "Send Feedback", comment: "Title for the subscription feedback button") static func preferencesSubscriptionActiveRenewCaption(period: String, formattedDate: String) -> String { let localized = NSLocalizedString("subscription.preferences.subscription.active.renew.caption", value: "Your %@ Privacy Pro subscription renews on %@.", comment: "Caption for the subscription preferences pane when the subscription is active and will renew. First parameter is renewal period (monthly/yearly). Second parameter is date.") diff --git a/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift index f85d03a145..b64e193a4b 100644 --- a/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift +++ b/UnitTests/DBP/Tests/DataBrokerProtectionFeatureGatekeeperTests.swift @@ -123,12 +123,15 @@ private class MockFeatureDisabler: DataBrokerProtectionFeatureDisabling { private class MockFeatureAvailability: SubscriptionFeatureAvailability { var mockFeatureAvailable: Bool = false var mockSubscriptionPurchaseAllowed: Bool = false + var mockUsesUnifiedFeedbackForm: Bool = false var isFeatureAvailable: Bool { mockFeatureAvailable } var isSubscriptionPurchaseAllowed: Bool { mockSubscriptionPurchaseAllowed } + var usesUnifiedFeedbackForm: Bool { mockUsesUnifiedFeedbackForm } func reset() { mockFeatureAvailable = false mockSubscriptionPurchaseAllowed = false + mockUsesUnifiedFeedbackForm = false } } diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 97b37a84c0..6d95c92e88 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -81,7 +81,8 @@ final class MoreOptionsMenuTests: XCTestCase { passwordManagerCoordinator: passwordManagerCoordinator, vpnFeatureGatekeeper: networkProtectionVisibilityMock, subscriptionFeatureAvailability: SubscriptionFeatureAvailabilityMock(isFeatureAvailable: true, - isSubscriptionPurchaseAllowed: true), + isSubscriptionPurchaseAllowed: true, + usesUnifiedFeedbackForm: false), sharingMenu: NSMenu(), internalUserDecider: internalUserDecider, subscriptionManager: subscriptionManager) diff --git a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift b/UnitTests/NetworkProtection/VPNFeedbackFormViewModelTests.swift similarity index 99% rename from UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift rename to UnitTests/NetworkProtection/VPNFeedbackFormViewModelTests.swift index a12d3a8fa1..9494befe17 100644 --- a/UnitTests/VPNFeedbackForm/VPNFeedbackFormViewModelTests.swift +++ b/UnitTests/NetworkProtection/VPNFeedbackFormViewModelTests.swift @@ -80,7 +80,7 @@ private class MockVPNMetadataCollector: VPNMetadataCollector { var collectedMetadata: Bool = false - func collectMetadata() async -> VPNMetadata { + func collectVPNMetadata() async -> VPNMetadata { self.collectedMetadata = true let appInfo = VPNMetadata.AppInfo( diff --git a/UnitTests/Subscription/Mocks/SubscriptionFeatureAvailabilityMock.swift b/UnitTests/Subscription/Mocks/SubscriptionFeatureAvailabilityMock.swift index d6153695f7..aa5c9a0b6f 100644 --- a/UnitTests/Subscription/Mocks/SubscriptionFeatureAvailabilityMock.swift +++ b/UnitTests/Subscription/Mocks/SubscriptionFeatureAvailabilityMock.swift @@ -23,9 +23,11 @@ import BrowserServicesKit public struct SubscriptionFeatureAvailabilityMock: SubscriptionFeatureAvailability { public var isFeatureAvailable: Bool public var isSubscriptionPurchaseAllowed: Bool + public var usesUnifiedFeedbackForm: Bool - public init(isFeatureAvailable: Bool, isSubscriptionPurchaseAllowed: Bool) { + public init(isFeatureAvailable: Bool, isSubscriptionPurchaseAllowed: Bool, usesUnifiedFeedbackForm: Bool) { self.isFeatureAvailable = isFeatureAvailable self.isSubscriptionPurchaseAllowed = isSubscriptionPurchaseAllowed + self.usesUnifiedFeedbackForm = usesUnifiedFeedbackForm } } diff --git a/UnitTests/UnifiedFeedbackForm/UnifiedFeedbackFormViewModelTests.swift b/UnitTests/UnifiedFeedbackForm/UnifiedFeedbackFormViewModelTests.swift new file mode 100644 index 0000000000..2e7dae8803 --- /dev/null +++ b/UnitTests/UnifiedFeedbackForm/UnifiedFeedbackFormViewModelTests.swift @@ -0,0 +1,195 @@ +// +// UnifiedFeedbackFormViewModelTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class UnifiedFeedbackFormViewModelTests: XCTestCase { + + func testWhenCreatingViewModel_ThenInitialStateIsFeedbackPending() throws { + let collector = MockVPNMetadataCollector() + let sender = MockVPNFeedbackSender() + let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: collector, feedbackSender: sender) + + XCTAssertEqual(viewModel.viewState, .feedbackPending) + } + + func testWhenSendingFeedbackSucceeds_ThenFeedbackIsSent() async throws { + let collector = MockVPNMetadataCollector() + let sender = MockVPNFeedbackSender() + let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: collector, feedbackSender: sender) + viewModel.selectedReportType = UnifiedFeedbackReportType.reportIssue.rawValue + let text = "Some feedback report text" + viewModel.feedbackFormText = text + + XCTAssertFalse(sender.sentMetadata) + await viewModel.process(action: .submit) + XCTAssertTrue(sender.sentMetadata) + XCTAssertEqual(sender.receivedData!.4, text) + } + + func testWhenSendingFeedbackFails_ThenFeedbackIsNotSent() async throws { + let collector = MockVPNMetadataCollector() + let sender = MockVPNFeedbackSender() + let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: collector, feedbackSender: sender) + viewModel.selectedReportType = UnifiedFeedbackReportType.reportIssue.rawValue + let text = "Some feedback report text" + viewModel.feedbackFormText = text + sender.throwErrorWhenSending = true + + XCTAssertFalse(sender.sentMetadata) + await viewModel.process(action: .submit) + XCTAssertFalse(sender.sentMetadata) + XCTAssertEqual(viewModel.viewState, .feedbackSendingFailed) + } + + func testWhenCancelActionIsReceived_ThenViewModelSendsCancelActionToDelegate() async throws { + let collector = MockVPNMetadataCollector() + let sender = MockVPNFeedbackSender() + let delegate = MockVPNFeedbackFormViewModelDelegate() + let viewModel = UnifiedFeedbackFormViewModel(vpnMetadataCollector: collector, feedbackSender: sender) + viewModel.delegate = delegate + + XCTAssertFalse(delegate.receivedDismissedViewCallback) + await viewModel.process(action: .cancel) + XCTAssertTrue(delegate.receivedDismissedViewCallback) + } +} + +// MARK: - Mocks + +private class MockVPNMetadataCollector: UnifiedMetadataCollector { + var collectedMetadata = false + + func collectMetadata() async -> VPNMetadata { + self.collectedMetadata = true + + let appInfo = VPNMetadata.AppInfo( + appVersion: "1.2.3", + lastAgentVersionRun: "1.2.3", + lastExtensionVersionRun: "1.2.3", + isInternalUser: false, + isInApplicationsDirectory: true + ) + + let deviceInfo = VPNMetadata.DeviceInfo( + osVersion: "14.0.0", + buildFlavor: "dmg", + lowPowerModeEnabled: false, + cpuArchitecture: "arm64" + ) + + let networkInfo = VPNMetadata.NetworkInfo(currentPath: "path") + + let vpnState = VPNMetadata.VPNState( + onboardingState: "onboarded", + connectionState: "connected", + lastStartErrorDescription: "none", + lastTunnelErrorDescription: "none", + lastKnownFailureDescription: "none", + connectedServer: "Paoli, PA", + connectedServerIP: "123.123.123.123" + ) + + let vpnSettingsState = VPNMetadata.VPNSettingsState( + connectOnLoginEnabled: true, + includeAllNetworksEnabled: true, + enforceRoutesEnabled: true, + excludeLocalNetworksEnabled: true, + notifyStatusChangesEnabled: true, + showInMenuBarEnabled: true, + selectedServer: "server", + selectedEnvironment: "production", + customDNS: false + ) + + let loginItemState = VPNMetadata.LoginItemState( + vpnMenuState: "enabled", + vpnMenuIsRunning: true, + notificationsAgentState: "enabled", + notificationsAgentIsRunning: true + ) + + let privacyProInfo = VPNMetadata.PrivacyProInfo( + hasPrivacyProAccount: true, + hasVPNEntitlement: true + ) + + return VPNMetadata( + appInfo: appInfo, + deviceInfo: deviceInfo, + networkInfo: networkInfo, + vpnState: vpnState, + vpnSettingsState: vpnSettingsState, + loginItemState: loginItemState, + privacyProInfo: privacyProInfo + ) + } + +} + +private class MockVPNFeedbackSender: UnifiedFeedbackSender { + var throwErrorWhenSending: Bool = false + var sentMetadata: Bool = false + + var receivedData: (VPNMetadata?, UnifiedFeedbackSource, String?, String?, String?)? + + enum SomeError: Error { + case error + } + + func sendFeatureRequestPixel(description: String, source: UnifiedFeedbackSource) async throws { + if throwErrorWhenSending { + throw SomeError.error + } + + self.sentMetadata = true + self.receivedData = (nil, source, nil, nil, description) + } + + func sendGeneralFeedbackPixel(description: String, source: UnifiedFeedbackSource) async throws { + if throwErrorWhenSending { + throw SomeError.error + } + + self.sentMetadata = true + self.receivedData = (nil, source, nil, nil, description) + } + + func sendReportIssuePixel(source: UnifiedFeedbackSource, category: String, subcategory: String, description: String, metadata: T?) async throws { + if throwErrorWhenSending { + throw SomeError.error + } + + self.sentMetadata = true + self.receivedData = (metadata as? VPNMetadata, source, category, subcategory, description) + } + + func sendFormShowPixel() {} + func sendSubmitScreenShowPixel(source: UnifiedFeedbackSource, reportType: String, category: String, subcategory: String) {} + func sendSubmitScreenFAQClickPixel(source: UnifiedFeedbackSource, reportType: String, category: String, subcategory: String) {} +} + +private class MockVPNFeedbackFormViewModelDelegate: UnifiedFeedbackFormViewModelDelegate { + var receivedDismissedViewCallback: Bool = false + + func feedbackViewModelDismissedView(_ viewModel: UnifiedFeedbackFormViewModel) { + receivedDismissedViewCallback = true + } + +} From d37e6991d8058446816e43fd16242c7e91a2b871 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 27 Aug 2024 20:41:33 -0700 Subject: [PATCH 02/14] Move WireGuard dependency to VPN extensions (#3144) Task/Issue URL: https://app.asana.com/0/414709148257752/1207874749913488/f Tech Design URL: CC: Description: This PR updates the app to add WireGuard as a direct dependency, instead of inheriting it from BSK. The issue with the inheritance approach is that WireGuard was being implicitly included in targets that didn't need it, like the VPN agent and app. --- DuckDuckGo.xcodeproj/project.pbxproj | 27 +++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 8 ++--- .../MacPacketTunnelProvider.swift | 32 +++++++++++++++++++ .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 6 files changed, 65 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index df3f38cb87..81aeb0a5f0 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1210,6 +1210,8 @@ 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4D60E32A0C883A00BCD287 /* AppMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60E12A0C883A00BCD287 /* AppMain.swift */; }; 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4F72EB266B2ED300814C60 /* CollectionExtension.swift */; }; + 4B5235452C7BB14D00AFAF64 /* WireGuard in Frameworks */ = {isa = PBXBuildFile; productRef = 4B5235442C7BB14D00AFAF64 /* WireGuard */; }; + 4B5235472C7BB15700AFAF64 /* WireGuard in Frameworks */ = {isa = PBXBuildFile; productRef = 4B5235462C7BB15700AFAF64 /* WireGuard */; }; 4B59023E26B35F3600489384 /* ChromiumLoginReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59023926B35F3600489384 /* ChromiumLoginReader.swift */; }; 4B59024026B35F3600489384 /* ChromiumDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59023B26B35F3600489384 /* ChromiumDataImporter.swift */; }; 4B59024826B3673600489384 /* ThirdPartyBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B59024726B3673600489384 /* ThirdPartyBrowser.swift */; }; @@ -4441,6 +4443,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4B5235452C7BB14D00AFAF64 /* WireGuard in Frameworks */, 37269F012B332FC8005E8E46 /* Common in Frameworks */, 9D9DE57B2C63AA1F00D20B15 /* AppKitExtensions in Frameworks */, EE7295E92A545BC4008C0991 /* NetworkProtection in Frameworks */, @@ -4505,6 +4508,7 @@ buildActionMask = 2147483647; files = ( F1DF95E52BD1807C0045E591 /* Subscription in Frameworks */, + 4B5235472C7BB15700AFAF64 /* WireGuard in Frameworks */, 37269EFF2B332FBB005E8E46 /* Common in Frameworks */, EE7295E72A545BBB008C0991 /* NetworkProtection in Frameworks */, F198C7162BD18A44000BF24D /* PixelKit in Frameworks */, @@ -8857,6 +8861,7 @@ 7B37C7A42BAA32A50062546A /* Subscription */, F198C7172BD18A4C000BF24D /* PixelKit */, 9D9DE57A2C63AA1F00D20B15 /* AppKitExtensions */, + 4B5235442C7BB14D00AFAF64 /* WireGuard */, ); productName = NetworkProtectionSystemExtension; productReference = 4B25375A2A11BE7300610219 /* com.duckduckgo.macos.vpn.network-extension.debug.systemextension */; @@ -8978,6 +8983,7 @@ F198C7152BD18A44000BF24D /* PixelKit */, 7B2366872C09FADA002D393F /* VPNAppLauncher */, 9D9DE5762C63AA1600D20B15 /* AppKitExtensions */, + 4B5235462C7BB15700AFAF64 /* WireGuard */, ); productName = NetworkProtectionAppExtension; productReference = 4B4D603D2A0B290200BCD287 /* NetworkProtectionAppExtension.appex */; @@ -9321,6 +9327,7 @@ B6F997B92B8F352500476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */, F1D43AF12B98E47800BAB743 /* XCRemoteSwiftPackageReference "BareBonesBrowser" */, 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */, + 4B5235432C7BB14D00AFAF64 /* XCRemoteSwiftPackageReference "wireguard-apple" */, ); productRefGroup = AA585D7F248FD31100E9A3E2 /* Products */; projectDirPath = ""; @@ -13532,12 +13539,20 @@ revision = c06709ba8a586f6a40190bacaaaaa96b2d55e540; }; }; + 4B5235432C7BB14D00AFAF64 /* XCRemoteSwiftPackageReference "wireguard-apple" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/duckduckgo/wireguard-apple"; + requirement = { + kind = exactVersion; + version = 1.1.3; + }; + }; 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 186.1.0; + version = 187.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -13814,6 +13829,16 @@ isa = XCSwiftPackageProductDependency; productName = NetworkProtectionUI; }; + 4B5235442C7BB14D00AFAF64 /* WireGuard */ = { + isa = XCSwiftPackageProductDependency; + package = 4B5235432C7BB14D00AFAF64 /* XCRemoteSwiftPackageReference "wireguard-apple" */; + productName = WireGuard; + }; + 4B5235462C7BB15700AFAF64 /* WireGuard */ = { + isa = XCSwiftPackageProductDependency; + package = 4B5235432C7BB14D00AFAF64 /* XCRemoteSwiftPackageReference "wireguard-apple" */; + productName = WireGuard; + }; 4B5F14FB2A15291D0060320F /* InputFilesChecker */ = { isa = XCSwiftPackageProductDependency; productName = "plugin:InputFilesChecker"; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2b8ba57fc4..3423380f00 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "606ccf9e86f5cad3ae83132f46241357feecf152", - "version" : "186.1.0" + "revision" : "db0a7b47918ccfea0946d88424415c104f67c37d", + "version" : "187.0.0" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "36dc07cba4bc1e7e0c1d1fb679c3cd077694a072", - "version" : "5.0.0" + "revision" : "665b23dc656c9f787494620494f8e56098a900b2", + "version" : "5.1.1" } }, { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 46a84aeca7..0843f59a93 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -24,6 +24,7 @@ import NetworkExtension import Networking import PixelKit import Subscription +import WireGuard final class MacPacketTunnelProvider: PacketTunnelProvider { @@ -461,6 +462,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { tunnelHealthStore: tunnelHealthStore, controllerErrorStore: controllerErrorStore, snoozeTimingStore: NetworkProtectionSnoozeTimingStore(userDefaults: .netP), + wireGuardInterface: DefaultWireGuardInterface(), keychainType: Bundle.keychainType, tokenStore: tokenStore, debugEvents: debugEvents, @@ -626,3 +628,33 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { } } + +final class DefaultWireGuardInterface: WireGuardInterface { + func turnOn(settings: UnsafePointer, handle: Int32) -> Int32 { + wgTurnOn(settings, handle) + } + + func turnOff(handle: Int32) { + wgTurnOff(handle) + } + + func getConfig(handle: Int32) -> UnsafeMutablePointer? { + return wgGetConfig(handle) + } + + func setConfig(handle: Int32, config: String) -> Int64 { + return wgSetConfig(handle, config) + } + + func bumpSockets(handle: Int32) { + wgBumpSockets(handle) + } + + func disableSomeRoamingForBrokenMobileSemantics(handle: Int32) { + wgDisableSomeRoamingForBrokenMobileSemantics(handle) + } + + func setLogger(context: UnsafeMutableRawPointer?, logFunction: (@convention(c) (UnsafeMutableRawPointer?, Int32, UnsafePointer?) -> Void)?) { + wgSetLogger(context, logFunction) + } +} diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index c95a1baf87..3c05ff905c 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: "186.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "187.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index a77a119908..d34968d47d 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "186.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "187.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index e488e81990..9169d430dd 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "186.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "187.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From e50b5e45be6e23b0f1123bc8a1eb77174fc11e38 Mon Sep 17 00:00:00 2001 From: Federico Cappelli Date: Wed, 28 Aug 2024 12:34:44 +0100 Subject: [PATCH 03/14] Logging refactoring phase #2 (#3154) Task/Issue URL: https://app.asana.com/0/1205842942115003/1206750146033742/f Tech Design URL: https://app.asana.com/0/1200194497630846/1206777133329590/f **Description**: Now logging is aligned with our guideline: https://app.asana.com/0/1202500774821704/1208001254061393/f - all OSLog wrappers removed - all os_log uses removed and replaced with Logger - ability to disable logs from the app removed - Local and shared Logger improved --- DuckDuckGo.xcodeproj/project.pbxproj | 68 ++++--- .../xcshareddata/swiftpm/Package.resolved | 6 +- DuckDuckGo/Application/AppDelegate.swift | 10 +- DuckDuckGo/Application/DockCustomizer.swift | 3 +- DuckDuckGo/Application/URLEventHandler.swift | 6 +- .../Autoconsent/AutoconsentExperiment.swift | 7 +- .../Autoconsent/AutoconsentUserScript.swift | 35 ++-- DuckDuckGo/Bookmarks/Model/BookmarkList.swift | 9 +- .../Bookmarks/Model/BookmarkManager.swift | 23 +-- .../Model/BookmarkOutlineViewDataSource.swift | 7 +- .../Services/LocalBookmarkStore.swift | 9 +- .../View/BookmarksBarViewController.swift | 3 +- .../Extensions/FileManagerExtension.swift | 3 +- .../Extensions/NSTextFieldExtension.swift | 5 +- .../Common/Extensions/NSViewExtension.swift | 3 +- .../Common/Extensions/URLExtension.swift | 3 +- .../WKWebViewConfigurationExtensions.swift | 3 +- .../Extensions/WKWebViewExtension.swift | 3 +- DuckDuckGo/Common/Logger+Multiple.swift | 31 +++ DuckDuckGo/Common/Logging/Logging.swift | 181 ------------------ .../Common/Utilities/TabInstrumentation.swift | 8 +- .../Configuration/ConfigurationManager.swift | 22 +-- .../Configuration/ConfigurationStore.swift | 17 +- .../ContentBlocker/ContentBlocking.swift | 3 +- .../CrashReports/Model/CrashReporter.swift | 2 +- .../DBP/DataBrokerProtectionAppEvents.swift | 2 +- .../DBP/DataBrokerProtectionDebugMenu.swift | 11 +- .../DataBrokerProtectionFeatureDisabler.swift | 3 +- ...ataBrokerProtectionFeatureGatekeeper.swift | 7 +- ...taBrokerProtectionLoginItemInterface.swift | 2 +- .../DBP/LoginItem+DataBrokerProtection.swift | 4 +- .../Safari/SafariBookmarksReader.swift | 3 +- .../Safari/SafariFaviconsReader.swift | 3 +- .../Model/DataImportViewModel.swift | 32 ++-- .../DataImport/View/FileImportView.swift | 9 +- .../DeviceAuthenticator.swift | 23 +-- .../QuartzIdleStateProvider.swift | 3 +- DuckDuckGo/Favicons/Logger+Favicons.swift | 24 +++ .../Favicons/Model/FaviconImageCache.swift | 13 +- .../Model/FaviconReferenceCache.swift | 21 +- .../Favicons/Services/FaviconStore.swift | 9 +- .../Feedback/Model/FeedbackSender.swift | 3 +- .../FileDownload/Logger+FileDownload.swift | 24 +++ .../Model/DownloadListViewModel.swift | 3 +- .../Model/DownloadViewModel.swift | 3 +- .../Model/FileDownloadManager.swift | 28 ++- .../FileDownload/Model/FilePresenter.swift | 75 +++----- .../SecurityScopedFileURLController.swift | 14 +- .../Model/WebKitDownloadTask.swift | 96 +++++----- .../Services/DownloadListCoordinator.swift | 66 +++---- DuckDuckGo/Fire/Logger+Fire.swift | 24 +++ DuckDuckGo/Fire/Model/Fire.swift | 17 +- .../Fire/View/FirePopoverViewController.swift | 3 +- .../Fireproofing/Model/FireproofDomains.swift | 3 +- DuckDuckGo/LoginItems/LoginItemsManager.swift | 21 +- .../MainWindow/MainViewController.swift | 11 +- DuckDuckGo/Menus/HistoryMenu.swift | 3 +- DuckDuckGo/Menus/MainMenu.swift | 3 +- DuckDuckGo/Menus/MainMenuActions.swift | 25 ++- .../AddressBarButtonsViewController.swift | 9 +- .../View/AddressBarTextField.swift | 11 +- .../NavigationBar/View/MoreOptionsMenu.swift | 3 +- .../View/NavigationBarViewController.swift | 8 +- .../View/NavigationButtonMenuDelegate.swift | 5 +- .../LoginItem+NetworkProtection.swift | 10 +- .../NetworkProtectionAppEvents.swift | 2 +- .../NetworkProtectionDebugMenu.swift | 5 +- .../NetworkProtectionTunnelController.swift | 11 +- ...lerUDSClient+ConvenienceInitializers.swift | 2 +- .../VPNRedditSessionWorkaround.swift | 9 +- ...NetworkProtectionIPCTunnelController.swift | 9 +- .../MacPacketTunnelProvider.swift | 5 +- ...ore+SubscriptionTokenKeychainStorage.swift | 3 +- .../NetworkExtensionTargets/VPNLogger.swift | 118 ------------ .../Onboarding/OnboardingActionsManager.swift | 3 +- .../Bitwarden/Logger+BitWarden.swift | 24 +++ .../Bitwarden/Model/BWManager.swift | 85 ++++---- .../Bitwarden/Model/BWRequest.swift | 7 +- .../Bitwarden/Model/BWResponse.swift | 4 +- .../Bitwarden/Services/BWCommunicator.swift | 5 +- .../PasswordManagerCoordinator.swift | 3 +- .../Permissions/Model/PermissionManager.swift | 5 +- .../PinnedTabs/Model/PinnedTabsManager.swift | 7 +- .../Model/PinnedTabsViewModel.swift | 7 +- .../Model/AppearancePreferences.swift | 3 +- .../Model/DownloadsPreferences.swift | 4 +- .../Preferences/Model/SyncPreferences.swift | 5 +- .../View/PreferencesSyncView.swift | 3 +- .../View/PrivacyDashboardViewController.swift | 5 +- .../RemoteMessagingDebugMenu.swift | 1 + .../PasswordManagementViewController.swift | 3 +- .../View/SaveCredentialsViewController.swift | 15 +- .../View/SaveIdentityViewController.swift | 3 +- .../SavePaymentMethodViewController.swift | 3 +- .../SmarterEncryption/PrivacyFeatures.swift | 5 +- .../AppStateRestorationManager.swift | 5 +- .../WindowManager+StateRestoration.swift | 6 +- .../Statistics/ATB/StatisticsLoader.swift | 25 +-- .../Statistics/ATB/VariantManager.swift | 7 +- .../Model/SuggestionContainer.swift | 9 +- .../View/SuggestionTableCellView.swift | 3 +- .../SuggestionContainerViewModel.swift | 5 +- DuckDuckGo/Sync/SyncBookmarksAdapter.swift | 10 +- DuckDuckGo/Sync/SyncCredentialsAdapter.swift | 1 - DuckDuckGo/Sync/SyncErrorHandler.swift | 3 +- DuckDuckGo/Sync/SyncSettingsAdapter.swift | 1 - DuckDuckGo/Tab/Model/Tab.swift | 4 +- .../Tab/Model/TabExtensionsBuilder.swift | 3 +- .../Tab/Services/WebsiteDataStore.swift | 9 +- .../AdClickAttributionTabExtension.swift | 4 +- .../TabExtensions/TabSnapshotExtension.swift | 11 +- .../Tab/TabLazyLoader/TabLazyLoader.swift | 29 ++- .../Tab/UserScripts/DebugUserScript.swift | 3 +- .../Tab/View/BrowserTabViewController.swift | 7 +- .../TabBar/View/TabBarCollectionView.swift | 7 +- .../TabBar/View/TabBarViewController.swift | 21 +- .../ViewModel/TabCollectionViewModel.swift | 33 ++-- .../Model/ViewSnapshotRenderer.swift | 7 +- .../Model/WebViewSnapshotRenderer.swift | 7 +- .../Services/TabSnapshotStore.swift | 9 +- .../TabPreviewWindowController.swift | 3 +- .../LocalUnprotectedDomains.swift | 3 +- .../Updates/BinaryOwnershipChecker.swift | 7 +- DuckDuckGo/Updates/UpdateController.swift | 28 +-- .../Updates/UpdateNotificationPresenter.swift | 3 +- DuckDuckGo/Waitlist/IPCServiceLauncher.swift | 2 +- .../Waitlist/VPNFeatureGatekeeper.swift | 1 - DuckDuckGo/Waitlist/VPNUninstaller.swift | 6 +- .../View/WindowControllersManager.swift | 3 +- ...kDuckGoDBPBackgroundAgentAppDelegate.swift | 7 +- .../Logger+DBPBackgroundAgent.swift | 24 +++ .../DuckDuckGoNotificationsAppDelegate.swift | 25 +-- DuckDuckGoNotifications/Logging.swift | 49 ----- DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 16 +- .../TunnelControllerIPCService.swift | 2 +- DuckDuckGoVPN/VPNAppEventsHandler.swift | 3 +- .../AutoconsentIntegrationTests.swift | 7 +- .../DataBrokerProtection/Package.swift | 2 +- .../CCF/DataBrokerProtectionFeature.swift | 14 +- .../CCF/DataBrokerProtectionUtils.swift | 6 +- .../CCF/WebViewHandler.swift | 23 +-- .../DataBrokerProtectionDataManager.swift | 9 +- .../DataBrokerProtectionDatabase.swift | 53 ++--- .../DataBrokerRunCustomJSONViewModel.swift | 5 +- .../DebugUI/DebugScanJob.swift | 9 +- .../IPC/DataBrokerProtectionIPCClient.swift | 11 +- .../Model/DBPUIViewModel.swift | 3 +- .../Model/DataBroker.swift | 3 +- .../Operations/DataBrokerJobRunner.swift | 3 +- .../Operations/DataBrokerOperation.swift | 17 +- ...taBrokerProfileQueryOperationManager.swift | 25 +-- .../DataBrokerProtectionBrokerUpdater.swift | 11 +- .../OperationPreferredDateUpdater.swift | 7 +- .../Operations/OptOutJob.swift | 3 +- .../MismatchCalculator.swift | 5 +- .../Operations/ScanJob.swift | 7 +- ...DataBrokerProtectionEngagementPixels.swift | 5 +- .../DataBrokerProtectionEventPixels.swift | 5 +- .../DataBrokerProtectionAgentManager.swift | 17 +- ...rotectionBackgroundActivityScheduler.swift | 5 +- .../DataBrokerProtectionQueueManager.swift | 3 +- .../Services/CaptchaService.swift | 11 +- .../Services/EmailService.swift | 7 +- ...ProtectionDatabaseMigrationsProvider.swift | 3 +- ...DataBrokerProtectionKeyStoreProvider.swift | 13 +- .../UI/DBPUICommunicationLayer.swift | 29 +-- .../DataBrokerProtection/UI/UIMapper.swift | 5 +- ...kerProtectionUserNotificationService.swift | 7 +- .../DataBrokerProtectionAgentStopper.swift | 15 +- .../DataBrokerProtectionSleepObserver.swift | 9 +- .../Utils/Logger+DataBrokerProtection.swift | 31 +++ .../DataBrokerProtection/Utils/Logging.swift | 77 -------- ...DataBrokerProtectionStatsPixelsTests.swift | 1 - .../Sources/LoginItems/LoginItem.swift | 28 ++- .../NetworkProtectionMac/Package.swift | 2 +- .../FlowManagers/TCPFlowManager.swift | 4 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../Sources/UDSHelper/Logger+UDSHelper.swift | 24 +++ .../Sources/UDSHelper/UDSClient.swift | 24 +-- .../Sources/UDSHelper/UDSReceiver.swift | 26 +-- .../Sources/UDSHelper/UDSServer.swift | 35 ++-- Submodules/privacy-reference-tests | 2 +- .../BrokenSiteReportingReferenceTests.swift | 4 +- .../FireproofingReferenceTests.swift | 2 +- sandbox-test-tool/SandboxTestTool.swift | 56 +----- 185 files changed, 1166 insertions(+), 1391 deletions(-) create mode 100644 DuckDuckGo/Common/Logger+Multiple.swift delete mode 100644 DuckDuckGo/Common/Logging/Logging.swift create mode 100644 DuckDuckGo/Favicons/Logger+Favicons.swift create mode 100644 DuckDuckGo/FileDownload/Logger+FileDownload.swift create mode 100644 DuckDuckGo/Fire/Logger+Fire.swift delete mode 100644 DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/VPNLogger.swift create mode 100644 DuckDuckGo/PasswordManager/Bitwarden/Logger+BitWarden.swift create mode 100644 DuckDuckGoDBPBackgroundAgent/Logger+DBPBackgroundAgent.swift delete mode 100644 DuckDuckGoNotifications/Logging.swift create mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/Logger+DataBrokerProtection.swift delete mode 100644 LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/Logging.swift create mode 100644 LocalPackages/UDSHelper/Sources/UDSHelper/Logger+UDSHelper.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 81aeb0a5f0..35783aeca4 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -266,7 +266,7 @@ 3706FA83293F65D500E42796 /* LazyLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534C9F28113101002621E7 /* LazyLoadable.swift */; }; 3706FA85293F65D500E42796 /* KeyedCodingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0230C0A2272080090018F728 /* KeyedCodingExtension.swift */; }; 3706FA87293F65D500E42796 /* DownloadListStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B22F26E61D630031CB7F /* DownloadListStore.swift */; }; - 3706FA88293F65D500E42796 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; + 3706FA88293F65D500E42796 /* Logger+Multiple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logger+Multiple.swift */; }; 3706FA89293F65D500E42796 /* CrashReportPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC30A2D268F1EE300D2D9CD /* CrashReportPromptPresenter.swift */; }; 3706FA8B293F65D500E42796 /* PreferencesRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AFCE8627DA334800471A10 /* PreferencesRootView.swift */; }; 3706FA8C293F65D500E42796 /* AppStateChangedPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B684590725C9027900DC17B6 /* AppStateChangedPublisher.swift */; }; @@ -1147,10 +1147,8 @@ 4B2D06292A11C0C900DE1F49 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; 4B2D062A2A11C0C900DE1F49 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B2D062C2A11C0E100DE1F49 /* Networking in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2D062B2A11C0E100DE1F49 /* Networking */; }; - 4B2D065B2A11D1FF00DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC322A11B509001D9AC5 /* Logging.swift */; }; 4B2D065E2A11D2D700DE1F49 /* DuckDuckGo Notifications.app in Embed Login Items */ = {isa = PBXBuildFile; fileRef = 4B4BEC202A11B4E2001D9AC5 /* DuckDuckGo Notifications.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4B2D065F2A11D2D700DE1F49 /* DuckDuckGo VPN.app in Embed Login Items */ = {isa = PBXBuildFile; fileRef = 4B2D06392A11CFBB00DE1F49 /* DuckDuckGo VPN.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC322A11B509001D9AC5 /* Logging.swift */; }; 4B2D067F2A1334D700DE1F49 /* NetworkProtectionUI in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2D067E2A1334D700DE1F49 /* NetworkProtectionUI */; }; 4B2E7D6326FF9D6500D2DB17 /* PrintingUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2E7D6226FF9D6500D2DB17 /* PrintingUserScript.swift */; }; 4B2F565C2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2F565B2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift */; }; @@ -1181,7 +1179,6 @@ 4B44FEF32B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B44FEF22B1FEF5A000619D8 /* FocusableTextEditor.swift */; }; 4B44FEF42B1FEF5A000619D8 /* FocusableTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B44FEF22B1FEF5A000619D8 /* FocusableTextEditor.swift */; }; 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC382A11B509001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift */; }; - 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4BEC322A11B509001D9AC5 /* Logging.swift */; }; 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60762A0B29FA00BCD287 /* NetworkProtectionUNNotificationsPresenter.swift */; }; 4B4BEC422A11B5C7001D9AC5 /* NetworkProtectionOptionKeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605F2A0B29FA00BCD287 /* NetworkProtectionOptionKeyExtension.swift */; }; 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D605E2A0B29FA00BCD287 /* Bundle+VPN.swift */; }; @@ -1535,8 +1532,6 @@ 7B00997D2B6508B700FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */; }; 7B00997F2B6508C200FE7C31 /* NetworkProtectionProxy in Frameworks */ = {isa = PBXBuildFile; productRef = 7B00997E2B6508C200FE7C31 /* NetworkProtectionProxy */; }; 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */; }; - 7B01AC6E2C36BB7E004FADC7 /* VPNLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B01AC6D2C36BB7E004FADC7 /* VPNLogger.swift */; }; - 7B01AC6F2C36BB7E004FADC7 /* VPNLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B01AC6D2C36BB7E004FADC7 /* VPNLogger.swift */; }; 7B0694982B6E980F00FA4DBA /* VPNProxyLauncher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */; }; 7B09CBA92BA4BE8100CF245B /* NetworkProtectionPixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */; }; 7B09CBAA2BA4BE8200CF245B /* NetworkProtectionPixelEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */; }; @@ -1698,7 +1693,7 @@ 85774B002A713D3B00DE0561 /* BookmarksBarMenuFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85774AFE2A713D3B00DE0561 /* BookmarksBarMenuFactory.swift */; }; 85774B032A71CDD000DE0561 /* BlockMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85774B022A71CDD000DE0561 /* BlockMenuItem.swift */; }; 85774B042A71CDD000DE0561 /* BlockMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85774B022A71CDD000DE0561 /* BlockMenuItem.swift */; }; - 85799C1825DEBB3F0007EC87 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logging.swift */; }; + 85799C1825DEBB3F0007EC87 /* Logger+Multiple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85799C1725DEBB3F0007EC87 /* Logger+Multiple.swift */; }; 857E44642A9F70F200ED77A7 /* CampaignVariantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857E44612A9F6F3500ED77A7 /* CampaignVariantTests.swift */; }; 857E44652A9F70F300ED77A7 /* CampaignVariantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857E44612A9F6F3500ED77A7 /* CampaignVariantTests.swift */; }; 857E5AF52A79045800FC0FB4 /* PixelExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 857E5AF42A79045800FC0FB4 /* PixelExperiment.swift */; }; @@ -2697,6 +2692,7 @@ EEE50C2A2C38249C003DD7FF /* OptionalExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE50C282C38249C003DD7FF /* OptionalExtension.swift */; }; EEF12E6F2A2111880023E6BF /* MacPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */; }; EEF53E182950CED5002D78F4 /* JSAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEF53E172950CED5002D78F4 /* JSAlertViewModelTests.swift */; }; + F10C99422C7E20A1005568B4 /* Logger+DBPBackgroundAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17E7DDB2C7C7F8100907A84 /* Logger+DBPBackgroundAgent.swift */; }; F116A7C32BD1924B00F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C22BD1924B00F3FCF7 /* PixelKitTestingUtilities */; }; F116A7C72BD1925500F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C62BD1925500F3FCF7 /* PixelKitTestingUtilities */; }; F116A7C92BD1929000F3FCF7 /* PixelKitTestingUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = F116A7C82BD1929000F3FCF7 /* PixelKitTestingUtilities */; }; @@ -2706,6 +2702,13 @@ F118EA862BEACC7000F77634 /* NonStandardPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118EA842BEACC7000F77634 /* NonStandardPixel.swift */; }; F1476FC02C1359FB00EAE46A /* SubscriptionUIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1476FBF2C1359FB00EAE46A /* SubscriptionUIHandler.swift */; }; F1476FC12C1359FB00EAE46A /* SubscriptionUIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1476FBF2C1359FB00EAE46A /* SubscriptionUIHandler.swift */; }; + F17114822C7C98FB009836C1 /* Logger+Favicons.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17114812C7C98FB009836C1 /* Logger+Favicons.swift */; }; + F17114832C7C98FB009836C1 /* Logger+Favicons.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17114812C7C98FB009836C1 /* Logger+Favicons.swift */; }; + F17114852C7C9D28009836C1 /* Logger+Fire.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17114842C7C9D28009836C1 /* Logger+Fire.swift */; }; + F17114862C7C9D28009836C1 /* Logger+Fire.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17114842C7C9D28009836C1 /* Logger+Fire.swift */; }; + F17E7DDC2C7C7F8100907A84 /* Logger+DBPBackgroundAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17E7DDB2C7C7F8100907A84 /* Logger+DBPBackgroundAgent.swift */; }; + F17E7DDE2C7C83E500907A84 /* Logger+FileDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17E7DDD2C7C83E500907A84 /* Logger+FileDownload.swift */; }; + F17E7DDF2C7C83E500907A84 /* Logger+FileDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17E7DDD2C7C83E500907A84 /* Logger+FileDownload.swift */; }; F188267C2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */; }; F188267D2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */; }; F18826802BBEB58100D9AC4F /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; @@ -2750,6 +2753,9 @@ F1C70D812BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; F1C70D822BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; F1C70D832BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */; }; + F1CA67062C7DCA2300264E6A /* Logger+BitWarden.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1CA67052C7DCA2300264E6A /* Logger+BitWarden.swift */; }; + F1CA67072C7DCA2D00264E6A /* Logger+BitWarden.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1CA67052C7DCA2300264E6A /* Logger+BitWarden.swift */; }; + F1CA67082C7DE92200264E6A /* Logger+FileDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17E7DDD2C7C83E500907A84 /* Logger+FileDownload.swift */; }; F1D0428E2BFB9F9C00A31506 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1D0428D2BFB9F9C00A31506 /* Subscription */; }; F1D042902BFB9FA300A31506 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = F1D0428F2BFB9FA300A31506 /* Subscription */; }; F1D042942BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */; }; @@ -3294,7 +3300,6 @@ 4B44FEF22B1FEF5A000619D8 /* FocusableTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextEditor.swift; sourceTree = ""; }; 4B4BEC182A11B3EA001D9AC5 /* DuckDuckGoNotifications.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGoNotifications.xcconfig; sourceTree = ""; }; 4B4BEC202A11B4E2001D9AC5 /* DuckDuckGo Notifications.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DuckDuckGo Notifications.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 4B4BEC322A11B509001D9AC5 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; 4B4BEC332A11B509001D9AC5 /* DuckDuckGoNotifications.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoNotifications.entitlements; sourceTree = ""; }; 4B4BEC342A11B509001D9AC5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 4B4BEC382A11B509001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckDuckGoNotificationsAppDelegate.swift; sourceTree = ""; }; @@ -3540,7 +3545,6 @@ 56D145F029E6F06D00E3488A /* MockBookmarkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBookmarkManager.swift; sourceTree = ""; }; 56D6A3D529DB2BAB0055215A /* ContinueSetUpView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContinueSetUpView.swift; sourceTree = ""; }; 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTransparentProxyProvider.swift; sourceTree = ""; }; - 7B01AC6D2C36BB7E004FADC7 /* VPNLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNLogger.swift; sourceTree = ""; }; 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkProtectionOnboardingMenu.swift; sourceTree = ""; }; 7B0694972B6E980F00FA4DBA /* VPNProxyLauncher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNProxyLauncher.swift; sourceTree = ""; }; 7B09CBA72BA4BE7000CF245B /* NetworkProtectionPixelEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionPixelEventTests.swift; sourceTree = ""; }; @@ -3646,7 +3650,7 @@ 85707F30276A7DCA00DC0649 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = ""; }; 85774AFE2A713D3B00DE0561 /* BookmarksBarMenuFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarMenuFactory.swift; sourceTree = ""; }; 85774B022A71CDD000DE0561 /* BlockMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockMenuItem.swift; sourceTree = ""; }; - 85799C1725DEBB3F0007EC87 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; + 85799C1725DEBB3F0007EC87 /* Logger+Multiple.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Logger+Multiple.swift"; sourceTree = ""; }; 857E44612A9F6F3500ED77A7 /* CampaignVariantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CampaignVariantTests.swift; sourceTree = ""; }; 857E5AF42A79045800FC0FB4 /* PixelExperiment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelExperiment.swift; sourceTree = ""; }; 857E5AF82A79618100FC0FB4 /* PixelExperimentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelExperimentTests.swift; sourceTree = ""; }; @@ -4329,6 +4333,10 @@ F118EA7C2BEA2B8700F77634 /* DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DefaultSubscriptionFeatureAvailability+DefaultInitializer.swift"; sourceTree = ""; }; F118EA842BEACC7000F77634 /* NonStandardPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonStandardPixel.swift; sourceTree = ""; }; F1476FBF2C1359FB00EAE46A /* SubscriptionUIHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionUIHandler.swift; sourceTree = ""; }; + F17114812C7C98FB009836C1 /* Logger+Favicons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Favicons.swift"; sourceTree = ""; }; + F17114842C7C9D28009836C1 /* Logger+Fire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Fire.swift"; sourceTree = ""; }; + F17E7DDB2C7C7F8100907A84 /* Logger+DBPBackgroundAgent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+DBPBackgroundAgent.swift"; sourceTree = ""; }; + F17E7DDD2C7C83E500907A84 /* Logger+FileDownload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+FileDownload.swift"; sourceTree = ""; }; F188267B2BBEB3AA00D9AC4F /* GeneralPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPixel.swift; sourceTree = ""; }; F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyProPixel.swift; sourceTree = ""; }; F18826832BBEE31700D9AC4F /* PixelKit+Assertion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PixelKit+Assertion.swift"; sourceTree = ""; }; @@ -4340,6 +4348,7 @@ F1C5763D2BFF972900C78647 /* SubscriptionUIHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionUIHandling.swift; sourceTree = ""; }; F1C70D782BFF50A400599292 /* DataBrokerProtectionLoginItemInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemInterface.swift; sourceTree = ""; }; F1C70D7B2BFF510000599292 /* SubscriptionEnvironment+Default.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SubscriptionEnvironment+Default.swift"; sourceTree = ""; }; + F1CA67052C7DCA2300264E6A /* Logger+BitWarden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+BitWarden.swift"; sourceTree = ""; }; F1D042932BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataBrokerProtectionSettings+Environment.swift"; sourceTree = ""; }; F1D042982BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SubscriptionManager+StandardConfiguration.swift"; sourceTree = ""; }; F1D43AED2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MainMenuActions+VanillaBrowser.swift"; sourceTree = ""; }; @@ -4752,14 +4761,6 @@ path = Services; sourceTree = ""; }; - 1D43EAFF291D7D280065E5D6 /* Logging */ = { - isa = PBXGroup; - children = ( - 85799C1725DEBB3F0007EC87 /* Logging.swift */, - ); - path = Logging; - sourceTree = ""; - }; 1D72D5902BFF361700AEDE36 /* Updates */ = { isa = PBXGroup; children = ( @@ -5389,7 +5390,6 @@ children = ( 7B5291882A1697680022E406 /* Info.plist */, 4B4BEC382A11B509001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift */, - 4B4BEC322A11B509001D9AC5 /* Logging.swift */, 4B4BEC342A11B509001D9AC5 /* Assets.xcassets */, 4B4BEC332A11B509001D9AC5 /* DuckDuckGoNotifications.entitlements */, ); @@ -5528,7 +5528,6 @@ EEF12E6D2A2111880023E6BF /* MacPacketTunnelProvider.swift */, 7B0099802B65C6B300FE7C31 /* MacTransparentProxyProvider.swift */, EE66418B2B9B1981005BCD17 /* NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift */, - 7B01AC6D2C36BB7E004FADC7 /* VPNLogger.swift */, ); path = NetworkExtensionTargets; sourceTree = ""; @@ -6311,6 +6310,7 @@ 8556A60C256C15C60092FA9D /* FileDownload */ = { isa = PBXGroup; children = ( + F17E7DDD2C7C83E500907A84 /* Logger+FileDownload.swift */, B6B1E87C26D5DA020062C350 /* View */, B61EF3EA266F91D700B4D78F /* Extensions */, 8556A615256C15E10092FA9D /* Model */, @@ -6662,6 +6662,7 @@ isa = PBXGroup; children = ( 9D9AE9152AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift */, + F17E7DDB2C7C7F8100907A84 /* Logger+DBPBackgroundAgent.swift */, 9D9AE9172AAA3B450026E7DC /* UserText.swift */, 31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */, 9D9AE9162AAA3B450026E7DC /* Assets.xcassets */, @@ -7077,6 +7078,7 @@ AA5FA698275F90CD00DCE9C9 /* Model */, AA5FA69B275F944500DCE9C9 /* Services */, 4BBD3BFF285ACE090047A89D /* NSNotificationName+Favicons.swift */, + F17114812C7C98FB009836C1 /* Logger+Favicons.swift */, ); path = Favicons; sourceTree = ""; @@ -7173,6 +7175,7 @@ AA6820E825503A21005ED0D5 /* Fire */ = { isa = PBXGroup; children = ( + F17114842C7C9D28009836C1 /* Logger+Fire.swift */, AAFCB38325E546FF00859DD4 /* View */, AA6820EF25503D93005ED0D5 /* ViewModel */, AA6820E925503A49005ED0D5 /* Model */, @@ -7223,6 +7226,7 @@ 4BBDEE8D28FC14760092FAA6 /* View */, 4BBDEE8B28FC14760092FAA6 /* Model */, 1D43EAFE291D29E40065E5D6 /* Services */, + F1CA67052C7DCA2300264E6A /* Logger+BitWarden.swift */, ); path = Bitwarden; sourceTree = ""; @@ -7314,7 +7318,6 @@ AADC60E92493B305008F8EF7 /* Extensions */, 4BA1A691258B06F600F6F690 /* FileSystem */, AA80EC52256BE33A007083E7 /* Localizables */, - 1D43EAFF291D7D280065E5D6 /* Logging */, 85AC3B3325DA828900C7D2AA /* Network */, 1D8057C62A83CAD500F4FED6 /* OsVersion */, 4BB88B4E25B7BA20006F6B06 /* Utilities */, @@ -7322,6 +7325,7 @@ AA86491424D831C4001BABEE /* View */, 4B37EE5B2B4CFC3C00A89A61 /* Surveys */, B65E5DAE2B74DE6D00480415 /* TrackerNetwork.swift */, + 85799C1725DEBB3F0007EC87 /* Logger+Multiple.swift */, ); path = Common; sourceTree = ""; @@ -9976,7 +9980,7 @@ 3706FA85293F65D500E42796 /* KeyedCodingExtension.swift in Sources */, 3706FA87293F65D500E42796 /* DownloadListStore.swift in Sources */, 37197EAB2942443D00394917 /* WebViewContainerView.swift in Sources */, - 3706FA88293F65D500E42796 /* Logging.swift in Sources */, + 3706FA88293F65D500E42796 /* Logger+Multiple.swift in Sources */, 3706FA89293F65D500E42796 /* CrashReportPromptPresenter.swift in Sources */, 3706FA8B293F65D500E42796 /* PreferencesRootView.swift in Sources */, 3706FA8C293F65D500E42796 /* AppStateChangedPublisher.swift in Sources */, @@ -10304,6 +10308,7 @@ 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, 3707C72A294B5D2900682A9F /* URLExtension.swift in Sources */, 3706FB76293F65D500E42796 /* ASN1Parser.swift in Sources */, + F17E7DDF2C7C83E500907A84 /* Logger+FileDownload.swift in Sources */, 9F56CFAA2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */, 37FD78122A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */, 987799F42999993C005D8EB6 /* LegacyBookmarksStoreMigration.swift in Sources */, @@ -10678,11 +10683,13 @@ 7BEC20432B0F505F00243D3E /* AddBookmarkPopoverView.swift in Sources */, 3706FC5B293F65D500E42796 /* NSOutlineViewExtensions.swift in Sources */, 3706FC5C293F65D500E42796 /* AppDelegate.swift in Sources */, + F1CA67072C7DCA2D00264E6A /* Logger+BitWarden.swift in Sources */, 31EF1E842B63FFD100E6DB17 /* DataBrokerProtectionDebugMenu.swift in Sources */, 3706FC5D293F65D500E42796 /* ContentOverlayViewController.swift in Sources */, 3706FC5E293F65D500E42796 /* OnboardingViewController.swift in Sources */, 7BE146082A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */, 3706FC5F293F65D500E42796 /* DeviceAuthenticator.swift in Sources */, + F17114832C7C98FB009836C1 /* Logger+Favicons.swift in Sources */, B6DE57F72B05EA9000CD54B9 /* SheetHostingWindow.swift in Sources */, 3706FEB9293F6EFF00E42796 /* BWVault.swift in Sources */, B6B5F5852B03580A008DB58A /* RequestFilePermissionView.swift in Sources */, @@ -10726,6 +10733,7 @@ 3706FC7D293F65D500E42796 /* NSMenuExtension.swift in Sources */, 3701C9CF29BD040C00305B15 /* FirefoxBerkeleyDatabaseReader.swift in Sources */, B65E5DB02B74E6A900480415 /* TrackerNetwork.swift in Sources */, + F17114862C7C9D28009836C1 /* Logger+Fire.swift in Sources */, 3706FC7E293F65D500E42796 /* MainWindowController.swift in Sources */, 3706FC7F293F65D500E42796 /* Tab.swift in Sources */, 3706FC81293F65D500E42796 /* DispatchQueueExtensions.swift in Sources */, @@ -11197,7 +11205,6 @@ B602E8192A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, F1FDC93B2BF51F41006B1435 /* VPNSettings+Environment.swift in Sources */, 4BF0E50B2AD2552200FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, - 7B01AC6F2C36BB7E004FADC7 /* VPNLogger.swift in Sources */, 4B41EDA12B15437A001EEDF4 /* NetworkProtectionNotificationsPresenterFactory.swift in Sources */, 7B0099822B65C6B300FE7C31 /* MacTransparentProxyProvider.swift in Sources */, B65DA5F32A77D3C700CBEE8D /* UserDefaultsWrapper.swift in Sources */, @@ -11215,7 +11222,6 @@ buildActionMask = 2147483647; files = ( B6F92BA22A691580002ABA6B /* UserDefaultsWrapper.swift in Sources */, - 4B2D065B2A11D1FF00DE1F49 /* Logging.swift in Sources */, 7BA7CC3A2AD11E2D0042E5CE /* DuckDuckGoVPNAppDelegate.swift in Sources */, 7BAF9E4C2A8A3CCA002D3B6E /* UserDefaults+NetworkProtectionShared.swift in Sources */, 7BA7CC592AD1203B0042E5CE /* UserText+NetworkProtection.swift in Sources */, @@ -11258,7 +11264,6 @@ 7B2DDCFB2A93B25F0039D884 /* KeychainType+ClientDefault.swift in Sources */, B6F92BA32A691583002ABA6B /* UserDefaultsWrapper.swift in Sources */, 4BA7C4DB2B3F63AE00AFE511 /* NetworkExtensionController.swift in Sources */, - 4B2D067C2A13340900DE1F49 /* Logging.swift in Sources */, 7B1459552B7D438F00047F2C /* VPNProxyLauncher.swift in Sources */, EEDE50122BA360C80017F3C4 /* NetworkProtection+VPNAgentConvenienceInitializers.swift in Sources */, 7BD7B0042C19D3830039D20A /* VPNIPCResources.swift in Sources */, @@ -11297,7 +11302,6 @@ buildActionMask = 2147483647; files = ( 4B4BEC3D2A11B56B001D9AC5 /* DuckDuckGoNotificationsAppDelegate.swift in Sources */, - 4B4BEC3E2A11B56E001D9AC5 /* Logging.swift in Sources */, 4B4BEC412A11B5BD001D9AC5 /* NetworkProtectionUNNotificationsPresenter.swift in Sources */, 4B4BEC432A11B5C7001D9AC5 /* Bundle+VPN.swift in Sources */, 4B4BEC452A11B5EE001D9AC5 /* UserText+NetworkProtectionExtensions.swift in Sources */, @@ -11324,7 +11328,6 @@ F1C70D7E2BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, 4B4D60A02A0B2D5B00BCD287 /* Bundle+VPN.swift in Sources */, F1DA51922BF6081C00CF29FA /* AttributionPixelHandler.swift in Sources */, - 7B01AC6E2C36BB7E004FADC7 /* VPNLogger.swift in Sources */, 4B4D60A52A0B2EC000BCD287 /* UserText+NetworkProtectionExtensions.swift in Sources */, 4BF0E50C2AD2552300FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */, B65DA5F22A77D3C600CBEE8D /* UserDefaultsWrapper.swift in Sources */, @@ -11378,6 +11381,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F17E7DDC2C7C7F8100907A84 /* Logger+DBPBackgroundAgent.swift in Sources */, 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, F1D042942BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, F1C70D822BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, @@ -11393,6 +11397,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F10C99422C7E20A1005568B4 /* Logger+DBPBackgroundAgent.swift in Sources */, 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, F1D042952BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, F1C70D832BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, @@ -11429,7 +11434,7 @@ B6E3E55B2BC0041900A41922 /* DownloadListStoreMock.swift in Sources */, 9FA5A0A52BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */, B6C0B23026E61D630031CB7F /* DownloadListStore.swift in Sources */, - 85799C1825DEBB3F0007EC87 /* Logging.swift in Sources */, + 85799C1825DEBB3F0007EC87 /* Logger+Multiple.swift in Sources */, AAC30A2E268F1EE300D2D9CD /* CrashReportPromptPresenter.swift in Sources */, 1D2DC00629016798008083A1 /* BWCredential.swift in Sources */, EEA3EEB12B24EBD000E8333A /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */, @@ -11892,6 +11897,7 @@ 1D69C553291302F200B75945 /* BWVault.swift in Sources */, AA6FFB4424DC33320028F4D0 /* NSViewExtension.swift in Sources */, B6C0B23E26E8BF1F0031CB7F /* DownloadListViewModel.swift in Sources */, + F17114822C7C98FB009836C1 /* Logger+Favicons.swift in Sources */, 1D9A37672BD8EA8800EBC58D /* DockPositionProvider.swift in Sources */, 4B9292D52667123700AD2C21 /* BookmarkManagementDetailViewController.swift in Sources */, F188267C2BBEB3AA00D9AC4F /* GeneralPixel.swift in Sources */, @@ -12045,6 +12051,7 @@ 3158B14A2B0BF74300AF130C /* DataBrokerProtectionDebugMenu.swift in Sources */, 4BBDEE9128FC14760092FAA6 /* BWInstallationService.swift in Sources */, 7B4D8A212BDA857300852966 /* VPNOperationErrorRecorder.swift in Sources */, + F17E7DDE2C7C83E500907A84 /* Logger+FileDownload.swift in Sources */, 859F30642A72A7BB00C20372 /* BookmarksBarPromptPopover.swift in Sources */, 7B6545ED2C0778BB00115BEA /* VPNControllerUDSClient+ConvenienceInitializers.swift in Sources */, BDBA85902C5D252A00BC54F5 /* VPNFeedbackSender.swift in Sources */, @@ -12146,6 +12153,7 @@ AABEE69C24A902BB0043105B /* SuggestionContainer.swift in Sources */, B6C00ECD292F89D9009C73A6 /* FindInPageTabExtension.swift in Sources */, 85589E8327BBB8630038AD11 /* HomePageViewController.swift in Sources */, + F17114852C7C9D28009836C1 /* Logger+Fire.swift in Sources */, 5614B3A12BBD639D009B5031 /* ZoomPopover.swift in Sources */, B6A9E46B2614618A0067D1B9 /* OperatingSystemVersionExtension.swift in Sources */, 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */, @@ -12220,6 +12228,7 @@ 4B92928B26670D1700AD2C21 /* BookmarksOutlineView.swift in Sources */, 4BF01C00272AE74C00884A61 /* CountryList.swift in Sources */, 37CD54CC27F2FDD100F1F7B9 /* PreferencesSection.swift in Sources */, + F1CA67062C7DCA2300264E6A /* Logger+BitWarden.swift in Sources */, 4B4D60B62A0C847D00BCD287 /* NetworkProtectionNavBarButtonModel.swift in Sources */, FD23FD2D2886A81D007F6985 /* AutoconsentManagement.swift in Sources */, 4B4D60D32A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */, @@ -12566,6 +12575,7 @@ B6E6BA142BA2CDD6008AA7E1 /* SandboxTestToolNotifications.swift in Sources */, B6E6BA042BA1FE05008AA7E1 /* FilePresenter.swift in Sources */, B6E6B9F62BA1FD90008AA7E1 /* SandboxTestTool.swift in Sources */, + F1CA67082C7DE92200264E6A /* Logger+FileDownload.swift in Sources */, B6E6BA252BA2EDDE008AA7E1 /* FileReadResult.swift in Sources */, B6EECB322BC40A1400B3CB77 /* FileManagerExtension.swift in Sources */, B6EECB302BC3FA5A00B3CB77 /* SecurityScopedFileURLController.swift in Sources */, @@ -13552,7 +13562,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 187.0.0; + version = 188.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3423380f00..0898a2acca 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "db0a7b47918ccfea0946d88424415c104f67c37d", - "version" : "187.0.0" + "revision" : "faf25f57d1d61ff855216178c454616031585c07", + "version" : "188.0.0" } }, { @@ -75,7 +75,7 @@ { "identity" : "lottie-spm", "kind" : "remoteSourceControl", - "location" : "https://github.com/airbnb/lottie-spm", + "location" : "https://github.com/airbnb/lottie-spm.git", "state" : { "revision" : "1d29eccc24cc8b75bff9f6804155112c0ffc9605", "version" : "4.4.3" diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 567082b8b1..3699ba82d2 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -39,6 +39,7 @@ import Subscription import NetworkProtectionIPC import DataBrokerProtection import RemoteMessaging +import os.log final class AppDelegate: NSObject, NSApplicationDelegate { @@ -65,7 +66,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let fileStore: FileStore #if APPSTORE - private let crashCollection = CrashCollection(platform: .macOSAppStore, log: .default) + private let crashCollection = CrashCollection(platform: .macOSAppStore) #else private let crashReporter = CrashReporter() #endif @@ -164,7 +165,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let encryptionKey = NSApplication.runType.requiresEnvironment ? try keyStore.readKey() : nil fileStore = EncryptedFileStore(encryptionKey: encryptionKey) } catch { - os_log("App Encryption Key could not be read: %s", "\(error)") + Logger.general.error("App Encryption Key could not be read: \(error.localizedDescription)") fileStore = EncryptedFileStore() } @@ -545,7 +546,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { dataProvidersSource: syncDataProviders, errorEvents: SyncErrorHandler(), privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, - log: OSLog.sync, environment: environment ) syncService.initializeIfNeeded() @@ -620,10 +620,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { return } if isLocked { - os_log(.debug, log: .sync, "Screen is locked") + Logger.sync.debug("Screen is locked") syncService.scheduler.cancelSyncAndSuspendSyncQueue() } else { - os_log(.debug, log: .sync, "Screen is unlocked") + Logger.sync.debug("Screen is unlocked") syncService.scheduler.resumeSyncQueue() } } diff --git a/DuckDuckGo/Application/DockCustomizer.swift b/DuckDuckGo/Application/DockCustomizer.swift index 61391fc34c..f2baf5d894 100644 --- a/DuckDuckGo/Application/DockCustomizer.swift +++ b/DuckDuckGo/Application/DockCustomizer.swift @@ -18,6 +18,7 @@ import Foundation import Common +import os.log protocol DockCustomization { var isAddedToDock: Bool { get } @@ -104,7 +105,7 @@ final class DockCustomizer: DockCustomization { } return true } catch { - os_log(.error, "Error writing to Dock plist: %{public}@", error.localizedDescription) + Logger.general.error("Error writing to Dock plist: \(error.localizedDescription, privacy: .public)") return false } } diff --git a/DuckDuckGo/Application/URLEventHandler.swift b/DuckDuckGo/Application/URLEventHandler.swift index d78142b001..3883e3bee4 100644 --- a/DuckDuckGo/Application/URLEventHandler.swift +++ b/DuckDuckGo/Application/URLEventHandler.swift @@ -24,6 +24,8 @@ import Subscription import NetworkProtectionUI import VPNAppLauncher import DataBrokerProtection +import os.log +import BrowserServicesKit // @MainActor final class URLEventHandler { @@ -64,14 +66,14 @@ final class URLEventHandler { @objc func handleUrlEvent(event: NSAppleEventDescriptor, reply: NSAppleEventDescriptor) { guard let stringValue = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue else { - os_log("UrlEventListener: unable to determine path", type: .error) + Logger.general.error("UrlEventListener: unable to determine path") let error = NSError(domain: "CouldNotGetPath", code: -1, userInfo: nil) PixelKit.fire(DebugEvent(GeneralPixel.appOpenURLFailed, error: error)) return } guard let url = URL.makeURL(from: stringValue) else { - os_log("UrlEventListener: failed to construct URL from path %s", type: .error, stringValue) + Logger.general.debug("UrlEventListener: failed to construct URL from path \(stringValue)") let error = NSError(domain: "CouldNotConstructURL", code: -1, userInfo: nil) PixelKit.fire(DebugEvent(GeneralPixel.appOpenURLFailed, error: error)) return diff --git a/DuckDuckGo/Autoconsent/AutoconsentExperiment.swift b/DuckDuckGo/Autoconsent/AutoconsentExperiment.swift index bdd1c01746..e90e350fad 100644 --- a/DuckDuckGo/Autoconsent/AutoconsentExperiment.swift +++ b/DuckDuckGo/Autoconsent/AutoconsentExperiment.swift @@ -18,11 +18,12 @@ import Foundation import Common +import os.log enum AutoconsentFilterlistExperiment: String, CaseIterable { static var logic = AutoconsentExperimentLogic() static var cohort: AutoconsentFilterlistExperiment? { - os_log("🚧 requesting CPM cohort", log: .autoconsent, type: .debug) + Logger.autoconsent.debug("🚧 requesting CPM cohort") return logic.experimentCohort } @@ -36,11 +37,11 @@ final internal class AutoconsentExperimentLogic { // if the stored cohort doesn't match, allocate a new one let cohort = AutoconsentFilterlistExperiment(rawValue: allocatedExperimentCohort) { - os_log("🚧 existing CPM cohort: %s", log: .autoconsent, type: .debug, String(describing: cohort.rawValue)) + Logger.autoconsent.debug("🚧 existing CPM cohort: \(String(describing: cohort.rawValue))") return cohort } let cohort = AutoconsentFilterlistExperiment.allCases.randomElement()! - os_log("🚧 new CPM cohort: %s", log: .autoconsent, type: .debug, String(describing: cohort.rawValue)) + Logger.autoconsent.debug("🚧 new CPM cohort: \(String(describing: cohort.rawValue))") allocatedExperimentCohort = cohort.rawValue return cohort } diff --git a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift index 61a9624319..38a15b5b97 100644 --- a/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift +++ b/DuckDuckGo/Autoconsent/AutoconsentUserScript.swift @@ -21,6 +21,7 @@ import BrowserServicesKit import Common import UserScript import PrivacyDashboard +import os.log protocol AutoconsentUserScriptDelegate: AnyObject { func autoconsentUserScript(consentStatus: CookieConsentInfo) @@ -50,7 +51,7 @@ final class AutoconsentUserScript: NSObject, WKScriptMessageHandlerWithReply, Us weak var delegate: AutoconsentUserScriptDelegate? init(scriptSource: ScriptSourceProviding, config: PrivacyConfiguration) { - os_log("Initialising autoconsent userscript", log: .autoconsent, type: .debug) + Logger.autoconsent.debug("Initialising autoconsent userscript") source = Self.loadJS("autoconsent-bundle", from: .main, withReplacements: [:]) self.config = config } @@ -65,7 +66,7 @@ final class AutoconsentUserScript: NSObject, WKScriptMessageHandlerWithReply, Us let consentStatus = CookieConsentInfo( consentManaged: consentManaged, cosmetic: cosmetic, optoutFailed: optoutFailed, selftestFailed: selftestFailed ) - os_log("Refreshing dashboard state: %s", log: .autoconsent, type: .debug, String(describing: consentStatus)) + Logger.autoconsent.debug("Refreshing dashboard state: \(String(describing: consentStatus))") self.delegate?.autoconsentUserScript(consentStatus: consentStatus) } @@ -73,7 +74,7 @@ final class AutoconsentUserScript: NSObject, WKScriptMessageHandlerWithReply, Us func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) { - os_log("Message received: %s", log: .autoconsent, type: .debug, String(describing: message.body)) + Logger.autoconsent.debug("Message received: \(String(describing: message.body))") return handleMessage(replyHandler: replyHandler, message: message) } } @@ -149,7 +150,7 @@ extension AutoconsentUserScript { let json = try JSONSerialization.data(withJSONObject: message) return try JSONDecoder().decode(Target.self, from: json) } catch { - os_log(.error, "Error decoding message body: %{public}@", error.localizedDescription) + Logger.autoconsent.error("Error decoding message body: \(error.localizedDescription, privacy: .public)") return nil } } @@ -170,13 +171,13 @@ extension AutoconsentUserScript { case MessageName.eval: handleEval(message: message, replyHandler: replyHandler) case MessageName.popupFound: - os_log("Autoconsent popup found", log: .autoconsent) + Logger.autoconsent.debug("Autoconsent popup found") replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection case MessageName.optOutResult: handleOptOutResult(message: message, replyHandler: replyHandler) case MessageName.optInResult: // this is not supported in browser - os_log("ignoring optInResult: %s", log: .autoconsent, type: .debug, String(describing: message.body)) + Logger.autoconsent.debug("ignoring optInResult: \(String(describing: message.body))") replyHandler(nil, "opt-in is not supported") case MessageName.cmpDetected: // no need to do anything here @@ -186,7 +187,7 @@ extension AutoconsentUserScript { case MessageName.autoconsentDone: handleAutoconsentDone(message: message, replyHandler: replyHandler) case MessageName.autoconsentError: - os_log("Autoconsent error: %s", log: .autoconsent, String(describing: message.body)) + Logger.autoconsent.debug("Autoconsent error: \(String(describing: message.body))") replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection } } @@ -204,7 +205,7 @@ extension AutoconsentUserScript { guard url.navigationalScheme?.isHypertextScheme == true else { // ignore special schemes - os_log("Ignoring special URL scheme: %s", log: .autoconsent, type: .debug, messageData.url) + Logger.autoconsent.debug("Ignoring special URL scheme: \(messageData.url)") replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection return } @@ -217,7 +218,7 @@ extension AutoconsentUserScript { let topURLDomain = message.webView?.url?.host guard config.isFeature(.autoconsent, enabledForDomain: topURLDomain) else { - os_log("disabled for site: %s", log: .autoconsent, type: .info, String(describing: url.absoluteString)) + Logger.autoconsent.info("disabled for site: \(String(describing: url.absoluteString))") replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection return } @@ -297,7 +298,7 @@ extension AutoconsentUserScript { replyHandler(nil, "cannot decode message") return } - os_log("opt-out result: %s", log: .autoconsent, type: .debug, String(describing: messageData)) + Logger.autoconsent.debug("opt-out result: \(String(describing: messageData))") if !messageData.result { refreshDashboardState(consentManaged: true, cosmetic: nil, optoutFailed: true, selftestFailed: nil) @@ -317,7 +318,7 @@ extension AutoconsentUserScript { replyHandler(nil, "cannot decode message") return } - os_log("opt-out successful: %s", log: .autoconsent, type: .debug, String(describing: messageData)) + Logger.autoconsent.debug("opt-out successful: \(String(describing: messageData))") guard let url = URL(string: messageData.url), let host = url.host else { @@ -329,7 +330,7 @@ extension AutoconsentUserScript { // trigger popup once per domain if !management.sitesNotifiedCache.contains(host) { - os_log("bragging that we closed a popup", log: .autoconsent, type: .debug) + Logger.autoconsent.debug("bragging that we closed a popup") management.sitesNotifiedCache.insert(host) // post popover notification on main thread DispatchQueue.main.async { @@ -344,7 +345,7 @@ extension AutoconsentUserScript { if let selfTestWebView = selfTestWebView, let selfTestFrameInfo = selfTestFrameInfo { - os_log("requesting self-test in: %s", log: .autoconsent, type: .debug, messageData.url) + Logger.autoconsent.debug("requesting self-test in: \(messageData.url)") selfTestWebView.evaluateJavaScript( "window.autoconsentMessageCallback({ type: 'selfTest' })", in: selfTestFrameInfo, @@ -352,14 +353,14 @@ extension AutoconsentUserScript { completionHandler: { (result) in switch result { case.failure(let error): - os_log("Error running self-test: %s", log: .autoconsent, type: .debug, String(describing: error)) + Logger.autoconsent.error("Error running self-test: \(error.localizedDescription, privacy: .public)") case.success: - os_log("self-test requested", log: .autoconsent, type: .debug) + Logger.autoconsent.debug("self-test requested") } } ) } else { - os_log("no self-test scheduled in this tab", log: .autoconsent, type: .debug) + Logger.autoconsent.debug("no self-test scheduled in this tab") } selfTestWebView = nil selfTestFrameInfo = nil @@ -372,7 +373,7 @@ extension AutoconsentUserScript { return } // store self-test result - os_log("self-test result: %s", log: .autoconsent, type: .debug, String(describing: messageData)) + Logger.autoconsent.debug("self-test result: \(String(describing: messageData))") refreshDashboardState(consentManaged: true, cosmetic: nil, optoutFailed: false, selftestFailed: messageData.result) replyHandler([ "type": "ok" ], nil) // this is just to prevent a Promise rejection } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift index dcae056327..61edd92a17 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkList.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkList.swift @@ -20,6 +20,7 @@ import Foundation import BrowserServicesKit import Common import Suggestions +import os.log struct BookmarkList { @@ -79,7 +80,7 @@ struct BookmarkList { mutating func insert(_ bookmark: Bookmark) { guard itemsDict[bookmark.url] == nil else { - os_log("BookmarkList: Adding failed, the item already is in the bookmark list", type: .error) + Logger.bookmarks.error("BookmarkList: Adding failed, the item already is in the bookmark list") return } @@ -108,7 +109,7 @@ struct BookmarkList { guard !newBookmark.isFolder else { return } guard itemsDict[newBookmark.url] != nil else { - os_log("BookmarkList: Update failed, no such item in bookmark list") + Logger.bookmarks.debug("BookmarkList: Update failed, no such item in bookmark list") return } @@ -136,7 +137,7 @@ struct BookmarkList { guard !bookmark.isFolder else { return nil } guard itemsDict[newURL] == nil else { - os_log("BookmarkList: Update failed, new url already in bookmark list") + Logger.bookmarks.debug("BookmarkList: Update failed, new url already in bookmark list") return nil } @@ -154,7 +155,7 @@ private extension BookmarkList { mutating private func updateBookmarkList(newBookmark: Bookmark, oldBookmark bookmark: Bookmark) -> Bookmark? { guard itemsDict[bookmark.url] != nil, let index = allBookmarkURLsOrdered.firstIndex(of: IdentifiableBookmark(from: bookmark)) else { - os_log("BookmarkList: Update failed, no such item in bookmark list") + Logger.bookmarks.debug("BookmarkList: Update failed, no such item in bookmark list") return nil } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift index 67d2ef132a..349d01526c 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkManager.swift @@ -20,6 +20,7 @@ import Bookmarks import Cocoa import Combine import Common +import os.log protocol BookmarkManager: AnyObject { @@ -116,19 +117,19 @@ final class LocalBookmarkManager: BookmarkManager { func loadBookmarks() { bookmarkStore.loadAll(type: .topLevelEntities) { [weak self] (topLevelEntities, error) in guard error == nil, let topLevelEntities = topLevelEntities else { - os_log("LocalBookmarkManager: Failed to fetch entities.", type: .error) + Logger.bookmarks.error("LocalBookmarkManager: Failed to fetch entities.") return } self?.bookmarkStore.loadAll(type: .bookmarks) { [weak self] (bookmarks, error) in guard error == nil, let bookmarks = bookmarks else { - os_log("LocalBookmarkManager: Failed to fetch bookmarks.", type: .error) + Logger.bookmarks.error("LocalBookmarkManager: Failed to fetch bookmarks.") return } self?.bookmarkStore.loadAll(type: .favorites) { [weak self] (favorites, error) in guard error == nil, let favorites = favorites else { - os_log("LocalBookmarkManager: Failed to fetch favorites.", type: .error) + Logger.bookmarks.error("LocalBookmarkManager: Failed to fetch favorites.") return } @@ -170,7 +171,7 @@ final class LocalBookmarkManager: BookmarkManager { guard list != nil else { return nil } guard !isUrlBookmarked(url: url) else { - os_log("LocalBookmarkManager: Url is already bookmarked", type: .error) + Logger.bookmarks.error("LocalBookmarkManager: Url is already bookmarked") return nil } @@ -200,7 +201,7 @@ final class LocalBookmarkManager: BookmarkManager { func remove(bookmark: Bookmark) { guard list != nil else { return } guard let latestBookmark = getBookmark(forUrl: bookmark.url) else { - os_log("LocalBookmarkManager: Attempt to remove already removed bookmark", type: .error) + Logger.bookmarks.error("LocalBookmarkManager: Attempt to remove already removed bookmark") return } @@ -232,7 +233,7 @@ final class LocalBookmarkManager: BookmarkManager { func update(bookmark: Bookmark) { guard list != nil else { return } guard getBookmark(forUrl: bookmark.url) != nil else { - os_log("LocalBookmarkManager: Failed to update bookmark - not in the list.", type: .error) + Logger.bookmarks.error("LocalBookmarkManager: Failed to update bookmark - not in the list.") return } @@ -246,12 +247,12 @@ final class LocalBookmarkManager: BookmarkManager { func update(bookmark: Bookmark, withURL url: URL, title: String, isFavorite: Bool) { guard list != nil else { return } guard getBookmark(forUrl: bookmark.url) != nil else { - os_log("LocalBookmarkManager: Failed to update bookmark url - not in the list.", type: .error) + Logger.bookmarks.error("LocalBookmarkManager: Failed to update bookmark url - not in the list.") return } guard let newBookmark = list?.update(bookmark: bookmark, newURL: url.absoluteString, newTitle: title, newIsFavorite: isFavorite) else { - os_log("LocalBookmarkManager: Failed to update URL of bookmark.", type: .error) + Logger.bookmarks.error("LocalBookmarkManager: Failed to update URL of bookmark.") return } @@ -275,12 +276,12 @@ final class LocalBookmarkManager: BookmarkManager { func updateUrl(of bookmark: Bookmark, to newUrl: URL) -> Bookmark? { guard list != nil else { return nil } guard getBookmark(forUrl: bookmark.url) != nil else { - os_log("LocalBookmarkManager: Failed to update bookmark url - not in the list.", type: .error) + Logger.bookmarks.error("LocalBookmarkManager: Failed to update bookmark url - not in the list.") return nil } guard let newBookmark = list?.updateUrl(of: bookmark, to: newUrl.absoluteString) else { - os_log("LocalBookmarkManager: Failed to update URL of bookmark.", type: .error) + Logger.bookmarks.error("LocalBookmarkManager: Failed to update URL of bookmark.") return nil } @@ -389,7 +390,7 @@ final class LocalBookmarkManager: BookmarkManager { guard let syncService = NSApp.delegateTyped.syncService else { return } - os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") + Logger.bookmarks.debug("Requesting sync if enabled") syncService.scheduler.notifyDataChanged() } } diff --git a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift index 09fa347374..7cf8b69414 100644 --- a/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift +++ b/DuckDuckGo/Bookmarks/Model/BookmarkOutlineViewDataSource.swift @@ -19,6 +19,7 @@ import AppKit import Common import Foundation +import os.log final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NSOutlineViewDelegate { @@ -305,13 +306,13 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS bookmark?.isFavorite = true }, completion: { error in if let error = error { - os_log("Failed to update entities during drop via outline view: %s", error.localizedDescription) + Logger.bookmarks.error("Failed to update entities during drop via outline view: \(error.localizedDescription, privacy: .public)") } }) } else if pseudoFolder == .bookmarks { bookmarkManager.add(objectsWithUUIDs: draggedObjectIdentifiers, to: nil) { error in if let error = error { - os_log("Failed to accept nil parent drop via outline view: %s", error.localizedDescription) + Logger.bookmarks.error("Failed to accept nil parent drop via outline view: \(error.localizedDescription, privacy: .public)") } } } @@ -340,7 +341,7 @@ final class BookmarkOutlineViewDataSource: NSObject, NSOutlineViewDataSource, NS let parent: ParentFolderType = (representedObject as? BookmarkFolder).map { .parent(uuid: $0.id) } ?? .root bookmarkManager.move(objectUUIDs: draggedObjectIdentifiers, toIndex: index, withinParentFolder: parent) { error in if let error = error { - os_log("Failed to accept existing parent drop via outline view: %s", error.localizedDescription) + Logger.bookmarks.error("Failed to accept existing parent drop via outline view: \(error.localizedDescription, privacy: .public)") } } diff --git a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift index b6b67dcacc..970ed26503 100644 --- a/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/LocalBookmarkStore.swift @@ -25,6 +25,7 @@ import Bookmarks import Cocoa import Persistence import PixelKit +import os.log final class LocalBookmarkStore: BookmarkStore { @@ -430,9 +431,9 @@ final class LocalBookmarkStore: BookmarkStore { bookmarkFolderToReturn = bookmarkFolder } catch BookmarkStoreError.badModelMapping { - os_log("Failed to map BookmarkEntity to BookmarkFolder, with error: %s", log: .bookmarks, type: .error) + Logger.bookmarks.error("Failed to map BookmarkEntity to BookmarkFolder, with error: BookmarkStoreError.badModelMapping") } catch { - os_log("Failed to fetch last saved folder for bookmarks all tabs, with error: %s", log: .bookmarks, type: .error, error.localizedDescription) + Logger.bookmarks.error("Failed to fetch last saved folder for bookmarks all tabs, with error: \(error.localizedDescription, privacy: .public)") } } @@ -796,7 +797,7 @@ final class LocalBookmarkStore: BookmarkStore { } } catch { - os_log("Failed to import bookmarks, with error: %s", log: .dataImportExport, type: .error, error.localizedDescription) + Logger.dataImportExport.error("Failed to import bookmarks, with error: \(error.localizedDescription, privacy: .public)") let error = error as NSError let processedErrors = CoreDataErrorsParser.parse(error: error) @@ -943,7 +944,7 @@ final class LocalBookmarkStore: BookmarkStore { } onError: { [weak self] error in self?.commonOnSaveErrorHandler(error) - os_log("Failed to remove invalid bookmark entities", type: .error) + Logger.bookmarks.error("Failed to remove invalid bookmark entities") } onDidSave: {} } diff --git a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift index 82d098a6e8..dd7c6ccb12 100644 --- a/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift +++ b/DuckDuckGo/BookmarksBar/View/BookmarksBarViewController.swift @@ -20,6 +20,7 @@ import AppKit import Combine import Common import Foundation +import os.log final class BookmarksBarViewController: NSViewController { @@ -217,7 +218,7 @@ final class BookmarksBarViewController: NSViewController { self.bookmarksBarCollectionView.indexPathForItem(at: point) }), let item = bookmarksBarCollectionView.item(at: indexPath.item) as? BookmarksBarCollectionViewItem else { - os_log("Item at mouseUp point not found.", type: .error) + Logger.bookmarks.error("Item at mouseUp point not found.") return } diff --git a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift index b7e767fece..ba6c31e203 100644 --- a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift +++ b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift @@ -18,6 +18,7 @@ import Common import Foundation +import os.log extension FileManager { @@ -89,7 +90,7 @@ extension FileManager { index += 1 } while index <= limit // If it gets beyond the limit then chances are something else is wrong - os_log("Failed to move file to %s, attempt: %d", type: .error, desiredURL.deletingLastPathComponent().path, index) + Logger.general.error("Failed to move file to \(desiredURL.deletingLastPathComponent().path), attempt: \(index)") throw CocoaError(.fileWriteFileExists) } diff --git a/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift b/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift index 9b37797f7b..c2f41dd0ff 100644 --- a/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSTextFieldExtension.swift @@ -18,6 +18,7 @@ import Cocoa import Common +import os.log extension NSTextField { @@ -54,7 +55,7 @@ extension NSTextField { func gradient(width: CGFloat, trailingPadding: CGFloat) { guard let layer = layer else { - os_log("NSTextField: Making of gradient failed - Text field has no layer.", type: .error) + Logger.general.error("NSTextField: Making of gradient failed - Text field has no layer.") return } @@ -65,7 +66,7 @@ extension NSTextField { } guard let mask = layer.mask as? CAGradientLayer else { - os_log("NSTextField: Making of gradient failed - Mask has no gradient layer.", type: .error) + Logger.general.error("NSTextField: Making of gradient failed - Mask has no gradient layer.") return } diff --git a/DuckDuckGo/Common/Extensions/NSViewExtension.swift b/DuckDuckGo/Common/Extensions/NSViewExtension.swift index 333dce57cf..13abfebb0a 100644 --- a/DuckDuckGo/Common/Extensions/NSViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSViewExtension.swift @@ -19,6 +19,7 @@ import Cocoa import Combine import Common +import os.log extension NSView { @@ -86,7 +87,7 @@ extension NSView { func makeMeFirstResponder() { guard let window = window else { - os_log("%s: Window not available", type: .error, className) + Logger.general.error("\(self.className): Window not available") return } // prevent all text selection on repeated Address Bar activation diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index ca459c0173..a937bc373b 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -21,6 +21,7 @@ import BrowserServicesKit import Common import Foundation import AppKitExtensions +import os.log extension URL.NavigationalScheme { @@ -117,7 +118,7 @@ extension URL { return searchUrl } - os_log("URL extension: Making URL from %s failed", type: .error, addressBarString) + Logger.general.error("URL extension: Making URL from \(addressBarString) failed") return nil } diff --git a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift index eed0d7c745..0019081b39 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift @@ -21,6 +21,7 @@ import Combine import Common import WebKit import UserScript +import os.log extension WKWebViewConfiguration { @@ -120,7 +121,7 @@ extension NSPopover { observer?.cancel() } - os_log(.error, "trying to present \(self) from \(positioningView) not in view hierarchy") + Logger.general.error("trying to present \(self) from \(positioningView) not in view hierarchy") return } self.swizzled_show(relativeTo: positioningRect, of: positioningView, preferredEdge: preferredEdge) diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index b3b56fd610..573dfaf38d 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -19,6 +19,7 @@ import Common import Navigation import WebKit +import os.log extension WKWebView { @@ -274,7 +275,7 @@ extension WKWebView { func loadAlternateHTML(_ html: String, baseURL: URL, forUnreachableURL failingURL: URL) { guard responds(to: Selector.loadAlternateHTMLString) else { if #available(macOS 12.0, *) { - os_log(.error, log: .navigation, "WKWebView._loadAlternateHTMLString not available") + Logger.navigation.error("WKWebView._loadAlternateHTMLString not available") loadSimulatedRequest(URLRequest(url: failingURL), responseHTML: html) } return diff --git a/DuckDuckGo/Common/Logger+Multiple.swift b/DuckDuckGo/Common/Logger+Multiple.swift new file mode 100644 index 0000000000..f29c3e8b7e --- /dev/null +++ b/DuckDuckGo/Common/Logger+Multiple.swift @@ -0,0 +1,31 @@ +// +// Logger+Multiple.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log + +extension Logger { + static var dataImportExport = { Logger(subsystem: "Data Import/Export", category: "") }() + static var config = { Logger(subsystem: "Configuration Downloading", category: "") }() + static var autoLock = { Logger(subsystem: "Auto-Lock", category: "") }() + static var httpsUpgrade = { Logger(subsystem: "HTTPS Upgrade", category: "") }() + static var atb = { Logger(subsystem: "ATB", category: "") }() + static var tabSnapshots = { Logger(subsystem: "Tab Snapshots", category: "") }() + static var tabLazyLoading = { Logger(subsystem: "Lazy Loading", category: "") }() + static var updates = { Logger(subsystem: "Updates", category: "") }() +} diff --git a/DuckDuckGo/Common/Logging/Logging.swift b/DuckDuckGo/Common/Logging/Logging.swift deleted file mode 100644 index 7b3af9d9ea..0000000000 --- a/DuckDuckGo/Common/Logging/Logging.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// Logging.swift -// -// Copyright © 2021 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 os.log - -extension OSLog { - - enum AppCategories: String, CaseIterable { - case atb = "ATB" - case config = "Configuration Downloading" - case downloads = "Downloads" - case fire = "Fire" - case dataImportExport = "Data Import/Export" - case pixel = "Pixel" - case httpsUpgrade = "HTTPS Upgrade" - case favicons = "Favicons" - case autoLock = "Auto-Lock" - case tabLazyLoading = "Lazy Loading" - case autoconsent = "Autoconsent" - case bookmarks = "Bookmarks" - case attribution = "Ad Attribution" - case bitwarden = "Bitwarden" - case navigation = "Navigation" - case duckPlayer = "Duck Player" - case tabSnapshots = "Tab Snapshots" - case updates = "Updates" - case sync = "Sync" - case dbp = "dbp" - } - - enum AllCategories { - static var allCases: [String] { - AppCategories.allCases.map(\.rawValue) - } - } - - @OSLogWrapper(AppCategories.atb) static var atb - @OSLogWrapper(AppCategories.config) static var config - @OSLogWrapper(AppCategories.downloads) static var downloads - @OSLogWrapper(AppCategories.fire) static var fire - @OSLogWrapper(AppCategories.dataImportExport) static var dataImportExport - @OSLogWrapper(AppCategories.pixel) static var pixel - @OSLogWrapper(AppCategories.httpsUpgrade) static var httpsUpgrade - @OSLogWrapper(AppCategories.favicons) static var favicons - @OSLogWrapper(AppCategories.autoLock) static var autoLock - @OSLogWrapper(AppCategories.tabLazyLoading) static var tabLazyLoading - @OSLogWrapper(AppCategories.autoconsent) static var autoconsent - @OSLogWrapper(AppCategories.bookmarks) static var bookmarks - @OSLogWrapper(AppCategories.attribution) static var attribution - @OSLogWrapper(AppCategories.bitwarden) static var bitwarden - @OSLogWrapper(AppCategories.navigation) static var navigation - @OSLogWrapper(AppCategories.duckPlayer) static var duckPlayer - @OSLogWrapper(AppCategories.tabSnapshots) static var tabSnapshots - @OSLogWrapper(AppCategories.updates) static var updates - @OSLogWrapper(AppCategories.sync) static var sync - @OSLogWrapper(AppCategories.dbp) static var dbp - - // Debug->Logging categories will only be enabled for one day - @UserDefaultsWrapper(key: .loggingEnabledDate, defaultValue: .distantPast) - private static var loggingEnabledDate: Date - private static var isLoggingEnabledToday: Bool { - NSCalendar.current.isDate(Date(), inSameDayAs: loggingEnabledDate) - } - - @UserDefaultsWrapper(key: .loggingCategories, defaultValue: []) - private static var loggingCategoriesSetting: [String] { - didSet { - loggingEnabledDate = Date() - } - } - static var loggingCategories: Set { - get { - guard isLoggingEnabledToday else { return [] } - return Set(loggingCategoriesSetting) - } - set { - loggingCategoriesSetting = Array(newValue) - enabledLoggingCategories = loggingCategories - } - } - - static let isRunningInDebugEnvironment: Bool = { - ProcessInfo().environment[ProcessInfo.Constants.osActivityMode] == ProcessInfo.Constants.debug - || ProcessInfo().environment[ProcessInfo.Constants.osActivityDtMode] == ProcessInfo.Constants.yes - }() - - static let subsystem = Bundle.main.bundleIdentifier! - -} - -extension ProcessInfo { - enum Constants { - static let osActivityMode = "OS_ACTIVITY_MODE" - static let osActivityDtMode = "OS_ACTIVITY_DT_MODE" - static let debug = "debug" - static let yes = "YES" - } -} - -extension OSLog.OSLogWrapper { - - private static let enableLoggingCategoriesOnce: Void = { -#if CI - OSLog.enabledLoggingCategories = Set(OSLog.AllCategories.allCases) -#else - OSLog.enabledLoggingCategories = OSLog.loggingCategories -#endif - }() - - init(_ category: OSLog.AppCategories) { - _=Self.enableLoggingCategoriesOnce - self.init(rawValue: category.rawValue) - } - -} - -func logOrAssertionFailure(_ message: String) { -#if DEBUG && !CI - assertionFailure(message) -#else - os_log("%{public}s", type: .error, message) -#endif -} - -#if DEBUG - -func breakByRaisingSigInt(_ description: String, file: StaticString = #file, line: Int = #line) { - let fileLine = "\(("\(file)" as NSString).lastPathComponent):\(line)" - os_log(""" - - - ------------------------------------------------------------------------------------------------------ - BREAK at %s: - ------------------------------------------------------------------------------------------------------ - - %s - - Hit Continue (^⌘Y) to continue program execution - ------------------------------------------------------------------------------------------------------ - - """, type: .debug, fileLine, description.components(separatedBy: "\n").map { " " + $0.trimmingWhitespace() }.joined(separator: "\n")) - raise(SIGINT) -} - -// get symbol from stack trace for a caller of a calling method -func callingSymbol() -> String { - let stackTrace = Thread.callStackSymbols - // find `callingSymbol` itself or dispatch_once_callout - var callingSymbolIdx = stackTrace.firstIndex(where: { $0.contains("_dispatch_once_callout") }) - ?? stackTrace.firstIndex(where: { $0.contains("callingSymbol") })! - // procedure calling `callingSymbol` - callingSymbolIdx += 1 - - var symbolName: String - repeat { - // caller for the procedure - callingSymbolIdx += 1 - let line = stackTrace[callingSymbolIdx].replacingOccurrences(of: Bundle.main.executableURL!.lastPathComponent, with: "DDG") - symbolName = String(line.split(separator: " ", maxSplits: 3)[3]).components(separatedBy: " + ")[0] - } while stackTrace[callingSymbolIdx - 1].contains(symbolName.dropping(suffix: "To")) // skip objc wrappers - - return symbolName -} - -#endif diff --git a/DuckDuckGo/Common/Utilities/TabInstrumentation.swift b/DuckDuckGo/Common/Utilities/TabInstrumentation.swift index 9e1d674db2..2fa526eaba 100644 --- a/DuckDuckGo/Common/Utilities/TabInstrumentation.swift +++ b/DuckDuckGo/Common/Utilities/TabInstrumentation.swift @@ -91,9 +91,7 @@ final class TabInstrumentation: TabInstrumentationProtocol { // 0 is treated as 1ms let timeInNS: UInt64 = timeInMs.asNanos - os_log(.debug, - log: type(of: self).tabsLog, - "[%@] Request: %@ - %@ - %@ (%@) in %llu", currentURL, url, requestType, status, reason, timeInNS) + Logger.general.debug("[\(currentURL)] Request: \(url) - \(requestType) - \(status) (\(reason)) in \(timeInNS)") } func jsEvent(name: String, executedIn timeInMs: Double) { @@ -102,9 +100,7 @@ final class TabInstrumentation: TabInstrumentationProtocol { // 0 is treated as 1ms let timeInNS: UInt64 = timeInMs.asNanos - os_log(.debug, - log: type(of: self).tabsLog, - "[%@] JSEvent: %@ executedIn: %llu", currentURL, name, timeInNS) + Logger.general.debug("[\(currentURL)] JSEvent: \(name) executedIn: \(timeInNS)") } } diff --git a/DuckDuckGo/Configuration/ConfigurationManager.swift b/DuckDuckGo/Configuration/ConfigurationManager.swift index f7bd81720b..4f912030f5 100644 --- a/DuckDuckGo/Configuration/ConfigurationManager.swift +++ b/DuckDuckGo/Configuration/ConfigurationManager.swift @@ -23,6 +23,7 @@ import Configuration import Common import Networking import PixelKit +import os.log final class ConfigurationManager { @@ -81,7 +82,7 @@ final class ConfigurationManager { } func start() { - os_log("Starting configuration refresh timer", log: .config, type: .debug) + Logger.config.debug("Starting configuration refresh timer") timerCancellable = Timer.publish(every: Constants.refreshCheckIntervalSeconds, on: .main, in: .default) .autoconnect() .receive(on: Self.queue) @@ -94,11 +95,6 @@ final class ConfigurationManager { } } - func log() { - os_log("last update %{public}s", log: .config, type: .default, String(describing: lastUpdateTime)) - os_log("last refresh check %{public}s", log: .config, type: .default, String(describing: lastRefreshCheckTime)) - } - private func refreshNow(isDebug: Bool = false) async { let updateTrackerBlockingDependenciesTask = Task { let didFetchAnyTrackerBlockingDependencies = await fetchTrackerBlockingDependencies(isDebug: isDebug) @@ -133,7 +129,9 @@ final class ConfigurationManager { await updateBloomFilterExclusionsTask.value ConfigurationStore.shared.log() - log() + + Logger.config.info("last update \(String(describing: self.lastUpdateTime), privacy: .public)") + Logger.config.info("last refresh check \(String(describing: self.lastRefreshCheckTime), privacy: .public)") } private func fetchTrackerBlockingDependencies(isDebug: Bool) async -> Bool { @@ -149,11 +147,7 @@ final class ConfigurationManager { try await task.value didFetchAnyTrackerBlockingDependencies = true } catch { - os_log("Failed to complete configuration update to %@: %@", - log: .config, - type: .error, - configuration.rawValue, - error.localizedDescription) + Logger.config.error("Failed to complete configuration update to \(configuration.rawValue): \(error.localizedDescription, privacy: .public)") tryAgainSoon() } } @@ -168,7 +162,7 @@ final class ConfigurationManager { return } - os_log("Failed to complete configuration update %@", log: .config, type: .error, error.localizedDescription) + Logger.config.error("Failed to complete configuration update \(error.localizedDescription, privacy: .public)") PixelKit.fire(DebugEvent(GeneralPixel.configurationFetchError(error: error))) tryAgainSoon() } @@ -176,7 +170,7 @@ final class ConfigurationManager { @discardableResult public func refreshIfNeeded() -> Task? { guard isReadyToRefresh else { - os_log("Configuration refresh is not needed at this time", log: .config, type: .debug) + Logger.config.debug("Configuration refresh is not needed at this time") return nil } return Task { diff --git a/DuckDuckGo/Configuration/ConfigurationStore.swift b/DuckDuckGo/Configuration/ConfigurationStore.swift index 3a754dccb3..5d21979c29 100644 --- a/DuckDuckGo/Configuration/ConfigurationStore.swift +++ b/DuckDuckGo/Configuration/ConfigurationStore.swift @@ -20,6 +20,7 @@ import Common import Foundation import Configuration import PixelKit +import os.log final class ConfigurationStore: ConfigurationStoring { @@ -119,14 +120,14 @@ final class ConfigurationStore: ConfigurationStoring { } func log() { - os_log("bloomFilterBinaryEtag %{public}s", log: .config, type: .default, bloomFilterBinaryEtag ?? "") - os_log("bloomFilterSpecEtag %{public}s", log: .config, type: .default, bloomFilterSpecEtag ?? "") - os_log("bloomFilterExcludedDomainsEtag %{public}s", log: .config, type: .default, bloomFilterExcludedDomainsEtag ?? "") - os_log("surrogatesEtag %{public}s", log: .config, type: .default, surrogatesEtag ?? "") - os_log("trackerRadarEtag %{public}s", log: .config, type: .default, trackerRadarEtag ?? "") - os_log("privacyConfigurationEtag %{public}s", log: .config, type: .default, privacyConfigurationEtag ?? "") - os_log("FBConfigEtag %{public}s", log: .config, type: .default, FBConfigEtag ?? "") - os_log("remoteMessagingConfig %{public}s", log: .config, type: .default, remoteMessagingConfigEtag ?? "") + Logger.config.info("bloomFilterBinaryEtag \(self.bloomFilterBinaryEtag ?? "", privacy: .public)") + Logger.config.info("bloomFilterSpecEtag \(self.bloomFilterSpecEtag ?? "", privacy: .public)") + Logger.config.info("bloomFilterExcludedDomainsEtag \(self.self.bloomFilterExcludedDomainsEtag ?? "", privacy: .public)") + Logger.config.info("surrogatesEtag \(self.surrogatesEtag ?? "", privacy: .public)") + Logger.config.info("trackerRadarEtag \(self.trackerRadarEtag ?? "", privacy: .public)") + Logger.config.info("privacyConfigurationEtag \(self.privacyConfigurationEtag ?? "", privacy: .public)") + Logger.config.info("FBConfigEtag \(self.FBConfigEtag ?? "", privacy: .public)") + Logger.config.info("remoteMessagingConfig \(self.remoteMessagingConfigEtag ?? "", privacy: .public)") } func fileUrl(for config: Configuration) -> URL { diff --git a/DuckDuckGo/ContentBlocker/ContentBlocking.swift b/DuckDuckGo/ContentBlocker/ContentBlocking.swift index 28825f7c5a..137ef93f2c 100644 --- a/DuckDuckGo/ContentBlocker/ContentBlocking.swift +++ b/DuckDuckGo/ContentBlocker/ContentBlocking.swift @@ -92,8 +92,7 @@ final class AppContentBlocking { compiledRulesSource: contentBlockingManager, exceptionsSource: exceptionsSource, errorReporting: attributionDebugEvents, - compilationErrorReporting: Self.debugEvents, - log: .attribution) + compilationErrorReporting: Self.debugEvents) } private static let debugEvents = EventMapping { event, error, parameters, onComplete in diff --git a/DuckDuckGo/CrashReports/Model/CrashReporter.swift b/DuckDuckGo/CrashReports/Model/CrashReporter.swift index f8e73f87e6..b3456a3adb 100644 --- a/DuckDuckGo/CrashReports/Model/CrashReporter.swift +++ b/DuckDuckGo/CrashReports/Model/CrashReporter.swift @@ -24,7 +24,7 @@ import PixelKit final class CrashReporter { private let reader = CrashReportReader() - private lazy var sender = CrashReportSender(platform: .macOS, log: .default) + private lazy var sender = CrashReportSender(platform: .macOS) private lazy var promptPresenter = CrashReportPromptPresenter() @UserDefaultsWrapper(key: .lastCrashReportCheckDate, defaultValue: nil) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift index 4eeb776e44..f0b173a42a 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionAppEvents.swift @@ -77,7 +77,7 @@ struct DataBrokerProtectionAppEvents { private func restartBackgroundAgent(loginItemsManager: LoginItemsManager) { DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerResetLoginItemDaily, frequency: .daily) loginItemsManager.disableLoginItems([LoginItem.dbpBackgroundAgent]) - loginItemsManager.enableLoginItems([LoginItem.dbpBackgroundAgent], log: .dbp) + loginItemsManager.enableLoginItems([LoginItem.dbpBackgroundAgent]) // restartLoginItems doesn't work when we change the agent name } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift index 660cf055c5..0a0651957e 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionDebugMenu.swift @@ -22,6 +22,7 @@ import AppKit import Common import LoginItems import NetworkProtectionProxy +import os.log @MainActor final class DataBrokerProtectionDebugMenu: NSMenu { @@ -175,28 +176,28 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } @objc private func startScheduledOperations(_ sender: NSMenuItem) { - os_log("Running queued operations...", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Running queued operations...") let showWebView = sender.representedObject as? Bool ?? false DataBrokerProtectionManager.shared.loginItemInterface.startScheduledOperations(showWebView: showWebView) } @objc private func runScanOperations(_ sender: NSMenuItem) { - os_log("Running scan operations...", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Running scan operations...") let showWebView = sender.representedObject as? Bool ?? false DataBrokerProtectionManager.shared.loginItemInterface.startImmediateOperations(showWebView: showWebView) } @objc private func runOptoutOperations(_ sender: NSMenuItem) { - os_log("Running Optout operations...", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Running Optout operations...") let showWebView = sender.representedObject as? Bool ?? false DataBrokerProtectionManager.shared.loginItemInterface.runAllOptOuts(showWebView: showWebView) } @objc private func backgroundAgentRestart() { - LoginItemsManager().restartLoginItems([LoginItem.dbpBackgroundAgent], log: .dbp) + LoginItemsManager().restartLoginItems([LoginItem.dbpBackgroundAgent]) } @objc private func backgroundAgentDisable() { @@ -204,7 +205,7 @@ final class DataBrokerProtectionDebugMenu: NSMenu { } @objc private func backgroundAgentEnable() { - LoginItemsManager().enableLoginItems([LoginItem.dbpBackgroundAgent], log: .dbp) + LoginItemsManager().enableLoginItems([LoginItem.dbpBackgroundAgent]) } @objc private func deleteAllDataAndStopAgent() { diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift index 66df557611..96b3f94c95 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureDisabler.swift @@ -19,6 +19,7 @@ import Foundation import DataBrokerProtection import Common +import os.log public extension Notification.Name { static let dbpWasDisabled = Notification.Name("com.duckduckgo.DBP.DBPWasDisabled") @@ -43,7 +44,7 @@ struct DataBrokerProtectionFeatureDisabler: DataBrokerProtectionFeatureDisabling try dataManager.removeAllData() // the dataManagers delegate handles login item disabling } catch { - os_log("DataBrokerProtectionFeatureDisabler error: disableAndDelete, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("DataBrokerProtectionFeatureDisabler error: disableAndDelete, error: \(error.localizedDescription, privacy: .public)") } DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerDisableAndDeleteDaily, frequency: .daily) diff --git a/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift index 491ca77a52..0affd1fb34 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionFeatureGatekeeper.swift @@ -21,6 +21,7 @@ import BrowserServicesKit import Common import DataBrokerProtection import Subscription +import os.log protocol DataBrokerProtectionFeatureGatekeeper { func isFeatureVisible() -> Bool @@ -76,7 +77,7 @@ struct DefaultDataBrokerProtectionFeatureGatekeeper: DataBrokerProtectionFeature func disableAndDeleteForAllUsers() { featureDisabler.disableAndDelete() - os_log("Disabling and removing DBP for all users", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Disabling and removing DBP for all users") } /// If we want to prevent new users from joining the waitlist while still allowing waitlist users to continue using it, @@ -118,12 +119,12 @@ private extension DefaultDataBrokerProtectionFeatureGatekeeper { func firePrerequisitePixelsAndLogIfNecessary(hasEntitlements: Bool, isAuthenticatedResult: Bool) { if !hasEntitlements { pixelHandler.fire(.gatekeeperEntitlementsInvalid) - os_log("🔴 DBP feature Gatekeeper: Entitlement check failed", log: .dataBrokerProtection) + Logger.dataBrokerProtection.error("DBP feature Gatekeeper: Entitlement check failed") } if !isAuthenticatedResult { pixelHandler.fire(.gatekeeperNotAuthenticated) - os_log("🔴 DBP feature Gatekeeper: Authentication check failed", log: .dataBrokerProtection) + Logger.dataBrokerProtection.error("DBP feature Gatekeeper: Authentication check failed") } } } diff --git a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift index 881312dd17..b619092aa3 100644 --- a/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift +++ b/DuckDuckGo/DBP/DataBrokerProtectionLoginItemInterface.swift @@ -51,7 +51,7 @@ extension DefaultDataBrokerProtectionLoginItemInterface: DataBrokerProtectionLog private func enableLoginItem() { DataBrokerProtectionLoginItemPixels.fire(pixel: GeneralPixel.dataBrokerEnableLoginItemDaily, frequency: .daily) - loginItemsManager.enableLoginItems([.dbpBackgroundAgent], log: .dbp) + loginItemsManager.enableLoginItems([.dbpBackgroundAgent]) } // MARK: - DataBrokerProtectionLoginItemInterface diff --git a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift index 3411cf1dc6..540dc8e1f6 100644 --- a/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift +++ b/DuckDuckGo/DBP/LoginItem+DataBrokerProtection.swift @@ -19,11 +19,11 @@ import Foundation import LoginItems import DataBrokerProtection +import os.log extension LoginItem { - static let dbpBackgroundAgent = LoginItem(bundleId: Bundle.main.dbpBackgroundAgentBundleId, defaults: .dbp, log: .dbp) - + static let dbpBackgroundAgent = LoginItem(bundleId: Bundle.main.dbpBackgroundAgentBundleId, defaults: .dbp, logger: Logger.dataBrokerProtection) } extension LoginItem: DBPLoginItemStatusChecker { diff --git a/DuckDuckGo/DataImport/Bookmarks/Safari/SafariBookmarksReader.swift b/DuckDuckGo/DataImport/Bookmarks/Safari/SafariBookmarksReader.swift index 51b2ceb122..4e0d75a040 100644 --- a/DuckDuckGo/DataImport/Bookmarks/Safari/SafariBookmarksReader.swift +++ b/DuckDuckGo/DataImport/Bookmarks/Safari/SafariBookmarksReader.swift @@ -18,6 +18,7 @@ import Common import Foundation +import os.log final class SafariBookmarksReader { @@ -71,7 +72,7 @@ final class SafariBookmarksReader { let bookmarks = try reallyReadBookmarks() return .success(bookmarks) } catch { - os_log("Failed to read Safari Bookmarks Plist at \"%s\": %s", log: .dataImportExport, type: .error, safariBookmarksFileURL.path, error.localizedDescription) + Logger.dataImportExport.error("Failed to read Safari Bookmarks Plist at \(self.safariBookmarksFileURL.path): \(error.localizedDescription)") switch error { case let error as ImportError: return .failure(error) diff --git a/DuckDuckGo/DataImport/Bookmarks/Safari/SafariFaviconsReader.swift b/DuckDuckGo/DataImport/Bookmarks/Safari/SafariFaviconsReader.swift index d871378c67..2a4013defa 100644 --- a/DuckDuckGo/DataImport/Bookmarks/Safari/SafariFaviconsReader.swift +++ b/DuckDuckGo/DataImport/Bookmarks/Safari/SafariFaviconsReader.swift @@ -21,6 +21,7 @@ import Foundation import Common import CryptoKit import GRDB +import os.log final class SafariFaviconsReader { @@ -119,7 +120,7 @@ final class SafariFaviconsReader { do { imageData = try readImageData(with: record.host) } catch { - os_log(.error, log: .dataImportExport, "could not read image data for \(record.host)") + Logger.dataImportExport.error("could not read image data for \(record.host): \(error.localizedDescription)") return nil } guard let formattedHost = record.formattedHost else { return nil } diff --git a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift index 489e857c6e..fb28f85350 100644 --- a/DuckDuckGo/DataImport/Model/DataImportViewModel.swift +++ b/DuckDuckGo/DataImport/Model/DataImportViewModel.swift @@ -20,6 +20,7 @@ import AppKit import Common import UniformTypeIdentifiers import PixelKit +import os.log struct DataImportViewModel { @@ -54,15 +55,6 @@ struct DataImportViewModel { /// Factory for a DataImporter for importSource private let reportSenderFactory: ReportSenderFactory - private func log(_ message: @autoclosure () -> String) { - if NSApp.runType == .unitTests { - } else if OSLog.dataImportExport != .disabled { - os_log(log: .dataImportExport, message()) - } else if NSApp.runType == .xcPreviews { - print(message()) - } - } - enum Screen: Hashable { case profileAndDataTypesPicker case moreInfo @@ -188,7 +180,7 @@ struct DataImportViewModel { ?? selectedDataTypes.subtracting(self.summary.filter { $0.result.isSuccess }.map(\.dataType)) let importer = dataImporterFactory(importSource, dataType, url, primaryPassword) - log("import \(dataTypes) at \"\(url.path)\" using \(type(of: importer))") + Logger.dataImportExport.debug("import \(dataTypes) at \"\(url.path)\" using \(type(of: importer))") // validate file access/encryption password requirement before starting import if let errors = importer.validateAccess(for: dataTypes), @@ -226,7 +218,7 @@ struct DataImportViewModel { private mutating func mergeImportSummary(with summary: DataImportSummary) { self.importTask = nil - log("merging summary \(summary)") + Logger.dataImportExport.debug("merging summary \(summary)") // append successful import results first keeping the original DataType sorting order self.summary.append(contentsOf: DataType.allCases.compactMap { dataType in @@ -261,23 +253,23 @@ struct DataImportViewModel { } if let nextScreen { - log("mergeImportSummary: next screen: \(nextScreen)") + Logger.dataImportExport.debug("mergeImportSummary: next screen: \(String(describing: nextScreen))") self.screen = nextScreen } else if screenForNextDataTypeRemainingToImport(after: DataType.allCases.last(where: summary.keys.contains)) == nil, // no next data type manual import screen // and there should be failed data types (and non-recovered) selectedDataTypes.contains(where: { dataType in self.summary.last(where: { $0.dataType == dataType })?.result.error != nil }) { - log("mergeImportSummary: feedback") + Logger.dataImportExport.debug("mergeImportSummary: feedback") // after last failed datatype show feedback self.screen = .feedback } else if self.screen.isFileImport, let dataType = self.screen.fileImportDataType { - log("mergeImportSummary: file import summary(\(dataType))") + Logger.dataImportExport.debug("mergeImportSummary: file import summary(\(dataType))") self.screen = .summary([dataType], isFileImport: true) } else if screenForNextDataTypeRemainingToImport(after: DataType.allCases.last(where: summary.keys.contains)) == nil { // no next data type manual import screen let allKeys = self.summary.reduce(into: Set()) { $0.insert($1.dataType) } - log("mergeImportSummary: final summary(\(Set(allKeys)))") + Logger.dataImportExport.debug("mergeImportSummary: final summary(\(Set(allKeys)))") self.screen = .summary(allKeys) } else { - log("mergeImportSummary: intermediary summary(\(Set(summary.keys)))") + Logger.dataImportExport.debug("mergeImportSummary: intermediary summary(\(Set(summary.keys)))") self.screen = .summary(Set(summary.keys)) } @@ -301,7 +293,7 @@ struct DataImportViewModel { // firefox passwords db is main-password protected: request password case let error as FirefoxLoginReader.ImportError where error.type == .requiresPrimaryPassword: - log("primary password required") + Logger.dataImportExport.debug("primary password required") // stay on the same screen but request password synchronously if let password = self.requestPrimaryPasswordCallback(importSource) { self.initiateImport(primaryPassword: password) @@ -315,7 +307,7 @@ struct DataImportViewModel { assertionFailure("No url") break } - log("file read no permission for \(url.path)") + Logger.dataImportExport.debug("file read no permission for \(url.path)") if url != selectedProfile?.profileURL.appendingPathComponent(SafariDataImporter.bookmarksFileName) { PixelKit.fire(GeneralPixel.dataImportFailed(source: importSource, sourceVersion: importSource.installedAppsMajorVersionDescription(selectedProfile: selectedProfile), error: importError)) @@ -566,7 +558,7 @@ extension DataImportViewModel { for await event in importTask.progress { switch event { case .progress(let update): - log("progress: \(update)") + Logger.dataImportExport.debug("progress: \(String(describing: update))") return .progress(update) // on completion returns new DataImportViewModel with merged import summary case .completed(.success(let summary)): @@ -721,7 +713,7 @@ extension DataImportViewModel { } } - log("dismiss") + Logger.dataImportExport.debug("dismiss") dismiss() if case .xcPreviews = NSApp.runType { self.update(with: importSource) // reset diff --git a/DuckDuckGo/DataImport/View/FileImportView.swift b/DuckDuckGo/DataImport/View/FileImportView.swift index a644363c63..8e704b2293 100644 --- a/DuckDuckGo/DataImport/View/FileImportView.swift +++ b/DuckDuckGo/DataImport/View/FileImportView.swift @@ -19,6 +19,7 @@ import Common import SwiftUI import UniformTypeIdentifiers +import os.log @InstructionsView.InstructionsBuilder func fileImportInstructionsBuilder(source: DataImport.Source, dataType: DataImport.DataType, button: @escaping (String) -> AnyView) -> [InstructionsView.InstructionsItem] { @@ -532,13 +533,13 @@ struct FileImportView: View { let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(typeIdentifier) }) else { - os_log(.error, log: .dataImportExport, "invalid type identifiers: \(allowedTypeIdentifiers)") + Logger.dataImportExport.error("invalid type identifiers: \(allowedTypeIdentifiers)") return false } provider.loadItem(forTypeIdentifier: typeIdentifier) { data, error in guard let data else { - os_log(.error, log: .dataImportExport, "error loading \(typeIdentifier): \(error?.localizedDescription ?? "?")") + Logger.dataImportExport.error("error loading \(typeIdentifier): \(error?.localizedDescription ?? "?")") return } let url: URL @@ -547,12 +548,12 @@ struct FileImportView: View { url = value case let data as Data: guard let value = URL(dataRepresentation: data, relativeTo: nil) else { - os_log(.error, log: .dataImportExport, "could not decode data: \(data.debugDescription)") + Logger.dataImportExport.error("could not decode data: \(data.debugDescription)") return } url = value default: - os_log(.error, log: .dataImportExport, "unsupported data: \(data)") + Logger.dataImportExport.error("unsupported data: \(String(describing: data))") return } diff --git a/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift b/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift index 45219d5e5d..aad803bf55 100644 --- a/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift +++ b/DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift @@ -19,6 +19,7 @@ import Foundation import LocalAuthentication import Common +import os.log extension NSNotification.Name { @@ -117,7 +118,7 @@ final class DeviceAuthenticator: UserAuthenticating { self._deviceIsLocked = newState } - os_log("Device lock state changed: %s", log: .autoLock, deviceIsLocked ? "locked" : "unlocked") + Logger.autoLock.debug("Device lock state changed: \(self.deviceIsLocked ? "locked" : "unlocked", privacy: .public)") if newState { NotificationCenter.default.post(name: .deviceBecameLocked, object: nil) @@ -167,12 +168,12 @@ final class DeviceAuthenticator: UserAuthenticating { return } - os_log("Began authenticating", log: .autoLock) + Logger.autoLock.debug("Began authenticating") isAuthenticating = true authenticationService.authenticateDevice(reason: reason.localizedDescription) { authenticationResult in - os_log("Completed authenticating, with result: %{bool}d", log: .autoLock, authenticationResult.authenticated) + Logger.autoLock.debug("Completed authenticating, with result: \(authenticationResult.authenticated, privacy: .public)") self.isAuthenticating = false self.deviceIsLocked = !authenticationResult.authenticated @@ -199,7 +200,7 @@ final class DeviceAuthenticator: UserAuthenticating { // MARK: - Idle Timer Monitoring private func beginIdleCheckTimer() { - os_log("Beginning idle check timer", log: .autoLock) + Logger.autoLock.debug("Beginning idle check timer") self.timer?.invalidate() self.timer = nil @@ -217,7 +218,7 @@ final class DeviceAuthenticator: UserAuthenticating { } private func cancelIdleCheckTimer() { - os_log("Cancelling idle check timer", log: .autoLock) + Logger.autoLock.debug("Cancelling idle check timer") self.timer?.invalidate() self.timer = nil } @@ -235,12 +236,12 @@ final class DeviceAuthenticator: UserAuthenticating { func beginCheckingIdleTimer() { guard !deviceIsLocked else { - os_log("Tried to start idle timer while device was already locked", log: .autoLock) + Logger.autoLock.debug("Tried to start idle timer while device was already locked") return } guard autofillPreferences.isAutoLockEnabled else { - os_log("Tried to start idle timer but device should not auto-lock", log: .autoLock) + Logger.autoLock.debug("Tried to start idle timer but device should not auto-lock") return } @@ -256,7 +257,7 @@ final class DeviceAuthenticator: UserAuthenticating { // MARK: - Credit Card Autofill Timer private func beginCreditCardAutofillTimer() { - os_log("Beginning credit card autofill timer", log: .autoLock) + Logger.autoLock.debug("Beginning credit card autofill timer") self.timerCreditCard?.invalidate() self.timerCreditCard = nil @@ -273,7 +274,7 @@ final class DeviceAuthenticator: UserAuthenticating { } private func cancelCreditCardAutofillTimer() { - os_log("Cancelling credit card autofill timer", log: .autoLock) + Logger.autoLock.debug("Cancelling credit card autofill timer") self.timerCreditCard?.invalidate() self.timerCreditCard = nil } @@ -288,7 +289,7 @@ final class DeviceAuthenticator: UserAuthenticating { // MARK: - Sync Timer private func beginSyncSettingsTimer() { - os_log("Beginning Sync Settings timer", log: .autoLock) + Logger.autoLock.debug("Beginning Sync Settings timer") self.timerSyncSettings?.invalidate() self.timerSyncSettings = nil @@ -305,7 +306,7 @@ final class DeviceAuthenticator: UserAuthenticating { } private func cancelSyncSettingsTimer() { - os_log("Cancelling Sync Settings timer", log: .autoLock) + Logger.autoLock.debug("Cancelling Sync Settings timer") self.timerSyncSettings?.invalidate() self.timerSyncSettings = nil } diff --git a/DuckDuckGo/DeviceAuthentication/QuartzIdleStateProvider.swift b/DuckDuckGo/DeviceAuthentication/QuartzIdleStateProvider.swift index 1e2fb0fd1f..2d8ebe26ea 100644 --- a/DuckDuckGo/DeviceAuthentication/QuartzIdleStateProvider.swift +++ b/DuckDuckGo/DeviceAuthentication/QuartzIdleStateProvider.swift @@ -19,6 +19,7 @@ import Foundation import Common import CoreGraphics +import os.log final class QuartzIdleStateProvider: DeviceIdleStateProvider { @@ -26,7 +27,7 @@ final class QuartzIdleStateProvider: DeviceIdleStateProvider { let anyInputEventType = CGEventType(rawValue: ~0)! let seconds = CGEventSource.secondsSinceLastEventType(.hidSystemState, eventType: anyInputEventType) - os_log("Idle duration since last user input event: %f", log: .autoLock, seconds) + Logger.autoLock.debug("Idle duration since last user input event: \(seconds)") return seconds } diff --git a/DuckDuckGo/Favicons/Logger+Favicons.swift b/DuckDuckGo/Favicons/Logger+Favicons.swift new file mode 100644 index 0000000000..d245415510 --- /dev/null +++ b/DuckDuckGo/Favicons/Logger+Favicons.swift @@ -0,0 +1,24 @@ +// +// Logger+Favicons.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log + +public extension Logger { + static var favicons = { Logger(subsystem: "Favicons", category: "") }() +} diff --git a/DuckDuckGo/Favicons/Model/FaviconImageCache.swift b/DuckDuckGo/Favicons/Model/FaviconImageCache.swift index 133712ccee..9b022a3d22 100644 --- a/DuckDuckGo/Favicons/Model/FaviconImageCache.swift +++ b/DuckDuckGo/Favicons/Model/FaviconImageCache.swift @@ -20,6 +20,7 @@ import Foundation import Combine import Common import BrowserServicesKit +import os.log @MainActor final class FaviconImageCache { @@ -49,9 +50,9 @@ final class FaviconImageCache { let favicons: [Favicon] do { favicons = try await storing.loadFavicons() - os_log("Favicons loaded successfully", log: .favicons) + Logger.favicons.debug("Favicons loaded successfully") } catch { - os_log("Loading of favicons failed: %s", log: .favicons, type: .error, error.localizedDescription) + Logger.favicons.error("Loading of favicons failed: \(error.localizedDescription)") throw error } @@ -76,12 +77,12 @@ final class FaviconImageCache { do { await removeFaviconsFromStore(oldFavicons) try await storing.save(favicons) - os_log("Favicon saved successfully. URL: %s", log: .favicons, favicons.map(\.url.absoluteString).description) + Logger.favicons.debug("Favicon saved successfully. URL: \(favicons.map(\.url.absoluteString).description)") await MainActor.run { NotificationCenter.default.post(name: .faviconCacheUpdated, object: nil) } } catch { - os_log("Saving of favicon failed: %s", log: .favicons, type: .error, error.localizedDescription) + Logger.favicons.error("Saving of favicon failed: \(error.localizedDescription)") } } } @@ -171,9 +172,9 @@ final class FaviconImageCache { do { try await storing.removeFavicons(favicons) - os_log("Favicons removed successfully.", log: .favicons) + Logger.favicons.debug("Favicons removed successfully.") } catch { - os_log("Removing of favicons failed: %s", log: .favicons, type: .error, error.localizedDescription) + Logger.favicons.error("Removing of favicons failed: \(error.localizedDescription)") } } diff --git a/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift b/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift index bb8be9fb8b..709d03f686 100644 --- a/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift +++ b/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift @@ -20,6 +20,7 @@ import Foundation import Combine import Common import BrowserServicesKit +import os.log @MainActor final class FaviconReferenceCache { @@ -62,12 +63,12 @@ final class FaviconReferenceCache { } loaded = true - os_log("References loaded successfully", log: .favicons) + Logger.favicons.debug("References loaded successfully") NotificationCenter.default.post(name: .faviconCacheUpdated, object: nil) }.value } catch { - os_log("Loading of references failed: %s", log: .favicons, type: .error, error.localizedDescription) + Logger.favicons.error("Loading of references failed: \(error.localizedDescription)") throw error } } @@ -268,9 +269,9 @@ final class FaviconReferenceCache { Task { do { try await self.storing.save(hostReference: hostReference) - os_log("Host reference saved successfully. host: %s", log: .favicons, hostReference.host) + Logger.favicons.debug("Host reference saved successfully. host: \(hostReference.host)") } catch { - os_log("Saving of host reference failed: %s", log: .favicons, type: .error, error.localizedDescription) + Logger.favicons.error("Saving of host reference failed: \(error.localizedDescription)") } } } @@ -295,9 +296,9 @@ final class FaviconReferenceCache { Task.detached { do { try await self.storing.save(urlReference: urlReference) - os_log("URL reference saved successfully. document URL: %s", log: .favicons, urlReference.documentUrl.absoluteString) + Logger.favicons.debug("URL reference saved successfully. document URL: \(urlReference.documentUrl.absoluteString)") } catch { - os_log("Saving of URL reference failed: %s", log: .favicons, type: .error, error.localizedDescription) + Logger.favicons.error("Saving of URL reference failed: \(error.localizedDescription)") } } } @@ -322,9 +323,9 @@ final class FaviconReferenceCache { do { try await storing.remove(hostReferences: hostReferences) - os_log("Host references removed successfully.", log: .favicons) + Logger.favicons.debug("Host references removed successfully.") } catch { - os_log("Removing of host references failed: %s", log: .favicons, type: .error, error.localizedDescription) + Logger.favicons.error("Removing of host references failed: \(error.localizedDescription)") } } @@ -342,9 +343,9 @@ final class FaviconReferenceCache { do { try await storing.remove(urlReferences: urlReferences) - os_log("URL references removed successfully.", log: .favicons) + Logger.favicons.debug("URL references removed successfully.") } catch { - os_log("Removing of URL references failed: %s", log: .favicons, type: .error, error.localizedDescription) + Logger.favicons.error("Removing of URL references failed: \(error.localizedDescription)") } } diff --git a/DuckDuckGo/Favicons/Services/FaviconStore.swift b/DuckDuckGo/Favicons/Services/FaviconStore.swift index 9ef7d9ed7d..049895add1 100644 --- a/DuckDuckGo/Favicons/Services/FaviconStore.swift +++ b/DuckDuckGo/Favicons/Services/FaviconStore.swift @@ -21,6 +21,7 @@ import CoreData import Combine import Common import PixelKit +import os.log protocol FaviconStoring { @@ -61,7 +62,7 @@ final class FaviconStore: FaviconStoring { fetchRequest.returnsObjectsAsFaults = false do { let faviconMOs = try context.fetch(fetchRequest) - os_log("%d favicons loaded ", log: .favicons, faviconMOs.count) + Logger.favicons.debug("\(faviconMOs.count) favicons loaded") let favicons = faviconMOs.compactMap { Favicon(faviconMO: $0) } continuation.resume(returning: favicons) @@ -112,7 +113,7 @@ final class FaviconStore: FaviconStoring { let faviconHostReferences: [FaviconHostReference] do { let faviconHostReferenceMOs = try context.fetch(hostFetchRequest) - os_log("%d favicon host references loaded ", log: .favicons, faviconHostReferenceMOs.count) + Logger.favicons.debug("\(faviconHostReferenceMOs.count) favicon host references loaded") faviconHostReferences = faviconHostReferenceMOs.compactMap { FaviconHostReference(faviconHostReferenceMO: $0) } } catch { continuation.resume(throwing: error) @@ -124,7 +125,7 @@ final class FaviconStore: FaviconStoring { urlFetchRequest.returnsObjectsAsFaults = false do { let faviconUrlReferenceMOs = try context.fetch(urlFetchRequest) - os_log("%d favicon url references loaded ", log: .favicons, faviconUrlReferenceMOs.count) + Logger.favicons.debug("\(faviconUrlReferenceMOs.count) favicon url references loaded") let faviconUrlReferences = faviconUrlReferenceMOs.compactMap { FaviconUrlReference(faviconUrlReferenceMO: $0) } continuation.resume(returning: (faviconHostReferences, faviconUrlReferences)) } catch { @@ -207,7 +208,7 @@ final class FaviconStore: FaviconStoring { let deletedObjects = result?.result as? [NSManagedObjectID] ?? [] let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: deletedObjects] NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context]) - os_log("%d entries of %s removed", log: .favicons, deletedObjects.count, entityName) + Logger.favicons.debug("\(deletedObjects.count) entries of \(entityName) removed") continuation.resume(returning: ()) } catch { diff --git a/DuckDuckGo/Feedback/Model/FeedbackSender.swift b/DuckDuckGo/Feedback/Model/FeedbackSender.swift index 09be2ef5a7..d261de729d 100644 --- a/DuckDuckGo/Feedback/Model/FeedbackSender.swift +++ b/DuckDuckGo/Feedback/Model/FeedbackSender.swift @@ -20,6 +20,7 @@ import Common import Foundation import Networking import PixelKit +import os.log final class FeedbackSender { @@ -43,7 +44,7 @@ final class FeedbackSender { let request = APIRequest(configuration: configuration, urlSession: URLSession.session()) request.fetch { _, error in if let error = error { - os_log("FeedbackSender: Failed to submit feedback %s", type: .error, error.localizedDescription) + Logger.general.error("FeedbackSender: Failed to submit feedback \(error.localizedDescription)") PixelKit.fire(DebugEvent(GeneralPixel.feedbackReportingFailed, error: error)) } } diff --git a/DuckDuckGo/FileDownload/Logger+FileDownload.swift b/DuckDuckGo/FileDownload/Logger+FileDownload.swift new file mode 100644 index 0000000000..304e4dc437 --- /dev/null +++ b/DuckDuckGo/FileDownload/Logger+FileDownload.swift @@ -0,0 +1,24 @@ +// +// Logger+FileDownload.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log + +extension Logger { + static var fileDownload = { Logger(subsystem: "File Download", category: "") }() +} diff --git a/DuckDuckGo/FileDownload/Model/DownloadListViewModel.swift b/DuckDuckGo/FileDownload/Model/DownloadListViewModel.swift index 8cf63751e3..64a62c902d 100644 --- a/DuckDuckGo/FileDownload/Model/DownloadListViewModel.swift +++ b/DuckDuckGo/FileDownload/Model/DownloadListViewModel.swift @@ -19,6 +19,7 @@ import Combine import Common import Foundation +import os.log @MainActor final class DownloadListViewModel { @@ -41,7 +42,7 @@ final class DownloadListViewModel { } private func handleDownloadsUpdate(of kind: DownloadListCoordinator.UpdateKind, item: DownloadListItem) { - os_log(.debug, log: .downloads, "DownloadListViewModel: .\(kind) \(item.identifier)") + Logger.fileDownload.debug("DownloadListViewModel: .\(String(describing: kind)) \(item.identifier)") dispatchPrecondition(condition: .onQueue(.main)) switch kind { diff --git a/DuckDuckGo/FileDownload/Model/DownloadViewModel.swift b/DuckDuckGo/FileDownload/Model/DownloadViewModel.swift index d22e8a9e6e..ecffbaedfb 100644 --- a/DuckDuckGo/FileDownload/Model/DownloadViewModel.swift +++ b/DuckDuckGo/FileDownload/Model/DownloadViewModel.swift @@ -20,6 +20,7 @@ import Combine import Common import Foundation import UniformTypeIdentifiers +import os.log final class DownloadViewModel { @@ -80,7 +81,7 @@ final class DownloadViewModel { let oldState = self.state let newState = State(item: item, shouldAnimateOnAppear: state.shouldAnimateOnAppear ?? true) if oldState != newState { - os_log(.debug, log: .downloads, "DownloadViewModel: \(item.identifier): \(oldState) ➡️ \(newState)") + Logger.fileDownload.debug("DownloadViewModel: \(item.identifier.uuidString): \(oldState.debugDescription) ➡️ \(newState.debugDescription)") self.state = newState } } diff --git a/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift b/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift index 8a02089430..fe9bb4f873 100644 --- a/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift +++ b/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift @@ -21,6 +21,7 @@ import Combine import Common import Navigation import UniformTypeIdentifiers +import os.log protocol FileDownloadManagerProtocol: AnyObject { var downloads: Set { get } @@ -47,14 +48,9 @@ final class FileDownloadManager: FileDownloadManagerProtocol { static let shared = FileDownloadManager() private let preferences: DownloadsPreferences - private let getLogger: (() -> OSLog) - private var log: OSLog { - getLogger() - } - init(preferences: DownloadsPreferences = .shared, log: @autoclosure @escaping (() -> OSLog) = .downloads) { + init(preferences: DownloadsPreferences = .shared) { self.preferences = preferences - self.getLogger = log } private(set) var downloads = Set() @@ -76,16 +72,16 @@ final class FileDownloadManager: FileDownloadManagerProtocol { destination = .prompt } let task = WebKitDownloadTask(download: download, destination: destination, isBurner: fromBurnerWindow) - os_log(log: log, "add \(download): \(download.originalRequest?.url?.absoluteString ?? "") -> \(destination): \(task)") + Logger.fileDownload.debug("add \(String(describing: download)): \(download.originalRequest?.url?.absoluteString ?? "") -> \(destination.debugDescription): \(task)") let shouldCancelDownloadIfDelegateIsGone = delegate != nil - self.downloadTaskDelegates[task] = { [weak delegate, log] in + self.downloadTaskDelegates[task] = { [weak delegate] in if let delegate { return delegate } // if the delegate was originally provided but deallocated since then – the download task should be cancelled if shouldCancelDownloadIfDelegateIsGone { - os_log(log: log, "🦀 \(download) delegate is gone: cancelling") + Logger.fileDownload.debug("🦀 \( String(describing: download) ) delegate is gone: cancelling") return CancelledDownloadTaskDelegate() } return nil @@ -101,7 +97,7 @@ final class FileDownloadManager: FileDownloadManagerProtocol { func cancelAll(waitUntilDone: Bool) { dispatchPrecondition(condition: .onQueue(.main)) - os_log(log: log, "FileDownloadManager: cancel all: [\(downloads.map(\.debugDescription).joined(separator: ", "))]") + Logger.fileDownload.debug("FileDownloadManager: cancel all: [\(self.downloads.map(\.debugDescription).joined(separator: ", "))]") let dispatchGroup: DispatchGroup? = waitUntilDone ? DispatchGroup() : nil var cancellables = Set() for task in downloads { @@ -131,10 +127,10 @@ extension FileDownloadManager: WebKitDownloadTaskDelegate { @MainActor func fileDownloadTaskNeedsDestinationURL(_ task: WebKitDownloadTask, suggestedFilename: String, suggestedFileType fileType: UTType?) async -> (URL?, UTType?) { guard case (.some(let url), let fileType) = await chooseDestination(for: task, suggestedFilename: suggestedFilename, suggestedFileType: fileType) else { - os_log(log: log, "choose destination cancelled: \(task)") + Logger.fileDownload.debug("choose destination cancelled: \(task)") return (nil, nil) } - os_log(log: log, "destination chosen: \(task): \"\(url.path)\" (\(fileType?.description ?? "nil"))") + Logger.fileDownload.debug("destination chosen: \(task): \"\(url.path)\" (\(fileType?.description ?? "nil"))") if let originalRect = self.downloadTaskDelegates[task]?()?.fileIconFlyAnimationOriginalRect(for: task) { let utType = UTType(filenameExtension: url.pathExtension) ?? fileType ?? .data @@ -191,7 +187,7 @@ extension FileDownloadManager: WebKitDownloadTaskDelegate { fileTypes.append(fileType) } - os_log(log: log, "FileDownloadManager: requesting download location \"\(suggestedFilename)\"/\(fileTypes.map(\.description).joined(separator: ", "))") + Logger.fileDownload.debug("FileDownloadManager: requesting download location \"\(suggestedFilename)\"/\(fileTypes.map(\.description).joined(separator: ", "))") delegate.chooseDestination(suggestedFilename: suggestedFilename, fileTypes: fileTypes) { url, fileType in guard let url else { completionHandler(nil, nil) @@ -216,13 +212,13 @@ extension FileDownloadManager: WebKitDownloadTaskDelegate { let fileName = suggestedFilename.isEmpty ? "download".appendingPathExtension(fileType?.preferredFilenameExtension) : suggestedFilename var url = downloadLocation.appendingPathComponent(fileName) - os_log(log: log, "FileDownloadManager: using default download location for \"\(suggestedFilename)\": \"\(url.path)\"") + Logger.fileDownload.debug("FileDownloadManager: using default download location for \"\(suggestedFilename)\": \"\(url.path)\"") // make sure the app has access to the destination let folderUrl = url.deletingLastPathComponent() let fm = FileManager.default guard fm.isWritableFile(atPath: folderUrl.path) else { - os_log(log: log, "FileDownloadManager: no write permissions for \"\(folderUrl.path)\": fallback to user request") + Logger.fileDownload.debug("FileDownloadManager: no write permissions for \"\(folderUrl.path)\": fallback to user request") return await requestDestinationFromUser(for: task, suggestedFilename: suggestedFilename, suggestedFileType: fileType) } @@ -246,7 +242,7 @@ extension FileDownloadManager: WebKitDownloadTaskDelegate { self.downloads.remove(task) self.downloadTaskDelegates[task] = nil - os_log(log: log, "❎ removed task \(task)") + Logger.fileDownload.debug("❎ removed task \(task)") } } diff --git a/DuckDuckGo/FileDownload/Model/FilePresenter.swift b/DuckDuckGo/FileDownload/Model/FilePresenter.swift index 1770935af2..a7205791c3 100644 --- a/DuckDuckGo/FileDownload/Model/FilePresenter.swift +++ b/DuckDuckGo/FileDownload/Model/FilePresenter.swift @@ -19,25 +19,15 @@ import Combine import Common import Foundation +import os.log private protocol FilePresenterDelegate: AnyObject { - var logger: FilePresenterLogger { get } var url: URL? { get } func presentedItemDidMove(to newURL: URL) func accommodatePresentedItemDeletion() throws func accommodatePresentedItemEviction() throws } -protocol FilePresenterLogger { - func log(_ message: @autoclosure () -> String) -} - -extension OSLog: FilePresenterLogger { - func log(_ message: @autoclosure () -> String) { - os_log(.debug, log: self, message()) - } -} - internal class FilePresenter { private static let dispatchSourceQueue = DispatchQueue(label: "CoordinatedFile.dispatchSourceQueue") @@ -111,8 +101,6 @@ internal class FilePresenter { private var innerPresenters = [DelegatingFilePresenter]() private var dispatchSourceCancellable: AnyCancellable? - fileprivate let logger: any FilePresenterLogger - private var urlController: SecurityScopedFileURLController? final var url: URL? { lock.withLock { @@ -140,7 +128,7 @@ internal class FilePresenter { urlController.isManagingSecurityScope { urlController.updateUrlKeepingSandboxExtensionRetainCount(newURL) } else { - urlController = SecurityScopedFileURLController(url: newURL, logger: logger) + urlController = SecurityScopedFileURLController(url: newURL) } return oldValue @@ -155,14 +143,12 @@ internal class FilePresenter { /// - Parameter consumeUnbalancedStartAccessingResource: assume the `url` is already accessible (e.g. after choosing the file using Open Panel). /// would cause an unbalanced `stopAccessingSecurityScopedResource` call on the File Presenter deallocation. /// - Note: see https://stackoverflow.com/questions/25627628/sandboxed-mac-app-exhausting-security-scoped-url-resources - init(url: URL, consumeUnbalancedStartAccessingResource: Bool = false, logger: FilePresenterLogger = OSLog.disabled, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { - self.urlController = SecurityScopedFileURLController(url: url, manageSecurityScope: consumeUnbalancedStartAccessingResource, logger: logger) - - self.logger = logger + init(url: URL, consumeUnbalancedStartAccessingResource: Bool = false, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { + self.urlController = SecurityScopedFileURLController(url: url, manageSecurityScope: consumeUnbalancedStartAccessingResource) do { try setupInnerPresenter(for: url, primaryItemURL: nil, createIfNeededCallback: createIfNeededCallback) - logger.log("🗄️ added file presenter for \"\(url.path)\"") + Logger.fileDownload.debug("🗄️ added file presenter for \"\(url.path)\"") } catch { removeFilePresenters() throw error @@ -176,13 +162,12 @@ internal class FilePresenter { /// - Note: when presenting a related item the security scoped resource access will always be stopped on the File Presenter deallocation /// - Parameter consumeUnbalancedStartAccessingResource: assume the `url` is already accessible (e.g. after choosing the file using Open Panel). /// would cause an unbalanced `stopAccessingSecurityScopedResource` call on the File Presenter deallocation. - init(url: URL, primaryItemURL: URL, logger: FilePresenterLogger = OSLog.disabled, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { - self.urlController = SecurityScopedFileURLController(url: url, logger: logger) - self.logger = logger + init(url: URL, primaryItemURL: URL, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { + self.urlController = SecurityScopedFileURLController(url: url) do { try setupInnerPresenter(for: url, primaryItemURL: primaryItemURL, createIfNeededCallback: createIfNeededCallback) - logger.log("🗄️ added file presenter for \"\(url.path) primary item: \"\(primaryItemURL.path)\"") + Logger.fileDownload.debug("🗄️ added file presenter for \"\(url.path) primary item: \"\(primaryItemURL.path)\"") } catch { removeFilePresenters() throw error @@ -203,7 +188,7 @@ internal class FilePresenter { guard let createFile = createIfNeededCallback else { throw CocoaError(.fileReadNoSuchFile, userInfo: [NSFilePathErrorKey: url.path]) } - logger.log("🗄️💥 creating file for presenter at \"\(url.path)\"") + Logger.fileDownload.debug("🗄️💥 creating file for presenter at \"\(url.path)\"") // create new file at the presented URL using the provided callback and update URL if needed _=self._setURL( try coordinateWrite(at: url, using: createFile) @@ -237,7 +222,7 @@ internal class FilePresenter { guard fileDescriptor != -1 else { let err = errno - logger.log("🗄️❌ error opening \(url.path): \(err) – \(String(cString: strerror(err)))") + Logger.fileDownload.debug("🗄️❌ error opening \(url.path): \(err) – \(String(cString: strerror(err)))") return } @@ -246,7 +231,7 @@ internal class FilePresenter { dispatchSource.setEventHandler { [weak self] in guard let self, let url = self.url else { return } - self.logger.log("🗄️⚠️ file delete event handler: \"\(url.path)\"") + Logger.fileDownload.debug("🗄️⚠️ file delete event handler: \"\(url.path)\"") var resolvedBookmarkData: URL? { var isStale = false guard let presenter = self as? BookmarkFilePresenter, @@ -258,7 +243,7 @@ internal class FilePresenter { return url } if let existingUrl = resolvedBookmarkData { - self.logger.log("🗄️⚠️ ignoring file delete event handler as file exists: \"\(url.path)\"") + Logger.fileDownload.debug("🗄️⚠️ ignoring file delete event handler as file exists: \"\(url.path)\"") presentedItemDidMove(to: existingUrl) return } @@ -281,7 +266,7 @@ internal class FilePresenter { if innerPresenter.fallbackPresentedItemURL == nil { innerPresenter.fallbackPresentedItemURL = urlController?.url } - logger.log("🗄️ removing file presenter \(idx) for \"\(innerPresenter.presentedItemURL?.path ?? "")\"") + Logger.fileDownload.debug("🗄️ removing file presenter \(idx) for \"\(innerPresenter.presentedItemURL?.path ?? "")\"") NSFileCoordinator.removeFilePresenter(innerPresenter) } if innerPresenters.count > 1 { @@ -294,7 +279,7 @@ internal class FilePresenter { fileprivate func didSetURL(_ newValue: URL?, oldValue: URL?) { assert(newValue == nil || newValue != oldValue) - logger.log("🗄️ did update url from \"\(oldValue?.path ?? "")\" to \"\(newValue?.path ?? "")\"") + Logger.fileDownload.debug("🗄️ did update url from \"\(oldValue?.path ?? "")\" to \"\(newValue?.path ?? "")\"") urlSubject.send(newValue) } @@ -307,19 +292,19 @@ internal class FilePresenter { extension FilePresenter: FilePresenterDelegate { func presentedItemDidMove(to newURL: URL) { - logger.log("🗄️ presented item did move to \"\(newURL.path)\"") + Logger.fileDownload.debug("🗄️ presented item did move to \"\(newURL.path)\"") setURL(newURL) } func accommodatePresentedItemDeletion() throws { - logger.log("🗄️ accommodatePresentedItemDeletion (\"\(url?.path ?? "")\")") + Logger.fileDownload.debug("🗄️ accommodatePresentedItemDeletion (\"\(self.url?.path ?? "")\")") // should go before resetting the URL to correctly remove File Presenter removeFilePresenters() setURL(nil) } func accommodatePresentedItemEviction() throws { - logger.log("🗄️ accommodatePresentedItemEviction (\"\(url?.path ?? "")\")") + Logger.fileDownload.debug("🗄️ accommodatePresentedItemEviction (\"\(self.url?.path ?? "")\")") try accommodatePresentedItemDeletion() } @@ -345,17 +330,17 @@ final class BookmarkFilePresenter: FilePresenter { /// - Parameter url: represented file URL access to which is coordinated by the File Presenter. /// - Parameter consumeUnbalancedStartAccessingResource: assume the `url` is already accessible (e.g. after choosing the file using Open Panel). /// would cause an unbalanced `stopAccessingSecurityScopedResource` call on the File Presenter deallocation. - override init(url: URL, consumeUnbalancedStartAccessingResource: Bool = false, logger: FilePresenterLogger = OSLog.disabled, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { + override init(url: URL, consumeUnbalancedStartAccessingResource: Bool = false, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { - try super.init(url: url, consumeUnbalancedStartAccessingResource: consumeUnbalancedStartAccessingResource, logger: logger, createIfNeededCallback: createIfNeededCallback) + try super.init(url: url, consumeUnbalancedStartAccessingResource: consumeUnbalancedStartAccessingResource, createIfNeededCallback: createIfNeededCallback) do { try self.coordinateRead(at: url, with: .withoutChanges) { url in - logger.log("📒 updating bookmark data for \"\(url.path)\"") + Logger.fileDownload.debug("📒 updating bookmark data for \"\(url.path)\"") self._fileBookmarkData = try url.bookmarkData(options: .withSecurityScope) } } catch { - logger.log("📕 bookmark data retreival failed for \"\(url.path)\": \(error)") + Logger.fileDownload.debug("📕 bookmark data retreival failed for \"\(url.path)\": \(error)") throw error } } @@ -367,28 +352,28 @@ final class BookmarkFilePresenter: FilePresenter { /// - Note: when presenting a related item the security scoped resource access will always be stopped on the File Presenter deallocation /// - Parameter consumeUnbalancedStartAccessingResource: assume the `url` is already accessible (e.g. after choosing the file using Open Panel). /// would cause an unbalanced `stopAccessingSecurityScopedResource` call on the File Presenter deallocation. - override init(url: URL, primaryItemURL: URL, logger: FilePresenterLogger = OSLog.disabled, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { - try super.init(url: url, primaryItemURL: primaryItemURL, logger: logger, createIfNeededCallback: createIfNeededCallback) + override init(url: URL, primaryItemURL: URL, createIfNeededCallback: ((URL) throws -> URL)? = nil) throws { + try super.init(url: url, primaryItemURL: primaryItemURL, createIfNeededCallback: createIfNeededCallback) do { try self.coordinateRead(at: url, with: .withoutChanges) { url in - logger.log("📒 updating bookmark data for \"\(url.path)\"") + Logger.fileDownload.debug("📒 updating bookmark data for \"\(url.path)\"") self._fileBookmarkData = try url.bookmarkData(options: .withSecurityScope) } } catch { - logger.log("📕 bookmark data retreival failed for \"\(url.path)\": \(error)") + Logger.fileDownload.debug("📕 bookmark data retreival failed for \"\(url.path)\": \(error)") throw error } } - init(fileBookmarkData: Data, logger: FilePresenterLogger = OSLog.disabled) throws { + init(fileBookmarkData: Data) throws { self._fileBookmarkData = fileBookmarkData var isStale = false - logger.log("📒 resolving url from bookmark data") + Logger.fileDownload.debug("📒 resolving url from bookmark data") let url = try URL(resolvingBookmarkData: fileBookmarkData, options: .withSecurityScope, bookmarkDataIsStale: &isStale) - try super.init(url: url, consumeUnbalancedStartAccessingResource: true, logger: logger) + try super.init(url: url, consumeUnbalancedStartAccessingResource: true) if isStale { DispatchQueue.global().async { [weak self] in @@ -403,13 +388,13 @@ final class BookmarkFilePresenter: FilePresenter { } fileprivate func updateFileBookmarkData(for url: URL?) { - logger.log("📒 updateFileBookmarkData for \"\(url?.path ?? "")\"") + Logger.fileDownload.debug("📒 updateFileBookmarkData for \"\(url?.path ?? "")\"") var fileBookmarkData: Data? do { fileBookmarkData = try url?.bookmarkData(options: .withSecurityScope) } catch { - logger.log("📕 updateFileBookmarkData failed with \(error)") + Logger.fileDownload.debug("📕 updateFileBookmarkData failed with \(error)") } guard lock.withLock({ diff --git a/DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift b/DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift index bec94e02ac..154e4e8671 100644 --- a/DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift +++ b/DuckDuckGo/FileDownload/Model/SecurityScopedFileURLController.swift @@ -18,6 +18,8 @@ import Foundation import Common +import os.log +import BrowserServicesKit /// Manages security-scoped resource access to a file URL. /// @@ -29,9 +31,6 @@ import Common /// `stopAccessingSecurityScopedResource` methods to accurately reflect the current number of start and stop calls. /// The number is reflected in the associated `URL.sandboxExtensionRetainCount` value. final class SecurityScopedFileURLController { - - fileprivate let logger: any FilePresenterLogger - private(set) var url: URL let isManagingSecurityScope: Bool @@ -42,7 +41,7 @@ final class SecurityScopedFileURLController { /// - manageSecurityScope: A Boolean value indicating whether the controller should manage the URL security scope access (i.e. call stop and end accessing resource methods). /// - logger: An optional logger instance for logging file operations. Defaults to disabled. /// - Note: when `manageSecurityScope` is `true` access to the represented URL will be stopped for the whole app on the controller deallocation. - init(url: URL, manageSecurityScope: Bool = true, logger: any FilePresenterLogger = OSLog.disabled) { + init(url: URL, manageSecurityScope: Bool = true) { assert(url.isFileURL) #if APPSTORE let didStartAccess = manageSecurityScope && url.startAccessingSecurityScopedResource() @@ -51,8 +50,7 @@ final class SecurityScopedFileURLController { #endif self.url = url self.isManagingSecurityScope = didStartAccess - self.logger = logger - logger.log("\(didStartAccess ? "🧪 " : "")SecurityScopedFileURLController.init: \(url.sandboxExtensionRetainCount) – \"\(url.path)\"") + Logger.fileDownload.debug("\(didStartAccess ? "🧪 " : "")SecurityScopedFileURLController.init: \(url.sandboxExtensionRetainCount) – \"\(url.path)\"") } func updateUrlKeepingSandboxExtensionRetainCount(_ newURL: URL) { @@ -67,7 +65,7 @@ final class SecurityScopedFileURLController { deinit { if isManagingSecurityScope { let url = url - logger.log("\(isManagingSecurityScope ? "🪓 " : "")SecurityScopedFileURLController.deinit: \(url.sandboxExtensionRetainCount) – \"\(url.path)\"") + Logger.fileDownload.debug("\(self.isManagingSecurityScope ? "🪓 " : "")SecurityScopedFileURLController.deinit: \(url.sandboxExtensionRetainCount) – \"\(url.path)\"") for _ in 0..<(url as NSURL).sandboxExtensionRetainCount { url.stopAccessingSecurityScopedResource() } @@ -75,7 +73,7 @@ final class SecurityScopedFileURLController { #if DEBUG && APPSTORE url.ensureUrlIsNotWritable { #if SANDBOX_TEST_TOOL - logger.log("❗️ url \(url.path) is still writable after stopping access to it") + Logger.fileDownload.log("❗️ url \(url.path) is still writable after stopping access to it") fatalError("❗️ url \(url.path) is still writable after stopping access to it") #else breakByRaisingSigInt("❗️ url \(url.path) is still writable after stopping access to it") diff --git a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift index 077b4cda82..54beae31f0 100644 --- a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift +++ b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift @@ -16,13 +16,15 @@ // limitations under the License. // +import Foundation import Combine +import BrowserServicesKit import Common -import Foundation import Navigation import UniformTypeIdentifiers import WebKit import PixelKit +import os.log protocol WebKitDownloadTaskDelegate: AnyObject { func fileDownloadTaskNeedsDestinationURL(_ task: WebKitDownloadTask, suggestedFilename: String, suggestedFileType: UTType?) async -> (URL?, UTType?) @@ -101,7 +103,6 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable /// downloads initiated from a Burner Window won‘t stay in the Downloads panel after completion let isBurner: Bool - private let log: OSLog private weak var delegate: WebKitDownloadTaskDelegate? private var download: WebKitDownload! @@ -130,17 +131,16 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable } @MainActor(unsafe) - init(download: WebKitDownload, destination: DownloadDestination, isBurner: Bool, log: OSLog = .downloads) { + init(download: WebKitDownload, destination: DownloadDestination, isBurner: Bool) { self.download = download self.progress = DownloadProgress(download: download) self.fileProgressPresenter = FileProgressPresenter(progress: progress) self.state = .initial(destination) self.isBurner = isBurner - self.log = log super.init() - progress.cancellationHandler = { [weak self, log, taskDescr=self.debugDescription] in - os_log(log: log, "❌ progress.cancellationHandler \(taskDescr)") + progress.cancellationHandler = { [weak self, taskDescr=self.debugDescription] in + Logger.fileDownload.debug("❌ progress.cancellationHandler \(taskDescr)") self?.cancel() } } @@ -159,7 +159,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable /// - if the temporary file is renamed, we try to rename the destination file accordingly (this would fail in sandboxed builds for non-preset directories) /// - if any of the two files is removed, the download is cancelled @MainActor func start(delegate: WebKitDownloadTaskDelegate) { - os_log(.debug, log: log, "🟢 start \(self)") + Logger.fileDownload.debug("🟢 start \(self)") self.delegate = delegate @@ -212,13 +212,13 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable destinationURL != newDestinationURL else { try? await Task.sleep(interval: 1); return } do { - os_log(.debug, log: log, "renaming destination file \"\(destinationURL.path)\" ➡️ \"\(destinationURL.path)\"") + Logger.fileDownload.debug("renaming destination file \"\(destinationURL.path)\" ➡️ \"\(destinationURL.path)\"") try destinationFilePresenter.coordinateMove(to: newDestinationURL, with: []) { from, to in try FileManager.default.moveItem(at: from, to: to) destinationFilePresenter.presentedItemDidMove(to: newDestinationURL) // coordinated File Presenter won‘t receive URL updates } } catch { - os_log(.debug, log: log, "renaming file failed: \(error)") + Logger.fileDownload.debug("renaming file failed: \(error)") } } @@ -235,7 +235,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable destinationURL.stopAccessingSecurityScopedResource() } } - os_log(.debug, log: log, "download task callback: creating temp directory for \"\(destinationURL.path)\"") + Logger.fileDownload.debug("download task callback: creating temp directory for \"\(destinationURL.path)\"") switch cleanupStyle { case .remove: @@ -245,7 +245,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable // validate we can write to the directory even if there‘s no existing file try Data().write(to: destinationURL) } - os_log(.debug, log: log, "🧹 removing \"\(url.path)\"") + Logger.fileDownload.debug("🧹 removing \"\(url.path)\"") try fm.removeItem(at: url) } case .clear: @@ -266,12 +266,12 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable } }) - os_log(.debug, log: log, "download task callback: temp file: \(tempURL.path)") + Logger.fileDownload.debug("download task callback: temp file: \(tempURL.path)") return tempURL } catch { await MainActor.run { - os_log(.error, log: log, "🛑 download task callback: \(self): \(error)") + Logger.fileDownload.error("🛑 download task callback: \(self): \(error)") self.download.cancel() self.finish(with: .failure(.failedToCompleteDownloadTask(underlyingError: error, resumeData: nil, isRetryable: false))) @@ -290,7 +290,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable let fileDescriptor = open(itemReplacementDirectory.path, O_EVTONLY) if fileDescriptor == -1 { let err = errno - os_log(.error, log: log, "could not open \(itemReplacementDirectory.path): \(err) – \(String(cString: strerror(err)))") + Logger.fileDownload.error("could not open \(itemReplacementDirectory.path): \(err) – \(String(cString: strerror(err)))") throw NSError(domain: NSPOSIXErrorDomain, code: Int(err), userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(err))]) } let fileMonitor = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .write, queue: .main) @@ -317,7 +317,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable /// when the download has started to a temporary directory, create a placeholder file at the destination URL and move the temp file to the destination directory as a `.duckload` @MainActor private func tempDownloadFileCreated(at tempURL: URL, destinationURL: URL) async { - os_log(.debug, log: log, "temp file created: \(self): \(tempURL.path)") + Logger.fileDownload.debug("temp file created: \(self): \(tempURL.path)") do { let presenters = if case .downloading(destination: let destination, tempFile: let tempFile) = state { @@ -330,7 +330,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable self.state = .downloading(destination: presenters.destinationFile, tempFile: presenters.tempFile) } catch { - os_log(.error, log: log, "🛑 file presenters failure: \(self): \(error)") + Logger.fileDownload.error("🛑 file presenters failure: \(self): \(error)") self.download.cancel() self.finish(with: .failure(.failedToCompleteDownloadTask(underlyingError: error, resumeData: nil, isRetryable: false))) @@ -347,10 +347,10 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable // 🧙‍♂️ now we‘re doing do some magique here 🧙‍♂️ // -------------------------------------- - os_log(.debug, log: log, "🧙‍♂️ magique.start: \"\(destinationURL.path)\" (\"\(duckloadURL.path)\") directory writable: \(fm.isWritableFile(atPath: destinationURL.deletingLastPathComponent().path))") + Logger.fileDownload.debug("🧙‍♂️ magique.start: \"\(destinationURL.path)\" (\"\(duckloadURL.path)\") directory writable: \(fm.isWritableFile(atPath: destinationURL.deletingLastPathComponent().path))") // 1. create our final destination file (let‘s say myfile.zip) and setup a File Presenter for it // doing this we preserve access to the file until it‘s actually downloaded - let destinationFilePresenter = try BookmarkFilePresenter(url: destinationURL, consumeUnbalancedStartAccessingResource: true, logger: log) { url in + let destinationFilePresenter = try BookmarkFilePresenter(url: destinationURL, consumeUnbalancedStartAccessingResource: true) { url in try fm.createFile(atPath: url.path, contents: nil) ? url : { throw CocoaError(.fileWriteNoPermission, userInfo: [NSFilePathErrorKey: url.path]) }() @@ -363,7 +363,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable // 2. mark the file as hidden until it‘s downloaded to not to confuse user // and prevent from unintentional opening of the empty file try destinationURL.setFileHidden(true) - os_log(.debug, log: log, "🧙‍♂️ \"\(destinationURL.path)\" hidden") + Logger.fileDownload.debug("🧙‍♂️ \"\(destinationURL.path)\" hidden") // 3. then we move the temporary download file to the destination directory (myfile.duckload) // this is doable in sandboxed builds by using “Related Items” i.e. using a file URL with an extra @@ -377,29 +377,29 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable try chooseAlternativeDuckloadFileNameOrRemove(&duckloadURL, destinationURL: destinationURL) } catch { // that‘s ok, we‘ll keep using the original temp file - os_log(.error, log: log, "❗️ can‘t resolve duckload file exists: \"\(duckloadURL.path)\": \(error)") + Logger.fileDownload.error("❗️ can‘t resolve duckload file exists: \"\(duckloadURL.path)\": \(error)") duckloadURL = tempURL } } let tempFilePresenter = if duckloadURL == tempURL { // we won‘t use a `.duckload` file for this download, the file will be left in the temp location instead - try BookmarkFilePresenter(url: duckloadURL, logger: log) + try BookmarkFilePresenter(url: duckloadURL) } else { // now move the temp file to `.duckload` instantiating a File Presenter with it - try BookmarkFilePresenter(url: duckloadURL, primaryItemURL: destinationURL, logger: log) { [log] duckloadURL in + try BookmarkFilePresenter(url: duckloadURL, primaryItemURL: destinationURL) { duckloadURL in do { try fm.moveItem(at: tempURL, to: duckloadURL) return duckloadURL } catch { // fallback: move failed, keep the temp file in the original location - os_log(.error, log: log, "🙁 fallback with \(error), will use \(tempURL.path)") + Logger.fileDownload.error("🙁 fallback with \(error), will use \(tempURL.path)") PixelKit.fire(DebugEvent(GeneralPixel.fileAccessRelatedItemFailed, error: error)) return tempURL } } } - os_log(.debug, log: log, "🧙‍♂️ \"\(duckloadURL.path)\" (\"\(tempFilePresenter.url?.path ?? "")\") ready") + Logger.fileDownload.debug("🧙‍♂️ \"\(duckloadURL.path)\" (\"\(tempFilePresenter.url?.path ?? "")\") ready") return (tempFile: tempFilePresenter, destinationFile: destinationFilePresenter) } @@ -426,7 +426,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable } } - os_log(.debug, log: log, "removing temp file \"\(duckloadURL.path)\"") + Logger.fileDownload.debug("Removing temp file") try FilePresenter(url: duckloadURL, primaryItemURL: destinationURL).coordinateWrite(with: .forDeleting) { duckloadURL in try fm.removeItem(at: duckloadURL) } @@ -449,9 +449,9 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable } return } - os_log(.debug, log: log, "cancel \(self)") - download.cancel { [weak self, log, taskDescr=self.debugDescription] resumeData in - os_log(.debug, log: log, "\(taskDescr): download.cancel callback") + Logger.fileDownload.debug("cancel \(self)") + download.cancel { [weak self, taskDescr=self.debugDescription] resumeData in + Logger.fileDownload.debug("\(taskDescr): download.cancel callback") DispatchQueue.main.asyncOrNow { self?.downloadDidFail(with: URLError(.cancelled), resumeData: resumeData) } @@ -467,7 +467,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable switch result { case .success(let presenter): let url = presenter.url - os_log(.debug, log: log, "finish \(self) with .success(\"\(url?.path ?? "")\")") + Logger.fileDownload.debug("finish \(self) with .success(\"\(url?.path ?? "")\")") if progress.totalUnitCount == -1 { progress.totalUnitCount = max(1, self.progress.completedUnitCount) @@ -477,7 +477,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable self.state = .downloaded(presenter) case .failure(let error): - os_log(.debug, log: log, "finish \(self) with .failure(\(error))") + Logger.fileDownload.debug("finish \(self) with .failure(\(error))") self.state = .failed(destination: error.isRetryable ? self.state.destinationFilePresenter : nil, // stop tracking removed files for non-retryable downloads tempFile: error.isRetryable ? self.state.tempFilePresenter : nil, @@ -492,8 +492,8 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable // don‘t remove itemReplacementDirectory if we‘re keeping the temp file in it for a retryable error self.state.tempFilePresenter?.url?.path.hasPrefix(itemReplacementDirectory.path) != true { - DispatchQueue.global().async { [log, itemReplacementDirectory] in - os_log(.debug, log: log, "removing \"\(itemReplacementDirectory.path)\"") + DispatchQueue.global().async { [itemReplacementDirectory] in + Logger.fileDownload.debug("removing \"\(itemReplacementDirectory.path)\"") try? FileManager.default.removeItem(at: itemReplacementDirectory) } self.itemReplacementDirectory = nil @@ -508,7 +508,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable self.finish(with: .failure(.failedToCompleteDownloadTask(underlyingError: error, resumeData: nil, isRetryable: false))) return } - os_log(.debug, log: log, "ignoring `cancel` for already completed task \(self)") + Logger.fileDownload.debug("ignoring `cancel` for already completed task \(self)") return } @@ -520,14 +520,14 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable true } - os_log(.debug, log: log, "❗️ downloadDidFail \(self): \(error), retryable: \(isRetryable)") + Logger.fileDownload.debug("❗️ downloadDidFail \(self): \(error), retryable: \(isRetryable)") self.finish(with: .failure(.failedToCompleteDownloadTask(underlyingError: error, resumeData: resumeData, isRetryable: isRetryable))) if !isRetryable { - DispatchQueue.global().async { [log, itemReplacementDirectory] in + DispatchQueue.global().async { [itemReplacementDirectory] in let fm = FileManager() try? destinationFile.coordinateWrite(with: .forDeleting) { url in - os_log(.debug, log: log, "removing \"\(url.path)\"") + Logger.fileDownload.debug("removing \"\(url.path)\"") try fm.removeItem(at: url) } try? tempFile.coordinateWrite(with: .forDeleting) { url in @@ -536,7 +536,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable if let itemReplacementDirectory, url.path.hasPrefix(itemReplacementDirectory.path) { url = itemReplacementDirectory } - os_log(.debug, log: log, "removing \"\(url.path)\"") + Logger.fileDownload.debug("removing \"\(url.path)\"") try fm.removeItem(at: url) } } @@ -570,7 +570,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable #if DEBUG let downloadDescr = download.debugDescription @MainActor(unsafe) func performRegardlessOfMainThread() { - os_log(.debug, log: log, ".deinit") + Logger.fileDownload.debug(".deinit") assert(state.isCompleted, "FileDownloadTask is deallocated without finish(with:) been called") } performRegardlessOfMainThread() @@ -594,7 +594,7 @@ extension WebKitDownloadTask: WKDownloadDelegate { @MainActor func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String) async -> URL? { - os_log(.debug, log: log, "decide destination \(self)") + Logger.fileDownload.debug("decide destination \(self)") guard let delegate = delegate else { assertionFailure("WebKitDownloadTask: delegate is gone") @@ -649,7 +649,7 @@ extension WebKitDownloadTask: WKDownloadDelegate { willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, decisionHandler: @escaping (WKDownload.RedirectPolicy) -> Void) { - os_log(log: log, "will perform HTTP redirection \(self): \(response) to \(request)") + Logger.fileDownload.debug("will perform HTTP redirection \(self): \(response) to \(request)") decisionHandler(.allow) } @@ -657,7 +657,7 @@ extension WebKitDownloadTask: WKDownloadDelegate { func download(_ download: WKDownload, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - os_log(log: log, "did receive challenge \(self): \(challenge)") + Logger.fileDownload.debug("did receive challenge \(self): \(challenge)") download.webView?.navigationDelegate?.webView?(download.webView!, didReceive: challenge, completionHandler: completionHandler) ?? { completionHandler(.performDefaultHandling, nil) }() @@ -670,21 +670,21 @@ extension WebKitDownloadTask: WKDownloadDelegate { guard case .downloading(destination: let destinationFile, tempFile: let tempFile) = self.state else { // if we receive `downloadDidFinish:` before the File Presenters are set up (async) // - we‘ll be waiting for the `.downloading` state to come in with the presenters - os_log(log: log, "🏁 download did finish too early, we‘ll wait for the `.downloading` state: \(self)") + Logger.fileDownload.debug("🏁 download did finish too early, we‘ll wait for the `.downloading` state: \(self)") assert(itemReplacementDirectory != nil, "itemReplacementDirectory should be set") waitForDownloadDidStart { [weak self] in self?.downloadDidFinish(download) } return } - os_log(log: log, "🏁 download did finish: \(self)") + Logger.fileDownload.debug("🏁 download did finish: \(self)") do { try tempFile.coordinateWrite(with: .forMoving) { tempURL in // replace destination file with temp file - try destinationFile.coordinateWrite(with: .forReplacing) { [log] destinationURL in + try destinationFile.coordinateWrite(with: .forReplacing) { destinationURL in if destinationURL != tempURL { // could be a corner-case when downloading a `.duckload` file - os_log(.debug, log: log, "replacing \"\(destinationURL.path)\" with \"\(tempURL.path)\"") + Logger.fileDownload.debug("replacing \"\(destinationURL.path)\" with \"\(tempURL.path)\"") _=try fm.replaceItemAt(destinationURL, withItemAt: tempURL) } // set quarantine attributes @@ -693,7 +693,7 @@ extension WebKitDownloadTask: WKDownloadDelegate { } // remove temp file item replacement directory if present if let itemReplacementDirectory { - os_log(.debug, log: log, "removing \(itemReplacementDirectory.path)") + Logger.fileDownload.debug("removing \(itemReplacementDirectory.path)") try? fm.removeItem(at: itemReplacementDirectory) self.itemReplacementDirectory = nil } @@ -701,7 +701,7 @@ extension WebKitDownloadTask: WKDownloadDelegate { } catch { PixelKit.fire(DebugEvent(GeneralPixel.fileMoveToDownloadsFailed, error: error)) - os_log(.error, log: log, "fileMoveToDownloadsFailed: \(error)") + Logger.fileDownload.error("fileMoveToDownloadsFailed: \(error)") self.finish(with: .failure(.failedToCompleteDownloadTask(underlyingError: error, resumeData: nil, isRetryable: false))) } } @@ -714,7 +714,7 @@ extension WebKitDownloadTask: WKDownloadDelegate { // in case the File Presenters instantiation task is still running - we‘ll either receive the `state` change // or the task will be finished with a file error || itemReplacementDirectoryFSOCancellable == nil else { - os_log(log: log, "❌ download did fail too early, we‘ll wait for the `.downloading` state: \(self)") + Logger.fileDownload.debug("❌ download did fail too early, we‘ll wait for the `.downloading` state: \(self)") waitForDownloadDidStart { [weak self] in self?.downloadDidFail(with: error, resumeData: resumeData) } diff --git a/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift b/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift index 5364ca781a..f645d9e60f 100644 --- a/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift +++ b/DuckDuckGo/FileDownload/Services/DownloadListCoordinator.swift @@ -21,6 +21,7 @@ import Common import Foundation import Navigation import PixelKit +import os.log @MainActor private func getFirstAvailableWebView() -> WKWebView? { @@ -65,21 +66,14 @@ final class DownloadListCoordinator { let progress = Progress() - private let getLogger: (() -> OSLog) - private var log: OSLog { - getLogger() - } - init(store: DownloadListStoring = DownloadListStore(), downloadManager: FileDownloadManagerProtocol = FileDownloadManager.shared, clearItemsOlderThan clearDate: Date = .daysAgo(2), - webViewProvider: (() -> WKWebView?)? = nil, - log: @autoclosure @escaping (() -> OSLog) = .downloads) { + webViewProvider: (() -> WKWebView?)? = nil) { self.store = store self.downloadManager = downloadManager self.webViewProvider = webViewProvider - self.getLogger = log load(clearingItemsOlderThan: clearDate) subscribeToDownloadManager() @@ -93,12 +87,12 @@ final class DownloadListCoordinator { switch result { case .success(let items): - os_log(.error, log: log, "coordinator: loaded \(items.count) items") + Logger.fileDownload.error("coordinator: loaded \(items.count) items") for item in items { do { try add(item, ifModifiedLaterThan: clearDate) } catch { - os_log(.debug, log: self.log, "❗️ coordinator: drop item \(item.identifier): \(error)") + Logger.fileDownload.debug("❗️ coordinator: drop item \(item.identifier): \(error)") // remove item from db removing temp files if needed without sending a `.removed` update cleanupTempFiles(for: item) filePresenters[item.identifier] = nil @@ -109,7 +103,7 @@ final class DownloadListCoordinator { } case .failure(let error): - os_log(.error, log: log, "coordinator: loading failed: \(error)") + Logger.fileDownload.error("coordinator: loading failed: \(error)") } } } @@ -156,9 +150,9 @@ final class DownloadListCoordinator { // locate destination file let destinationPresenterResult = Result { if let destinationFileBookmarkData = item.destinationFileBookmarkData { - try BookmarkFilePresenter(fileBookmarkData: destinationFileBookmarkData, logger: log) + try BookmarkFilePresenter(fileBookmarkData: destinationFileBookmarkData) } else if let destinationURL = item.destinationURL { - try BookmarkFilePresenter(url: destinationURL, logger: log) + try BookmarkFilePresenter(url: destinationURL) } else { nil } @@ -167,9 +161,9 @@ final class DownloadListCoordinator { // locate temp download file var tempFilePresenterResult = Result { if let tempFileBookmarkData = item.tempFileBookmarkData { - try BookmarkFilePresenter(fileBookmarkData: tempFileBookmarkData, logger: log) + try BookmarkFilePresenter(fileBookmarkData: tempFileBookmarkData) } else if let tempURL = item.tempURL { - try BookmarkFilePresenter(url: tempURL, logger: log) + try BookmarkFilePresenter(url: tempURL) } else { nil } @@ -213,13 +207,13 @@ final class DownloadListCoordinator { guard downloadTaskCancellables[task] == nil else { return } let item = item ?? DownloadListItem(task: task) - os_log(.debug, log: log, "coordinator: subscribing to \(item.identifier)") + Logger.fileDownload.debug("coordinator: subscribing to \(item.identifier)") self.downloadTaskCancellables[task] = task.$state .sink { [weak self] state in DispatchQueue.main.async { guard let self else { return } - os_log(.debug, log: self.log, "coordinator: state updated \(item.identifier) ➡️ \(state)") + Logger.fileDownload.debug("coordinator: state updated \(item.identifier.uuidString) ➡️ \(state.debugDescription)") switch state { case .initial: // only add item when download starts, destination URL is set @@ -241,7 +235,7 @@ final class DownloadListCoordinator { @MainActor private func addItemIfNeededAndSubscribe(to presenters: (destination: FilePresenter, tempFile: FilePresenter?), for initialItem: DownloadListItem) { - os_log(.debug, log: log, "coordinator: add/update \(initialItem.identifier)") + Logger.fileDownload.debug("coordinator: add/update \(initialItem.identifier)") updateItem(withId: initialItem.identifier) { item in if item == nil { item = initialItem } } @@ -259,14 +253,14 @@ final class DownloadListCoordinator { .scan((oldURL: nil, newURL: nil, fileBookmarkData: nil)) { (oldURL: $0.newURL, newURL: $1.0, fileBookmarkData: $1.1) } .sink { [weak self] oldURL, newURL, fileBookmarkData in DispatchQueue.main.asyncOrNow { - self?.updateItem(withId: item.identifier) { [id=item.identifier, log=(self?.log ?? .disabled)] item in + self?.updateItem(withId: item.identifier) { [id=item.identifier] item in guard !Self.checkIfFileWasRemoved(oldURL: oldURL, newURL: newURL) else { - os_log(.debug, log: log, "coordinator: destination file removed \(id)") + Logger.fileDownload.debug("coordinator: destination file removed \(id)") item = nil return } - os_log(.debug, log: log, "⚠️coordinator: destination url updated \(id): \"\(newURL?.path ?? "")\"") + Logger.fileDownload.debug("⚠️coordinator: destination url updated \(id): \"\(newURL?.path ?? "")\"") item?.destinationURL = newURL item?.destinationFileBookmarkData = fileBookmarkData // keep the filename even when the destinationURL is nullified @@ -288,14 +282,14 @@ final class DownloadListCoordinator { .scan((oldURL: nil, newURL: nil, fileBookmarkData: nil)) { (oldURL: $0.newURL, newURL: $1.0, fileBookmarkData: $1.1) } .sink { [weak self] oldURL, newURL, fileBookmarkData in DispatchQueue.main.asyncOrNow { - self?.updateItem(withId: item.identifier) { [id=item.identifier, log=(self?.log ?? .disabled)] item in + self?.updateItem(withId: item.identifier) { [id=item.identifier] item in guard !Self.checkIfFileWasRemoved(oldURL: oldURL, newURL: newURL) else { - os_log(.debug, log: log, "coordinator: temp file removed \(id)") + Logger.fileDownload.debug("coordinator: temp file removed \(id)") item = nil return } - os_log(.debug, log: log, "coordinator: temp url updated \(id): \"\(newURL?.path ?? "")\"") + Logger.fileDownload.debug("coordinator: temp url updated \(id): \"\(newURL?.path ?? "")\"") item?.tempURL = newURL item?.tempFileBookmarkData = fileBookmarkData } @@ -335,7 +329,7 @@ final class DownloadListCoordinator { task.$state.receive(on: DispatchQueue.main) .sink { [weak self] state in guard let self, state.isCompleted else { return } - os_log(.debug, log: log, "coordinator: unsubscribe from progress: \(task)") + Logger.fileDownload.debug("coordinator: unsubscribe from progress: \(task)") progress.completedUnitCount -= lastKnownProgress.completed progress.totalUnitCount -= lastKnownProgress.total @@ -346,7 +340,7 @@ final class DownloadListCoordinator { @MainActor private func downloadTask(_ task: WebKitDownloadTask, withOriginalItem initialItem: DownloadListItem, completedWith result: Subscribers.Completion) -> DownloadListItem? { - os_log(.debug, log: log, "coordinator: task did finish \(initialItem.identifier) \(task) with .\(result)") + Logger.fileDownload.debug("coordinator: task did finish \(initialItem.identifier.uuidString) \(task.debugDescription) with .\(String(describing: result))") self.downloadTaskCancellables[task] = nil @@ -409,11 +403,11 @@ final class DownloadListCoordinator { let fm = FileManager.default do { try filePresenters[item.identifier]?.tempFile?.coordinateWrite(with: .forDeleting, using: { url in - os_log(.debug, log: self.log, "🦀 coordinator: removing \"\(url.path)\" (\(item.identifier))") + Logger.fileDownload.debug("🦀 coordinator: removing \"\(url.path)\" (\(item.identifier))") try fm.removeItem(at: url) }) } catch { - os_log(.error, log: self.log, "🦀 coordinator: failed to remove temp file: \(error)") + Logger.fileDownload.error("🦀 coordinator: failed to remove temp file: \(error)") } struct DestinationFileNotEmpty: Error {} @@ -421,13 +415,13 @@ final class DownloadListCoordinator { guard let presenter = filePresenters[item.identifier]?.destination, (try? presenter.url?.resourceValues(forKeys: [.fileSizeKey]).fileSize) == 0 else { throw DestinationFileNotEmpty() } try presenter.coordinateWrite(with: .forDeleting, using: { url in - os_log(.debug, log: self.log, "🦀 coordinator: removing \"\(url.path)\" (\(item.identifier))") + Logger.fileDownload.debug("🦀 coordinator: removing \"\(url.path)\" (\(item.identifier))") try fm.removeItem(at: url) }) } catch is DestinationFileNotEmpty { // don‘t delete non-empty destination file } catch { - os_log(.error, log: self.log, "🦀 coordinator: failed to remove destination file: \(error)") + Logger.fileDownload.error("🦀 coordinator: failed to remove destination file: \(error)") } } @@ -436,7 +430,7 @@ final class DownloadListCoordinator { // Important: WebKitDownloadTask (as well as WKWebView) should be deallocated on the Main Thread dispatchPrecondition(condition: .onQueue(.main)) withExtendedLifetime(webView) { - os_log(.debug, log: self.log, "coordinator: restarting \(item.identifier): \(download)") + Logger.fileDownload.debug("coordinator: restarting \(item.identifier.uuidString): \(String(describing: download))") let destination: WebKitDownloadTask.DownloadDestination = if let presenters { .resume(destination: presenters.destination, tempFile: presenters.tempFile) } else { @@ -471,7 +465,7 @@ final class DownloadListCoordinator { @MainActor func restart(downloadWithIdentifier identifier: UUID) { - os_log(.debug, log: self.log, "coordinator: restart \(identifier)") + Logger.fileDownload.debug("coordinator: restart \(identifier)") guard let item = items[identifier], let webView = (webViewProvider ?? getFirstAvailableWebView)() else { return } do { guard var resumeData = item.error?.resumeData, @@ -510,7 +504,7 @@ final class DownloadListCoordinator { @MainActor func cleanupInactiveDownloads() { - os_log(.debug, log: self.log, "coordinator: cleanupInactiveDownloads") + Logger.fileDownload.debug("coordinator: cleanupInactiveDownloads") for (id, item) in self.items where item.progress == nil { remove(downloadWithIdentifier: id) @@ -519,7 +513,7 @@ final class DownloadListCoordinator { @MainActor func cleanupInactiveDownloads(for baseDomains: Set, tld: TLD) { - os_log(.debug, log: self.log, "coordinator: cleanupInactiveDownloads for \(baseDomains)") + Logger.fileDownload.debug("coordinator: cleanupInactiveDownloads for \(baseDomains)") for (id, item) in self.items where item.progress == nil { let websiteUrlBaseDomain = tld.eTLDplus1(item.websiteURL?.host) ?? "" @@ -533,7 +527,7 @@ final class DownloadListCoordinator { @MainActor func remove(downloadWithIdentifier identifier: UUID) { - os_log(.debug, log: self.log, "coordinator: remove \(identifier)") + Logger.fileDownload.debug("coordinator: remove \(identifier)") updateItem(withId: identifier) { item in item = nil @@ -542,7 +536,7 @@ final class DownloadListCoordinator { @MainActor func cancel(downloadWithIdentifier identifier: UUID) { - os_log(.debug, log: self.log, "coordinator: cancel \(identifier)") + Logger.fileDownload.debug("coordinator: cancel \(identifier)") guard let item = self.items[identifier] else { assertionFailure("Item with identifier \(identifier) not found") diff --git a/DuckDuckGo/Fire/Logger+Fire.swift b/DuckDuckGo/Fire/Logger+Fire.swift new file mode 100644 index 0000000000..04f675c35b --- /dev/null +++ b/DuckDuckGo/Fire/Logger+Fire.swift @@ -0,0 +1,24 @@ +// +// Logger+Fire.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log + +public extension Logger { + static var fire = { Logger(subsystem: "Fire", category: "") }() +} diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index a89385145b..dfcef3ee80 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -24,6 +24,7 @@ import PrivacyDashboard import WebKit import SecureStorage import History +import os.log final class Fire { @@ -129,7 +130,7 @@ final class Fire { func burnEntity(entity: BurningEntity, includingHistory: Bool = true, completion: (() -> Void)? = nil) { - os_log("Fire started", log: .fire) + Logger.fire.debug("Fire started") let group = DispatchGroup() dispatchGroup = group @@ -180,14 +181,14 @@ final class Fire { completion?() - os_log("Fire finished", log: .fire) + Logger.fire.debug("Fire finished") } } } @MainActor func burnAll(completion: (() -> Void)? = nil) { - os_log("Fire started", log: .fire) + Logger.fire.debug("Fire started") let group = DispatchGroup() dispatchGroup = group @@ -231,7 +232,7 @@ final class Fire { self.burningData = nil completion?() - os_log("Fire finished", log: .fire) + Logger.fire.debug("Fire finished") } } } @@ -331,15 +332,15 @@ final class Fire { // MARK: - Web cache private func burnWebCache() async { - os_log("WebsiteDataStore began cookie deletion", log: .fire) + Logger.fire.debug("WebsiteDataStore began cookie deletion") await webCacheManager.clear() - os_log("WebsiteDataStore completed cookie deletion", log: .fire) + Logger.fire.debug("WebsiteDataStore completed cookie deletion") } private func burnWebCache(baseDomains: Set? = nil) async { - os_log("WebsiteDataStore began cookie deletion", log: .fire) + Logger.fire.debug("WebsiteDataStore began cookie deletion") await webCacheManager.clear(baseDomains: baseDomains) - os_log("WebsiteDataStore completed cookie deletion", log: .fire) + Logger.fire.debug("WebsiteDataStore completed cookie deletion") } // MARK: - History diff --git a/DuckDuckGo/Fire/View/FirePopoverViewController.swift b/DuckDuckGo/Fire/View/FirePopoverViewController.swift index 53b1dc91b2..54cd06ebe4 100644 --- a/DuckDuckGo/Fire/View/FirePopoverViewController.swift +++ b/DuckDuckGo/Fire/View/FirePopoverViewController.swift @@ -20,6 +20,7 @@ import Cocoa import Combine import Common import History +import os.log protocol FirePopoverViewControllerDelegate: AnyObject { @@ -328,7 +329,7 @@ final class FirePopoverViewController: NSViewController { private func setupOptionsButton() { guard let menu = optionsButton.menu, let font = optionsButton.font else { - os_log("FirePopoverViewController: Menu and/or font not present for optionsMenu", type: .error) + Logger.fire.error("FirePopoverViewController: Menu and/or font not present for optionsMenu") return } menu.removeAllItems() diff --git a/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift b/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift index 6552a67c4c..d9814ce301 100644 --- a/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift +++ b/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift @@ -19,6 +19,7 @@ import Common import Foundation import CoreData +import os.log internal class FireproofDomains { @@ -78,7 +79,7 @@ internal class FireproofDomains { return try store.load() } catch { - os_log("FireproofDomainsStore: Failed to load Fireproof Domains", type: .error) + Logger.fire.error("FireproofDomainsStore: Failed to load Fireproof Domains") return FireproofDomainsContainer() } } diff --git a/DuckDuckGo/LoginItems/LoginItemsManager.swift b/DuckDuckGo/LoginItems/LoginItemsManager.swift index 1260177691..146191c316 100644 --- a/DuckDuckGo/LoginItems/LoginItemsManager.swift +++ b/DuckDuckGo/LoginItems/LoginItemsManager.swift @@ -20,12 +20,13 @@ import Common import Foundation import LoginItems import PixelKit +import os.log protocol LoginItemsManaging { - func enableLoginItems(_ items: Set, log: OSLog) - func throwingEnableLoginItems(_ items: Set, log: OSLog) throws + func enableLoginItems(_ items: Set) + func throwingEnableLoginItems(_ items: Set) throws func disableLoginItems(_ items: Set) - func restartLoginItems(_ items: Set, log: OSLog) + func restartLoginItems(_ items: Set) func isAnyEnabled(_ items: Set) -> Bool } @@ -41,11 +42,11 @@ final class LoginItemsManager: LoginItemsManaging { // MARK: - Main Interactions - func enableLoginItems(_ items: Set, log: OSLog) { + func enableLoginItems(_ items: Set) { for item in items { do { try item.enable() - os_log("🟢 Enabled successfully %{public}@", log: log, String(describing: item)) + Logger.networkProtection.debug("🟢 Enabled successfully \(String(describing: item), privacy: .public)") } catch let error as NSError { handleError(for: item, action: .enable, error: error) } @@ -54,11 +55,11 @@ final class LoginItemsManager: LoginItemsManaging { /// Throwing version of enableLoginItems /// - func throwingEnableLoginItems(_ items: Set, log: OSLog) throws { + func throwingEnableLoginItems(_ items: Set) throws { for item in items { do { try item.enable() - os_log("🟢 Enabled successfully %{public}@", log: log, String(describing: item)) + Logger.networkProtection.debug("🟢 Enabled successfully \(String(describing: item), privacy: .public)") } catch let error as NSError { handleError(for: item, action: .enable, error: error) throw error @@ -66,11 +67,11 @@ final class LoginItemsManager: LoginItemsManaging { } } - func restartLoginItems(_ items: Set, log: OSLog) { + func restartLoginItems(_ items: Set) { for item in items { do { try item.restart() - os_log("🟢 Restarted successfully %{public}@", log: log, String(describing: item)) + Logger.networkProtection.debug("🟢 Restarted successfully \(String(describing: item), privacy: .public)") } catch let error as NSError { handleError(for: item, action: .restart, error: error) } @@ -95,7 +96,7 @@ final class LoginItemsManager: LoginItemsManaging { buildType: AppVersion.shared.buildType, osVersion: AppVersion.shared.osVersion) PixelKit.fire(DebugEvent(event, error: error), frequency: .dailyAndCount) - os_log("🔴 Could not enable %{public}@: %{public}@", item.debugDescription, error.debugDescription) + Logger.networkProtection.error("Could not enable \(item.debugDescription, privacy: .public): \(error.debugDescription, privacy: .public)") } // MARK: - Debug Interactions diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 56d49002c3..47c0364a3c 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -22,6 +22,7 @@ import Combine import Common import NetworkProtection import NetworkProtectionIPC +import os.log final class MainViewController: NSViewController { private lazy var mainView = MainView(frame: NSRect(x: 0, y: 0, width: 600, height: 660)) @@ -385,7 +386,7 @@ final class MainViewController: NSViewController { private func updateFindInPage() { guard let model = tabCollectionViewModel.selectedTabViewModel?.findInPage else { findInPageViewController.makeMeFirstResponder() - os_log("MainViewController: Failed to get find in page model", type: .error) + Logger.general.error("MainViewController: Failed to get find in page model") return } @@ -399,7 +400,7 @@ final class MainViewController: NSViewController { private func updateBackMenuItem() { guard self.view.window?.isMainWindow == true else { return } guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("MainViewController: No tab view model selected", type: .error) + Logger.general.error("MainViewController: No tab view model selected") return } NSApp.mainMenuTyped.backMenuItem.isEnabled = selectedTabViewModel.canGoBack @@ -408,7 +409,7 @@ final class MainViewController: NSViewController { private func updateForwardMenuItem() { guard self.view.window?.isMainWindow == true else { return } guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("MainViewController: No tab view model selected", type: .error) + Logger.general.error("MainViewController: No tab view model selected") return } NSApp.mainMenuTyped.forwardMenuItem.isEnabled = selectedTabViewModel.canGoForward @@ -417,7 +418,7 @@ final class MainViewController: NSViewController { private func updateReloadMenuItem() { guard self.view.window?.isMainWindow == true else { return } guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("MainViewController: No tab view model selected", type: .error) + Logger.general.error("MainViewController: No tab view model selected") return } NSApp.mainMenuTyped.reloadMenuItem.isEnabled = selectedTabViewModel.canReload @@ -426,7 +427,7 @@ final class MainViewController: NSViewController { private func updateStopMenuItem() { guard self.view.window?.isMainWindow == true else { return } guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("MainViewController: No tab view model selected", type: .error) + Logger.general.error("MainViewController: No tab view model selected") return } NSApp.mainMenuTyped.stopMenuItem.isEnabled = selectedTabViewModel.isLoading diff --git a/DuckDuckGo/Menus/HistoryMenu.swift b/DuckDuckGo/Menus/HistoryMenu.swift index 54b9c3633a..3f2a6ad8d1 100644 --- a/DuckDuckGo/Menus/HistoryMenu.swift +++ b/DuckDuckGo/Menus/HistoryMenu.swift @@ -20,6 +20,7 @@ import Cocoa import Combine import Common import History +import os.log @MainActor final class HistoryMenu: NSMenu { @@ -334,7 +335,7 @@ private extension HistoryCoordinating { func getSortedArrayOfVisits() -> [Visit] { guard let history = history else { - os_log("HistoryCoordinator: No history available", type: .error) + Logger.general.error("HistoryCoordinator: No history available") return [] } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index fefb5754e8..478ddd8dc9 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -715,8 +715,7 @@ final class MainMenu: NSMenu { let logStore = try OSLogStore(scope: .currentProcessIdentifier) try logStore.getEntries() .compactMap { - guard let entry = $0 as? OSLogEntryLog, - entry.subsystem == OSLog.subsystem else { return nil } + guard let entry = $0 as? OSLogEntryLog else { return nil } return "\(formatter.string(from: entry.date)) [\(entry.category)] \(entry.composedMessage)" } .joined(separator: "\n") diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 8204d364dd..314d57a9d5 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -25,6 +25,7 @@ import History import PixelKit import Subscription import WebKit +import os.log // Actions are sent to objects of responder chain @@ -199,7 +200,7 @@ extension AppDelegate { @objc func navigateToBookmark(_ sender: Any?) { guard let menuItem = sender as? NSMenuItem else { - os_log("AppDelegate: Casting to menu item failed", type: .error) + Logger.general.error("AppDelegate: Casting to menu item failed") return } @@ -385,7 +386,7 @@ extension MainViewController { @objc func openLocation(_ sender: Any?) { makeKeyIfNeeded() guard let addressBarTextField = navigationBarViewController.addressBarViewController?.addressBarTextField else { - os_log("MainViewController: Cannot reference address bar text field", type: .error) + Logger.general.error("MainViewController: Cannot reference address bar text field") return } @@ -600,7 +601,7 @@ extension MainViewController { @objc func openBookmark(_ sender: Any?) { guard let menuItem = sender as? NSMenuItem else { - os_log("MainViewController: Casting to menu item failed", type: .error) + Logger.general.error("MainViewController: Casting to menu item failed") return } @@ -612,7 +613,7 @@ extension MainViewController { @objc func openAllInTabs(_ sender: Any?) { guard let menuItem = sender as? NSMenuItem else { - os_log("MainViewController: Casting to menu item failed", type: .error) + Logger.general.error("MainViewController: Casting to menu item failed") return } @@ -658,11 +659,11 @@ extension MainViewController { @objc func showTab(_ sender: Any?) { makeKeyIfNeeded() guard let sender = sender as? NSMenuItem else { - os_log("MainViewController: Casting to NSMenuItem failed", type: .error) + Logger.general.error("MainViewController: Casting to NSMenuItem failed") return } guard let keyEquivalent = Int(sender.keyEquivalent), keyEquivalent >= 0 && keyEquivalent <= 9 else { - os_log("MainViewController: Key equivalent is not correct for tab selection", type: .error) + Logger.general.error("MainViewController: Key equivalent is not correct for tab selection") return } let index = keyEquivalent - 1 @@ -912,18 +913,14 @@ extension MainViewController { @objc func removeUserScripts(_ sender: Any?) { tabCollectionViewModel.selectedTab?.userContentController?.cleanUpBeforeClosing() tabCollectionViewModel.selectedTab?.reload() - os_log("User scripts removed from the current tab", type: .info) + Logger.general.info("User scripts removed from the current tab") } @objc func reloadConfigurationNow(_ sender: Any?) { - OSLog.loggingCategories.insert(OSLog.AppCategories.config.rawValue) - ConfigurationManager.shared.forceRefresh(isDebug: true) } private func setConfigurationUrl(_ configurationUrl: URL?) { - OSLog.loggingCategories.insert(OSLog.AppCategories.config.rawValue) - var configurationProvider = AppConfigurationURLProvider(customPrivacyConfiguration: configurationUrl) if configurationUrl == nil { configurationProvider.resetToDefaultConfigurationUrl() @@ -931,9 +928,9 @@ extension MainViewController { Configuration.setURLProvider(configurationProvider) ConfigurationManager.shared.forceRefresh(isDebug: true) if let configurationUrl { - os_log("New configuration URL set to \(configurationUrl.absoluteString)", type: .info) + Logger.general.debug("New configuration URL set to \(configurationUrl.absoluteString)") } else { - os_log("New configuration URL reset to default", type: .info) + Logger.general.log("New configuration URL reset to default") } } @@ -943,7 +940,7 @@ extension MainViewController { if alert.runModal() != .cancel { guard let textField = alert.accessoryView as? NSTextField, let newConfigurationUrl = URL(string: textField.stringValue) else { - os_log("Failed to set custom configuration URL", type: .error) + Logger.general.error("Failed to set custom configuration URL") return } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 318940ba35..9c3f11dfdf 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -22,6 +22,7 @@ import Cocoa import Combine import Common import Lottie +import os.log protocol AddressBarButtonsViewControllerDelegate: AnyObject { @@ -400,7 +401,7 @@ final class AddressBarButtonsViewController: NSViewController { guard let tabViewModel, let state = tabViewModel.usedPermissions.microphone else { - os_log("%s: Selected tab view model is nil or no microphone state", type: .error, className) + Logger.general.error("Selected tab view model is nil or no microphone state") return } if case .requested(let query) = state { @@ -419,7 +420,7 @@ final class AddressBarButtonsViewController: NSViewController { guard let tabViewModel, let state = tabViewModel.usedPermissions.geolocation else { - os_log("%s: Selected tab view model is nil or no geolocation state", type: .error, className) + Logger.general.error("Selected tab view model is nil or no geolocation state") return } if case .requested(let query) = state { @@ -438,7 +439,7 @@ final class AddressBarButtonsViewController: NSViewController { guard let tabViewModel, let state = tabViewModel.usedPermissions.popups else { - os_log("%s: Selected tab view model is nil or no popups state", type: .error, className) + Logger.general.error("Selected tab view model is nil or no popups state") return } @@ -463,7 +464,7 @@ final class AddressBarButtonsViewController: NSViewController { guard let tabViewModel, let (permissionType, state) = tabViewModel.usedPermissions.first(where: { $0.key.isExternalScheme }) else { - os_log("%s: Selected tab view model is nil or no externalScheme state", type: .error, className) + Logger.general.error("Selected tab view model is nil or no externalScheme state") return } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 79d5bb73cc..5d9611bfbe 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -24,6 +24,7 @@ import Common import PixelKit import Suggestions import Subscription +import os.log final class AddressBarTextField: NSTextField { @@ -348,7 +349,7 @@ final class AddressBarTextField: NSTextField { private func updateTabUrlWithUrl(_ providedUrl: URL, userEnteredValue: String, downloadRequested: Bool, suggestion: Suggestion?) { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("AddressBarTextField: Selected tab view model is nil", type: .error) + Logger.general.error("AddressBarTextField: Selected tab view model is nil") return } @@ -410,7 +411,7 @@ final class AddressBarTextField: NSTextField { makeUrl(suggestion: suggestion, stringValueWithoutSuffix: stringValueWithoutSuffix) { [weak self] url, userEnteredValue, isUpgraded in guard let self, let url else { - os_log("AddressBarTextField: Making url from address bar string failed", type: .error) + Logger.general.error("AddressBarTextField: Making url from address bar string failed") return } let tab = Tab(content: .url(url, source: .userEntered(userEnteredValue)), @@ -500,7 +501,7 @@ final class AddressBarTextField: NSTextField { private func displaySelectedSuggestionViewModel() { guard let suggestionWindow = suggestionWindowController?.window else { - os_log("AddressBarTextField: Window not available", type: .error) + Logger.general.error("AddressBarTextField: Window not available") return } guard suggestionWindow.isVisible else { return } @@ -575,7 +576,7 @@ final class AddressBarTextField: NSTextField { private func showSuggestionWindow() { guard let window = window, let suggestionWindow = suggestionWindowController?.window else { - os_log("AddressBarTextField: Window not available", type: .error) + Logger.general.error("AddressBarTextField: Window not available") return } @@ -607,7 +608,7 @@ final class AddressBarTextField: NSTextField { return } guard let superview = superview else { - os_log("AddressBarTextField: Superview not available", type: .error) + Logger.general.error("AddressBarTextField: Superview not available") return } diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index 2daca8b0b9..ffd505d092 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -23,6 +23,7 @@ import BrowserServicesKit import PixelKit import NetworkProtection import Subscription +import os.log protocol OptionsButtonMenuDelegate: AnyObject { @@ -169,7 +170,7 @@ final class MoreOptionsMenu: NSMenu { @objc func toggleFireproofing(_ sender: NSMenuItem) { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("MainViewController: No tab view model selected", type: .error) + Logger.general.error("MainViewController: No tab view model selected") return } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 6cf1768160..d7eb9f021a 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -214,7 +214,7 @@ final class NavigationBarViewController: NSViewController { @IBAction func goBackAction(_ sender: NSButton) { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("%s: Selected tab view model is nil", type: .error, className) + Logger.navigation.error("Selected tab view model is nil") return } @@ -230,7 +230,7 @@ final class NavigationBarViewController: NSViewController { @IBAction func goForwardAction(_ sender: NSButton) { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("%s: Selected tab view model is nil", type: .error, className) + Logger.navigation.error("Selected tab view model is nil") return } @@ -251,7 +251,7 @@ final class NavigationBarViewController: NSViewController { @IBAction func refreshOrStopAction(_ sender: NSButton) { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("%s: Selected tab view model is nil", type: .error, className) + Logger.navigation.error("Selected tab view model is nil") return } @@ -264,7 +264,7 @@ final class NavigationBarViewController: NSViewController { @IBAction func homeButtonAction(_ sender: NSButton) { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { - os_log("%s: Selected tab view model is nil", type: .error, className) + Logger.navigation.error("Selected tab view model is nil") return } selectedTabViewModel.tab.openHomePage() diff --git a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift index c6b005c2fa..3a04bf68fe 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift @@ -20,6 +20,7 @@ import Cocoa import Common import WebKit import History +import os.log @MainActor final class NavigationButtonMenuDelegate: NSObject { @@ -52,7 +53,7 @@ extension NavigationButtonMenuDelegate: NSMenuDelegate { func menu(_ menu: NSMenu, update item: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool { guard let listItem = listItems[safe: index] else { - os_log("%s: Index out of bounds", type: .error, className) + Logger.general.error("Index out of bounds") return true } @@ -75,7 +76,7 @@ extension NavigationButtonMenuDelegate: NSMenuDelegate { @objc func menuItemAction(_ sender: NSMenuItem) { let index = sender.tag guard let listItem = listItems[safe: index] else { - os_log("%s: Index out of bounds", type: .error, className) + Logger.general.error("Index out of bounds") return } tabCollectionViewModel.selectedTabViewModel?.tab.go(to: listItem) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift index 30d99488bf..1704de0057 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/LoginItem+NetworkProtection.swift @@ -18,11 +18,17 @@ import Foundation import LoginItems +import NetworkProtection +import os.log extension LoginItem { - static let vpnMenu = LoginItem(bundleId: Bundle.main.vpnMenuAgentBundleId, defaults: .netP, log: .networkProtection) + static let vpnMenu = LoginItem(bundleId: Bundle.main.vpnMenuAgentBundleId, + defaults: .netP, + logger: Logger.networkProtection) #if NETP_SYSTEM_EXTENSION - static let notificationsAgent = LoginItem(bundleId: Bundle.main.notificationsAgentBundleId, defaults: .netP, log: .networkProtection) + static let notificationsAgent = LoginItem(bundleId: Bundle.main.notificationsAgentBundleId, + defaults: .netP, + logger: Logger.networkProtection) #endif } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift index f63eb7551c..e39818559e 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionAppEvents.swift @@ -88,7 +88,7 @@ final class NetworkProtectionAppEvents { return } - loginItemsManager.restartLoginItems(LoginItemsManager.networkProtectionLoginItems, log: .networkProtection) + loginItemsManager.restartLoginItems(LoginItemsManager.networkProtectionLoginItems) } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 9afbacf519..46710a6d96 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -22,6 +22,7 @@ import Foundation import NetworkProtection import NetworkProtectionProxy import SwiftUI +import os.log /// Controller for the VPN debug menu. /// @@ -176,7 +177,7 @@ final class NetworkProtectionDebugMenu: NSMenu { do { try await debugUtilities.resetAllState(keepAuthToken: false) } catch { - os_log("Error in resetAllState: %{public}@", log: .networkProtection, error.localizedDescription) + Logger.networkProtection.error("Error in resetAllState: \(error.localizedDescription, privacy: .public)") } } } @@ -189,7 +190,7 @@ final class NetworkProtectionDebugMenu: NSMenu { do { try await debugUtilities.resetAllState(keepAuthToken: true) } catch { - os_log("Error in resetAllState: %{public}@", log: .networkProtection, error.localizedDescription) + Logger.networkProtection.error("Error in resetAllState: \(error.localizedDescription, privacy: .public)") } } } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift index 78d6cda52f..65e8d18b37 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift @@ -26,6 +26,7 @@ import NetworkProtectionProxy import NetworkProtectionUI import Networking import PixelKit +import os.log #if NETP_SYSTEM_EXTENSION import SystemExtensionManager @@ -59,10 +60,6 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr /// static var simulationOptions = NetworkProtectionSimulationOptions() - /// The logger that this object will use for errors that are handled by this class. - /// - private let logger: NetworkProtectionLogger - /// Stores the last controller error for the purpose of updating the UI as needed. /// private let controllerErrorStore = NetworkProtectionControllerErrorStore() @@ -166,10 +163,8 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr defaults: UserDefaults, tokenStore: NetworkProtectionTokenStore = NetworkProtectionKeychainTokenStore(), notificationCenter: NotificationCenter = .default, - logger: NetworkProtectionLogger = DefaultNetworkProtectionLogger(), accessTokenStorage: SubscriptionTokenKeychainStorage) { - self.logger = logger self.networkExtensionBundleID = networkExtensionBundleID self.networkExtensionController = networkExtensionController self.notificationCenter = notificationCenter @@ -839,10 +834,10 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr private func fetchAuthToken() throws -> NSString? { if let accessToken = try? accessTokenStorage.getAccessToken() { - os_log(.error, log: .networkProtection, "🟢 TunnelController found token") + Logger.networkProtection.debug("🟢 TunnelController found token") return Self.adaptAccessTokenForVPN(accessToken) as NSString? } - os_log(.error, log: .networkProtection, "🔴 TunnelController found no token :(") + Logger.networkProtection.error("TunnelController found no token") return try tokenStore.fetchToken() as NSString? } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerUDSClient+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerUDSClient+ConvenienceInitializers.swift index b4ea2f7d65..930c18315a 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerUDSClient+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNControllerUDSClient+ConvenienceInitializers.swift @@ -28,6 +28,6 @@ extension VPNControllerUDSClient { extension UDSClient { static let sharedVPNUDSClient: UDSClient = { - return UDSClient(socketFileURL: VPNIPCResources.socketFileURL, log: .networkProtectionIPCLog) + return UDSClient(socketFileURL: VPNIPCResources.socketFileURL) }() } diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift index 3949a24805..5c376db928 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNRedditSessionWorkaround.swift @@ -22,6 +22,7 @@ import NetworkProtectionIPC import Subscription import WebKit import Common +import os.log final class VPNRedditSessionWorkaround { @@ -70,9 +71,9 @@ final class VPNRedditSessionWorkaround { } if requiresRedditSessionCookie { - os_log(.error, log: .networkProtection, "Installing VPN cookie workaround...") + Logger.networkProtection.error("Installing VPN cookie workaround...") await cookieStore.setCookie(redditSessionCookie) - os_log(.error, log: .networkProtection, "Installed VPN cookie workaround") + Logger.networkProtection.error("Installed VPN cookie workaround") } } @@ -85,9 +86,9 @@ final class VPNRedditSessionWorkaround { for cookie in cookies { if cookie.domain == redditSessionCookie.domain, cookie.name == redditSessionCookie.name { if cookie.value == redditSessionCookie.value { - os_log(.error, log: .networkProtection, "Removing VPN cookie workaround") + Logger.networkProtection.error("Removing VPN cookie workaround") await cookieStore.deleteCookie(cookie) - os_log(.error, log: .networkProtection, "Removed VPN cookie workaround") + Logger.networkProtection.error("Removed VPN cookie workaround") } break diff --git a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift index 960974ff29..4bc5ec17c5 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/DeveloperIDTarget/NetworkProtectionIPCTunnelController.swift @@ -22,6 +22,7 @@ import NetworkProtection import NetworkProtectionIPC import PixelKit import UDSHelper +import os.log /// VPN tunnel controller through IPC. /// @@ -75,7 +76,7 @@ final class NetworkProtectionIPCTunnelController { private func enableLoginItems() async throws { do { - try loginItemsManager.throwingEnableLoginItems(LoginItemsManager.networkProtectionLoginItems, log: .networkProtection) + try loginItemsManager.throwingEnableLoginItems(LoginItemsManager.networkProtectionLoginItems) } catch { throw RequestError.internalLoginItemError(error) } @@ -160,11 +161,11 @@ extension NetworkProtectionIPCTunnelController: TunnelController { private func log(_ error: Error) { switch error { case RequestError.notAuthorizedToEnableLoginItem: - os_log("🔴 IPC Controller not authorized to enable the login item", log: .networkProtection) + Logger.networkProtection.error("IPC Controller not authorized to enable the login item: \(error.localizedDescription)") case RequestError.internalLoginItemError(let error): - os_log("🔴 IPC Controller found an error while enabling the login item: \(error)", log: .networkProtection) + Logger.networkProtection.error("IPC Controller found an error while enabling the login item: \(error.localizedDescription)") default: - os_log("🔴 IPC Controller found an unknown error: \(error)", log: .networkProtection) + Logger.networkProtection.error("IPC Controller found an unknown error: \(error.localizedDescription)") } } } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index 0843f59a93..baceae66e6 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -24,6 +24,7 @@ import NetworkExtension import Networking import PixelKit import Subscription +import os.log import WireGuard final class MacPacketTunnelProvider: PacketTunnelProvider { @@ -567,7 +568,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { try super.loadVendorOptions(from: provider) guard let vendorOptions = provider?.providerConfiguration else { - os_log("🔵 Provider is nil, or providerConfiguration is not set", log: .networkProtection) + Logger.networkProtection.debug("🔵 Provider is nil, or providerConfiguration is not set") throw ConfigurationError.missingProviderConfiguration } @@ -576,7 +577,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { private func loadDefaultPixelHeaders(from options: [String: Any]) throws { guard let defaultPixelHeaders = options[NetworkProtectionOptionKey.defaultPixelHeaders] as? [String: String] else { - os_log("🔵 Pixel options are not set", log: .networkProtection) + Logger.networkProtection.debug("🔵 Pixel options are not set") throw ConfigurationError.missingPixelHeaders } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift index 0724849265..e2304c6539 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/NetworkProtectionTokenStore+SubscriptionTokenKeychainStorage.swift @@ -20,6 +20,7 @@ import Foundation import Subscription import NetworkProtection import Common +import os.log extension NetworkProtectionKeychainTokenStore: SubscriptionTokenStoring { @@ -32,7 +33,7 @@ extension NetworkProtectionKeychainTokenStore: SubscriptionTokenStoring { if token.hasPrefix("ddg:") { token = token.replacingOccurrences(of: "ddg:", with: "") } - os_log("🟢 Wrapper successfully fetched token %{token}@", log: .networkProtection, token) + Logger.networkProtection.debug("🟢 Wrapper successfully fetched token \(token)") return token } diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/VPNLogger.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/VPNLogger.swift deleted file mode 100644 index 52f8000ea3..0000000000 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/VPNLogger.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// VPNLogger.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import NetworkProtection -import os.log - -/// Logger for the VPN -/// -/// Since we'll want to ensure this adheres to our privacy standards, grouping the logging logic to be mostly -/// handled by a single class sounds like a good approach to be able to review what's being logged.. -/// -public final class VPNLogger { - public typealias AttemptStep = PacketTunnelProvider.AttemptStep - public typealias ConnectionAttempt = PacketTunnelProvider.ConnectionAttempt - public typealias ConnectionTesterStatus = PacketTunnelProvider.ConnectionTesterStatus - public typealias LogCallback = (OSLogType, OSLogMessage) -> Void - - public init() {} - - public func logStartingWithoutAuthToken() { - os_log("🔴 Starting tunnel without an auth token", log: .networkProtection, type: .error) - } - - public func log(_ step: AttemptStep, named name: String) { - let log = OSLog.networkProtection - - switch step { - case .begin: - os_log("🔵 %{public}@ attempt begins", log: log, name) - case .failure(let error): - os_log("🔴 %{public}@ attempt failed with error: %{public}@", log: log, type: .error, name, error.localizedDescription) - case .success: - os_log("🟢 %{public}@ attempt succeeded", log: log, name) - } - } - - public func log(_ step: ConnectionAttempt) { - let log = OSLog.networkProtection - - switch step { - case .connecting: - os_log("🔵 Connection attempt detected", log: log) - case .failure: - os_log("🔴 Connection attempt failed", log: log, type: .error) - case .success: - os_log("🟢 Connection attempt successful", log: log) - } - } - - public func log(_ status: ConnectionTesterStatus, server: String) { - let log = OSLog.networkProtectionConnectionTesterLog - - switch status { - case .failed(let duration): - os_log("🔴 Connection tester (%{public}@ - %{public}@) failure", log: log, type: .error, duration.rawValue, server) - case .recovered(let duration, let failureCount): - os_log("🟢 Connection tester (%{public}@ - %{public}@) recovery (after %{public}@ failures)", log: log, duration.rawValue, server, String(failureCount)) - } - } - - public func log(_ step: FailureRecoveryStep) { - let log = OSLog.networkProtectionTunnelFailureMonitorLog - - switch step { - case .started: - os_log("🔵 Failure Recovery attempt started", log: log) - case .failed(let error): - os_log("🔴 Failure Recovery attempt failed with error: %{public}@", log: log, type: .error, error.localizedDescription) - case .completed(let health): - switch health { - case .healthy: - os_log("🟢 Failure Recovery attempt completed", log: log) - case .unhealthy: - os_log("🔴 Failure Recovery attempt ended as unhealthy", log: log, type: .error) - } - } - } - - public func log(_ step: NetworkProtectionTunnelFailureMonitor.Result) { - let log = OSLog.networkProtectionTunnelFailureMonitorLog - - switch step { - case .failureDetected: - os_log("🔴 Tunnel failure detected", log: log, type: .error) - case .failureRecovered: - os_log("🟢 Tunnel failure recovered", log: log) - case .networkPathChanged: - os_log("🔵 Tunnel recovery detected path change", log: log) - } - } - - public func log(_ result: NetworkProtectionLatencyMonitor.Result) { - let log = OSLog.networkProtectionLatencyMonitorLog - - switch result { - case .error: - os_log("🔴 There was an error logging the latency", log: log, type: .error) - case .quality(let quality): - os_log("Connection quality is: %{public}@", log: log, quality.rawValue) - } - } -} diff --git a/DuckDuckGo/Onboarding/OnboardingActionsManager.swift b/DuckDuckGo/Onboarding/OnboardingActionsManager.swift index 30efbe0e66..22321ea4cc 100644 --- a/DuckDuckGo/Onboarding/OnboardingActionsManager.swift +++ b/DuckDuckGo/Onboarding/OnboardingActionsManager.swift @@ -20,6 +20,7 @@ import Foundation import Combine import PixelKit import Common +import os.log enum OnboardingSteps: String, CaseIterable { case summary @@ -198,7 +199,7 @@ final class OnboardingActionsManager: OnboardingActionsManaging { let message = param["message"] ?? "" let id = param["id"] ?? "" PixelKit.fire(GeneralPixel.onboardingExceptionReported(message: message, id: id), frequency: .standard) - os_log("Onboarding error: %{public}@", log: .error, "\(id): \(message)") + Logger.general.error("Onboarding error: \("\(id): \(message)", privacy: .public)") } private func onboardingHasFinished() { diff --git a/DuckDuckGo/PasswordManager/Bitwarden/Logger+BitWarden.swift b/DuckDuckGo/PasswordManager/Bitwarden/Logger+BitWarden.swift new file mode 100644 index 0000000000..d9d389dd7a --- /dev/null +++ b/DuckDuckGo/PasswordManager/Bitwarden/Logger+BitWarden.swift @@ -0,0 +1,24 @@ +// +// Logger+BitWarden.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log + +public extension Logger { + static var bitWarden = { Logger(subsystem: "Bitwarden", category: "") }() +} diff --git a/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift b/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift index 4cf474c0b9..12900d8d51 100644 --- a/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift +++ b/DuckDuckGo/PasswordManager/Bitwarden/Model/BWManager.swift @@ -21,6 +21,7 @@ import Foundation import SwiftUI import OpenSSL import PixelKit +import os.log final class BWManager: BWManagement, ObservableObject { @@ -125,7 +126,7 @@ final class BWManager: BWManagement, ObservableObject { do { try communicator.runProxyProcess() } catch { - os_log("BWManagement: Running of proxy process failed", type: .error) + Logger.bitWarden.error("BWManagement: Running of proxy process failed") PixelKit.fire(DebugEvent(GeneralPixel.bitwardenNotResponding)) status = .error(error: .runningOfProxyProcessFailed) scheduleConnectionAttempt() @@ -215,7 +216,8 @@ final class BWManager: BWManagement, ObservableObject { status = .notRunning } default: - logOrAssertionFailure("BWManager: Wrong handler") + Logger.bitWarden.fault("BWManager: Wrong handler") + assertionFailure("BWManager: Wrong handler") } } @@ -236,7 +238,8 @@ final class BWManager: BWManagement, ObservableObject { private func handleError(_ error: String) { switch error { case "cannot-decrypt": - logOrAssertionFailure("BWManagement: Bitwarden error - cannot decrypt") + Logger.bitWarden.fault("BWManagement: Bitwarden error - cannot decrypt") + assertionFailure("BWManagement: Bitwarden error - cannot decrypt") PixelKit.fire(DebugEvent(GeneralPixel.bitwardenRespondedCannotDecrypt), frequency: .daily) case "locked": if case let .connected(vault) = status { @@ -245,7 +248,9 @@ final class BWManager: BWManagement, ObservableObject { sendStatus() } return - default: logOrAssertionFailure("BWManager: Unhandled error") + default: + Logger.bitWarden.fault("BWManager: Unhandled error") + assertionFailure("BWManager: Unhandled error") PixelKit.fire(DebugEvent(GeneralPixel.bitwardenRespondedWithError)) } } @@ -293,7 +298,8 @@ final class BWManager: BWManagement, ObservableObject { let ourHmac = encryption.computeHmac(data, iv: ivData) guard ourHmac == hmac else { PixelKit.fire(DebugEvent(GeneralPixel.bitwardenHmacComparisonFailed)) - logOrAssertionFailure("BWManager: HMAC comparison failed") + Logger.bitWarden.fault("BWManager: HMAC comparison failed") + assertionFailure("BWManager: HMAC comparison failed") return } @@ -335,7 +341,8 @@ final class BWManager: BWManagement, ObservableObject { default: break } - logOrAssertionFailure("BWManager: Unhandled response") + Logger.bitWarden.fault("BWManager: Unhandled response") + assertionFailure("BWManager: Unhandled response") } private func handleStatusResponse(payloadItemArray: [BWResponse.PayloadItem]) { @@ -351,7 +358,8 @@ final class BWManager: BWManagement, ObservableObject { private func handleCredentialRetrievalResponse(messageId: MessageId, payload: BWResponse.Payload) { guard let (domain, completion) = retrieveCredentialsCompletionCache[messageId] else { - logOrAssertionFailure("BWManager: Missing or already removed completion block") + Logger.bitWarden.fault("BWManager: Missing or already removed completion block") + assertionFailure("BWManager: Missing or already removed completion block") return } @@ -362,7 +370,8 @@ final class BWManager: BWManagement, ObservableObject { completion(credentials, nil) case .item(let payloadItem): guard let error = payloadItem.error else { - logOrAssertionFailure("BWManager: Unexpected response in credential retrieval") + Logger.bitWarden.fault("BWManager: Unexpected response in credential retrieval") + assertionFailure("BWManager: Unexpected response in credential retrieval") return } @@ -375,7 +384,8 @@ final class BWManager: BWManagement, ObservableObject { private func handleCredentialCreationResponse(messageId: MessageId, payloadItem: BWResponse.PayloadItem) { guard let completion = createCredentialCompletionCache[messageId] else { - logOrAssertionFailure("BWManager: Missing completion block") + Logger.bitWarden.fault("BWManager: Missing completion block") + assertionFailure("BWManager: Missing completion block") return } createCredentialCompletionCache[messageId] = nil @@ -391,7 +401,8 @@ final class BWManager: BWManagement, ObservableObject { private func handleCredentialUpdateResponse(messageId: MessageId, payloadItem: BWResponse.PayloadItem) { guard let completion = updateCredentialCompletionCache[messageId] else { - logOrAssertionFailure("BWManager: Missing completion block") + Logger.bitWarden.fault("BWManager: Missing completion block") + assertionFailure("BWManager: Missing completion block") return } updateCredentialCompletionCache[messageId] = nil @@ -410,13 +421,15 @@ final class BWManager: BWManagement, ObservableObject { func sendHandshake() { guard let publicKey = generateKeyPair() else { - logOrAssertionFailure("BWManager: Public key is missing") + Logger.bitWarden.fault("BWManager: Public key is missing") + assertionFailure("BWManager: Public key is missing") return } guard let messageData = BWRequest.makeHandshakeRequest(with: publicKey, - messageId: messageIdGenerator.generateMessageId()).data else { - logOrAssertionFailure("BWManager: Making the handshake message failed") + messageId: messageIdGenerator.generateMessageId()).data else { + Logger.bitWarden.fault("BWManager: Making the handshake message failed") + assertionFailure("BWManager: Making the handshake message failed") return } @@ -428,7 +441,8 @@ final class BWManager: BWManagement, ObservableObject { let encryptedCommand = encryptCommandData(commandData), let messageData = BWRequest.makeEncryptedCommandRequest(encryptedCommand: encryptedCommand, messageId: messageIdGenerator.generateMessageId()).data else { - logOrAssertionFailure("BWManager: Making the status message failed") + Logger.bitWarden.fault("BWManager: Making the status message failed") + assertionFailure("BWManager: Making the status message failed") PixelKit.fire(DebugEvent(GeneralPixel.bitwardenSendingOfMessageFailed)) status = .error(error: .sendingOfMessageFailed) return @@ -440,11 +454,12 @@ final class BWManager: BWManagement, ObservableObject { private func sendCredentialRetrieval(url: URL, messageId: MessageId) { let payload = BWRequest.EncryptedCommand.Payload(uri: url.absoluteString) guard let commandData = BWRequest.EncryptedCommand(command: .credentialRetrieval, - payload: payload).data, + payload: payload).data, let encryptedCommand = encryptCommandData(commandData), let messageData = BWRequest.makeEncryptedCommandRequest(encryptedCommand: encryptedCommand, messageId: messageId).data else { - logOrAssertionFailure("BWManager: Making the credential retrieval message failed") + Logger.bitWarden.fault("BWManager: Making the credential retrieval message failed") + assertionFailure("BWManager: Making the credential retrieval message failed") PixelKit.fire(DebugEvent(GeneralPixel.bitwardenSendingOfMessageFailed)) status = .error(error: .sendingOfMessageFailed) return @@ -455,16 +470,17 @@ final class BWManager: BWManagement, ObservableObject { private func sendCredentialCreation(_ credential: BWCredential, messageId: MessageId) { let payload = BWRequest.EncryptedCommand.Payload(uri: credential.url, - userId: credential.userId, - userName: credential.username, - password: credential.password, - name: credential.credentialName) + userId: credential.userId, + userName: credential.username, + password: credential.password, + name: credential.credentialName) guard let commandData = BWRequest.EncryptedCommand(command: .credentialCreate, - payload: payload).data, + payload: payload).data, let encryptedCommand = encryptCommandData(commandData), let messageData = BWRequest.makeEncryptedCommandRequest(encryptedCommand: encryptedCommand, messageId: messageId).data else { - logOrAssertionFailure("BWManager: Making the credential creation message failed") + Logger.bitWarden.fault("BWManager: Making the credential creation message failed") + assertionFailure("BWManager: Making the credential creation message failed") PixelKit.fire(DebugEvent(GeneralPixel.bitwardenSendingOfMessageFailed)) status = .error(error: .sendingOfMessageFailed) return @@ -475,17 +491,18 @@ final class BWManager: BWManagement, ObservableObject { private func sendCredentialUpdate(_ credential: BWCredential, messageId: MessageId) { let payload = BWRequest.EncryptedCommand.Payload(uri: credential.url, - userId: credential.userId, - userName: credential.username, - password: credential.password, - name: credential.credentialName, - credentialId: credential.credentialId) + userId: credential.userId, + userName: credential.username, + password: credential.password, + name: credential.credentialName, + credentialId: credential.credentialId) guard let commandData = BWRequest.EncryptedCommand(command: .credentialUpdate, - payload: payload).data, + payload: payload).data, let encryptedCommand = encryptCommandData(commandData), let messageData = BWRequest.makeEncryptedCommandRequest(encryptedCommand: encryptedCommand, messageId: messageId).data else { - logOrAssertionFailure("BWManager: Making the credential update message failed") + Logger.bitWarden.fault("BWManager: Making the credential update message failed") + assertionFailure("BWManager: Making the credential update message failed") PixelKit.fire(DebugEvent(GeneralPixel.bitwardenSendingOfMessageFailed)) status = .error(error: .sendingOfMessageFailed) return @@ -524,7 +541,7 @@ final class BWManager: BWManagement, ObservableObject { @Published private(set) var status: BWStatus = .disabled { didSet { - os_log("Status changed: %s", log: .bitwarden, type: .default, String(describing: status)) + Logger.bitWarden.log("Status changed: \(String(describing: self.status))") // If vault is locked, keep refreshing the latest status if case .connected(vault: let vault) = status, @@ -603,7 +620,8 @@ extension BWManager: BWCommunicatorDelegate { func bitwardenCommunicator(_ bitwardenCommunicator: BWCommunication, didReceiveMessageData messageData: Data) { guard let response = BWResponse(from: messageData) else { - logOrAssertionFailure("BWManager: Can't decode the message") + Logger.bitWarden.fault("BWManager: Can't decode the message") + assertionFailure("BWManager: Can't decode the message") return } @@ -613,7 +631,7 @@ extension BWManager: BWCommunicatorDelegate { } guard let messageId = response.messageId, messageIdGenerator.verify(messageId: messageId) else { - os_log("BWManager: Unkown message id. Ignoring the message", log: .bitwarden, type: .default) + Logger.bitWarden.log("BWManager: Unknown message id. Ignoring the message") return } @@ -635,6 +653,7 @@ extension BWManager: BWCommunicatorDelegate { return } - logOrAssertionFailure("BWManager: Unhandled message from Bitwarden") + Logger.bitWarden.fault("BWManager: Unhandled message from Bitwarden") + assertionFailure("BWManager: Unhandled message from Bitwarden") } } diff --git a/DuckDuckGo/PasswordManager/Bitwarden/Model/BWRequest.swift b/DuckDuckGo/PasswordManager/Bitwarden/Model/BWRequest.swift index 9ea359a3d2..5f80be87eb 100644 --- a/DuckDuckGo/PasswordManager/Bitwarden/Model/BWRequest.swift +++ b/DuckDuckGo/PasswordManager/Bitwarden/Model/BWRequest.swift @@ -17,6 +17,7 @@ // import Foundation +import os.log struct BWRequest: Codable { @@ -109,7 +110,8 @@ struct BWRequest: Codable { do { jsonData = try JSONEncoder().encode(self) } catch { - logOrAssertionFailure("BWRequest: Can't encode the message") + Logger.general.fault("BWRequest: Can't encode the message") + assertionFailure("BWRequest: Can't encode the message") return nil } return jsonData @@ -122,7 +124,8 @@ struct BWRequest: Codable { do { jsonData = try JSONEncoder().encode(self) } catch { - logOrAssertionFailure("BWRequest: Can't encode the message") + Logger.general.fault("BWRequest: Can't encode the message") + assertionFailure("BWRequest: Can't encode the message") return nil } return jsonData diff --git a/DuckDuckGo/PasswordManager/Bitwarden/Model/BWResponse.swift b/DuckDuckGo/PasswordManager/Bitwarden/Model/BWResponse.swift index df71e82c4f..e2b9778b19 100644 --- a/DuckDuckGo/PasswordManager/Bitwarden/Model/BWResponse.swift +++ b/DuckDuckGo/PasswordManager/Bitwarden/Model/BWResponse.swift @@ -17,6 +17,7 @@ // import Foundation +import os.log typealias Base64EncodedString = String typealias MessageId = String @@ -94,7 +95,8 @@ struct BWResponse: Codable { do { self = try JSONDecoder().decode(BWResponse.self, from: messageData) } catch { - logOrAssertionFailure("Decoding the message failed") + Logger.general.fault("Decoding the message failed") + assertionFailure("Decoding the message failed") return nil } } diff --git a/DuckDuckGo/PasswordManager/Bitwarden/Services/BWCommunicator.swift b/DuckDuckGo/PasswordManager/Bitwarden/Services/BWCommunicator.swift index b564e0f089..682981da10 100644 --- a/DuckDuckGo/PasswordManager/Bitwarden/Services/BWCommunicator.swift +++ b/DuckDuckGo/PasswordManager/Bitwarden/Services/BWCommunicator.swift @@ -18,6 +18,7 @@ import Common import Foundation +import os.log protocol BWCommunicatorDelegate: AnyObject { @@ -74,7 +75,7 @@ final class BWCommunicator: BWCommunication { process.terminationHandler = processDidTerminate(_:) try process.run() - os_log("BWCommunicator: Proxy process running", log: .bitwarden, type: .default) + Logger.bitWarden.log("BWCommunicator: Proxy process running") self.process = BitwardenProcess(process: process, readingHandle: outHandle, writingHandle: inputHandle) } @@ -85,7 +86,7 @@ final class BWCommunicator: BWCommunication { } private func processDidTerminate(_ process: Process) { - os_log("BWCommunicator: Proxy process terminated", log: .bitwarden, type: .default) + Logger.bitWarden.log("BWCommunicator: Proxy process terminated") if let runningProcess = self.process?.process { if process != runningProcess { diff --git a/DuckDuckGo/PasswordManager/PasswordManagerCoordinator.swift b/DuckDuckGo/PasswordManager/PasswordManagerCoordinator.swift index ca7145d88d..c09b08cfab 100644 --- a/DuckDuckGo/PasswordManager/PasswordManagerCoordinator.swift +++ b/DuckDuckGo/PasswordManager/PasswordManagerCoordinator.swift @@ -21,6 +21,7 @@ import BrowserServicesKit import Combine import Common import PixelKit +import os.log protocol PasswordManagerCoordinating: BrowserServicesKit.PasswordManager { @@ -207,8 +208,8 @@ final class PasswordManagerCoordinator: PasswordManagerCoordinating { completion: @escaping (Error?) -> Void) { guard case let .connected(vault) = bitwardenManagement.status, let bitwardenCredential = BWCredential(from: credentials, vault: vault) else { + Logger.general.fault("Failed to store credentials: Bitwarden is not connected or bad credential") assertionFailure("Bitwarden is not connected or bad credential") - os_log("Failed to store credentials: Bitwarden is not connected or bad credential", type: .error) return } diff --git a/DuckDuckGo/Permissions/Model/PermissionManager.swift b/DuckDuckGo/Permissions/Model/PermissionManager.swift index d708b30949..af615165eb 100644 --- a/DuckDuckGo/Permissions/Model/PermissionManager.swift +++ b/DuckDuckGo/Permissions/Model/PermissionManager.swift @@ -19,6 +19,7 @@ import Foundation import Combine import Common +import os.log protocol PermissionManagerProtocol: AnyObject { @@ -55,7 +56,7 @@ final class PermissionManager: PermissionManagerProtocol { self.set(entity.permission, forDomain: entity.domain.droppingWwwPrefix(), permissionType: entity.type) } } catch { - os_log("PermissionStore: Failed to load permissions", type: .error) + Logger.general.error("PermissionStore: Failed to load permissions") } } @@ -93,7 +94,7 @@ final class PermissionManager: PermissionManagerProtocol { do { storedPermission = try store.add(domain: domain, permissionType: permissionType, decision: decision) } catch { - os_log("PermissionStore: Failed to store permission", type: .error) + Logger.general.error("PermissionStore: Failed to store permission") return } } diff --git a/DuckDuckGo/PinnedTabs/Model/PinnedTabsManager.swift b/DuckDuckGo/PinnedTabs/Model/PinnedTabsManager.swift index e8d455962d..0f0d15785b 100644 --- a/DuckDuckGo/PinnedTabs/Model/PinnedTabsManager.swift +++ b/DuckDuckGo/PinnedTabs/Model/PinnedTabsManager.swift @@ -20,6 +20,7 @@ import AppKit import Combine import Common import Foundation +import os.log final class PinnedTabsManager { @@ -38,11 +39,11 @@ final class PinnedTabsManager { func unpinTab(at index: Int, published: Bool = false) -> Tab? { guard let tab = tabCollection.tabs[safe: index] else { - os_log("PinnedTabsManager: unable to unpin a tab") + Logger.general.debug("PinnedTabsManager: unable to unpin a tab") return nil } guard tabCollection.removeTab(at: index, published: published) else { - os_log("PinnedTabsManager: unable to unpin a tab") + Logger.general.debug("PinnedTabsManager: unable to unpin a tab") return nil } didUnpinTabSubject.send(index) @@ -55,7 +56,7 @@ final class PinnedTabsManager { func tabViewModel(at index: Int) -> TabViewModel? { guard index >= 0, tabCollection.tabs.count > index else { - os_log("PinnedTabsManager: Index out of bounds", type: .error) + Logger.general.error("PinnedTabsManager: Index out of bounds") return nil } diff --git a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift index 5c77dfdf96..56ca4a9168 100644 --- a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift +++ b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift @@ -19,6 +19,7 @@ import Foundation import Combine import Common +import os.log final class PinnedTabsViewModel: ObservableObject { @@ -164,7 +165,7 @@ extension PinnedTabsViewModel { func unpin(_ tab: Tab) { guard let index = items.firstIndex(of: tab) else { - os_log("PinnedTabsViewModel: Failed to get index of a tab", type: .error) + Logger.bitWarden.error("PinnedTabsViewModel: Failed to get index of a tab") return } contextMenuActionSubject.send(.unpin(index)) @@ -172,7 +173,7 @@ extension PinnedTabsViewModel { func duplicate(_ tab: Tab) { guard let index = items.firstIndex(of: tab) else { - os_log("PinnedTabsViewModel: Failed to get index of a tab", type: .error) + Logger.bitWarden.error("PinnedTabsViewModel: Failed to get index of a tab") return } contextMenuActionSubject.send(.duplicate(index)) @@ -180,7 +181,7 @@ extension PinnedTabsViewModel { func close(_ tab: Tab) { guard let index = items.firstIndex(of: tab) else { - os_log("PinnedTabsViewModel: Failed to get index of a tab", type: .error) + Logger.bitWarden.error("PinnedTabsViewModel: Failed to get index of a tab") return } contextMenuActionSubject.send(.close(index)) diff --git a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift index aa6ffd512a..0513524939 100644 --- a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift +++ b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift @@ -21,6 +21,7 @@ import AppKit import Bookmarks import Common import PixelKit +import os.log protocol AppearancePreferencesPersistor { var showFullURL: Bool { get set } @@ -249,7 +250,7 @@ final class AppearancePreferences: ObservableObject { guard let syncService = (NSApp.delegate as? AppDelegate)?.syncService else { return } - os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") + Logger.sync.debug("Requesting sync if enabled") syncService.scheduler.notifyDataChanged() } } diff --git a/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift b/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift index e8e435c447..81cc59181d 100644 --- a/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift @@ -116,7 +116,7 @@ final class DownloadsPreferences: ObservableObject { if isStale { setSelectedDownloadLocation(url) // update bookmark data and selectedDownloadLocationController } else { - selectedDownloadLocationController = SecurityScopedFileURLController(url: url, logger: OSLog.downloads) + selectedDownloadLocationController = SecurityScopedFileURLController(url: url) } return url } @@ -135,7 +135,7 @@ final class DownloadsPreferences: ObservableObject { } private func setSelectedDownloadLocation(_ url: URL?) { - selectedDownloadLocationController = url.map { SecurityScopedFileURLController(url: $0, logger: OSLog.downloads) } + selectedDownloadLocationController = url.map { SecurityScopedFileURLController(url: $0) } let locationString: String? #if APPSTORE locationString = (try? url?.bookmarkData(options: .withSecurityScope).base64EncodedString()) ?? url?.absoluteString diff --git a/DuckDuckGo/Preferences/Model/SyncPreferences.swift b/DuckDuckGo/Preferences/Model/SyncPreferences.swift index 99a5416edd..fb42705f35 100644 --- a/DuckDuckGo/Preferences/Model/SyncPreferences.swift +++ b/DuckDuckGo/Preferences/Model/SyncPreferences.swift @@ -26,6 +26,7 @@ import SwiftUI import PDFKit import Navigation import PixelKit +import os.log extension SyncDevice { init(_ account: SyncAccount) { @@ -371,7 +372,7 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { func refreshDevices() { guard !isScreenLocked else { - os_log(.debug, log: .sync, "Screen is locked, skipping devices refresh") + Logger.sync.debug("Screen is locked, skipping devices refresh") return } guard syncService.account != nil else { @@ -383,7 +384,7 @@ final class SyncPreferences: ObservableObject, SyncUI.ManagementViewModel { let registeredDevices = try await syncService.fetchDevices() mapDevices(registeredDevices) } catch { - os_log(.error, log: .sync, "Failed to refresh devices: \(error)") + Logger.sync.debug("Failed to refresh devices: \(error)") } } } diff --git a/DuckDuckGo/Preferences/View/PreferencesSyncView.swift b/DuckDuckGo/Preferences/View/PreferencesSyncView.swift index b78c746ab3..0047801645 100644 --- a/DuckDuckGo/Preferences/View/PreferencesSyncView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesSyncView.swift @@ -21,6 +21,7 @@ import Common import SyncUI import SwiftUIExtensions import BrowserServicesKit +import os.log struct SyncView: View { @@ -46,7 +47,7 @@ struct SyncView: View { guard let syncService = (NSApp.delegate as? AppDelegate)?.syncService else { return } - os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") + Logger.sync.debug("Requesting sync if enabled") syncService.scheduler.notifyDataChanged() } } diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift index 93259608d9..2b124ba79a 100644 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift +++ b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardViewController.swift @@ -23,6 +23,7 @@ import BrowserServicesKit import PrivacyDashboard import Common import PixelKit +import os.log protocol PrivacyDashboardViewControllerSizeDelegate: AnyObject { @@ -265,7 +266,7 @@ extension PrivacyDashboardViewController: PrivacyDashboardControllerDelegate { let report = try await makeBrokenSiteReport(category: category, description: description, source: privacyDashboardController.source) try brokenSiteReporter.report(report, reportMode: .regular) } catch { - os_log("Failed to generate or send the broken site report: \(error.localizedDescription)", type: .error) + Logger.general.error("Failed to generate or send the broken site report: \(error.localizedDescription)") } } } @@ -283,7 +284,7 @@ extension PrivacyDashboardViewController: PrivacyDashboardControllerDelegate { let report = try await makeBrokenSiteReport(source: source) try toggleProtectionsOffReporter.report(report, reportMode: .toggle) } catch { - os_log("Failed to generate or send the broken site report: %@", type: .error, error.localizedDescription) + Logger.general.error("Failed to generate or send the broken site report: \(error.localizedDescription)") } } } diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingDebugMenu.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingDebugMenu.swift index 1543a87946..d3a6bf5c36 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingDebugMenu.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingDebugMenu.swift @@ -19,6 +19,7 @@ import AppKit import RemoteMessaging import AppKitExtensions +import BrowserServicesKit final class RemoteMessagingDebugMenu: NSMenu { diff --git a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift index c3a0b0fe7d..3a8e2ee45a 100644 --- a/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/SecureVault/View/PasswordManagementViewController.swift @@ -25,6 +25,7 @@ import Foundation import SecureStorage import SwiftUI import PixelKit +import os.log protocol PasswordManagementDelegate: AnyObject { @@ -1012,7 +1013,7 @@ final class PasswordManagementViewController: NSViewController { guard let syncService = NSApp.delegateTyped.syncService else { return } - os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") + Logger.sync.debug("Requesting sync if enabled") syncService.scheduler.requestSyncImmediately() } diff --git a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift index cddee05928..ae751fb401 100644 --- a/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveCredentialsViewController.swift @@ -21,6 +21,7 @@ import BrowserServicesKit import Combine import Common import PixelKit +import os.log protocol SaveCredentialsDelegate: AnyObject { @@ -228,27 +229,27 @@ final class SaveCredentialsViewController: NSViewController { do { if passwordManagerCoordinator.isEnabled { guard !passwordManagerCoordinator.isLocked else { - os_log("Failed to store credentials: Password manager is locked") + Logger.sync.error("Failed to store credentials: Password manager is locked") return } passwordManagerCoordinator.storeWebsiteCredentials(credentials) { error in if let error = error { - os_log("Failed to store credentials: %s", type: .error, error.localizedDescription) + Logger.sync.error("Failed to store credentials: \(error.localizedDescription)") } } } else { let vault = try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared) _ = try vault.storeWebsiteCredentials(credentials) NSApp.delegateTyped.syncService?.scheduler.notifyDataChanged() - os_log(.debug, log: OSLog.sync, "Requesting sync if enabled") + Logger.sync.debug("Requesting sync if enabled") if existingCredentials?.account.id == nil, !LocalPinningManager.shared.isPinned(.autofill), let count = try? vault.accountsCount(), count == 1 { shouldFirePinPromptNotification = true } } } catch { - os_log("%s:%s: failed to store credentials %s", type: .error, className, #function, error.localizedDescription) + Logger.sync.error("failed to store credentials \(error.localizedDescription)") PixelKit.fire(DebugEvent(GeneralPixel.secureVaultError(error: error))) } @@ -317,7 +318,7 @@ final class SaveCredentialsViewController: NSViewController { } guard let window = view.window else { - os_log("%s: Window is nil", type: .error, className) + Logger.sync.error("Window is nil") notifyDelegate() return } @@ -343,7 +344,7 @@ final class SaveCredentialsViewController: NSViewController { do { _ = try AutofillNeverPromptWebsitesManager.shared.saveNeverPromptWebsite(domainLabel.stringValue) } catch { - os_log("%: failed to save never prompt for website %s", type: .error, #function, error.localizedDescription) + Logger.sync.error("failed to save never prompt for website \(error.localizedDescription)") } PixelKit.fire(GeneralPixel.autofillLoginsSaveLoginModalExcludeSiteConfirmed) @@ -405,7 +406,7 @@ final class SaveCredentialsViewController: NSViewController { if passwordManagerCoordinator.isEnabled { guard !passwordManagerCoordinator.isLocked else { - os_log("Failed to access credentials: Password manager is locked") + Logger.sync.debug("Failed to access credentials: Password manager is locked") return existingCredentials } diff --git a/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift b/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift index 61ce88b628..f10565a27a 100644 --- a/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift +++ b/DuckDuckGo/SecureVault/View/SaveIdentityViewController.swift @@ -21,6 +21,7 @@ import BrowserServicesKit import Combine import Common import PixelKit +import os.log protocol SaveIdentityDelegate: AnyObject { @@ -74,7 +75,7 @@ final class SaveIdentityViewController: NSViewController { try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared).storeIdentity(identity) PixelKit.fire(GeneralPixel.autofillItemSaved(kind: .identity)) } catch { - os_log("%s:%s: failed to store identity %s", type: .error, className, #function, error.localizedDescription) + Logger.general.error("Failed to store identity \(error.localizedDescription)") PixelKit.fire(DebugEvent(GeneralPixel.secureVaultError(error: error))) } } diff --git a/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift b/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift index 31c9d6a3b3..239a7f441e 100644 --- a/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift +++ b/DuckDuckGo/SecureVault/View/SavePaymentMethodViewController.swift @@ -21,6 +21,7 @@ import BrowserServicesKit import Combine import Common import PixelKit +import os.log protocol SavePaymentMethodDelegate: AnyObject { @@ -94,7 +95,7 @@ final class SavePaymentMethodViewController: NSViewController { do { try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared).storeCreditCard(paymentMethod) } catch { - os_log("%s:%s: failed to store payment method %s", type: .error, className, #function, error.localizedDescription) + Logger.secureVault.error("Failed to store payment method \(error.localizedDescription)") PixelKit.fire(DebugEvent(GeneralPixel.secureVaultError(error: error))) } } diff --git a/DuckDuckGo/SmarterEncryption/PrivacyFeatures.swift b/DuckDuckGo/SmarterEncryption/PrivacyFeatures.swift index cb16b2bcef..f97115cb49 100644 --- a/DuckDuckGo/SmarterEncryption/PrivacyFeatures.swift +++ b/DuckDuckGo/SmarterEncryption/PrivacyFeatures.swift @@ -21,6 +21,7 @@ import Common import Foundation import Persistence import PixelKit +import os.log protocol PrivacyFeaturesProtocol { var contentBlocking: AnyContentBlocking { get } @@ -78,13 +79,13 @@ final class AppPrivacyFeatures: PrivacyFeaturesProtocol { convenience init(contentBlocking: AnyContentBlocking, database: CoreDataDatabase) { let bloomFilterDataURL = URL.sandboxApplicationSupportURL.appendingPathComponent("HttpsBloomFilter.bin") - let httpsUpgradeStore = AppHTTPSUpgradeStore(database: database, bloomFilterDataURL: bloomFilterDataURL, embeddedResources: Self.embeddedBloomFilterResources, errorEvents: Self.httpsUpgradeDebugEvents, log: .httpsUpgrade) + let httpsUpgradeStore = AppHTTPSUpgradeStore(database: database, bloomFilterDataURL: bloomFilterDataURL, embeddedResources: Self.embeddedBloomFilterResources, errorEvents: Self.httpsUpgradeDebugEvents, logger: Logger.httpsUpgrade) self.init(contentBlocking: contentBlocking, httpsUpgradeStore: httpsUpgradeStore) } init(contentBlocking: AnyContentBlocking, httpsUpgradeStore: HTTPSUpgradeStore) { self.contentBlocking = contentBlocking - self.httpsUpgrade = HTTPSUpgrade(store: httpsUpgradeStore, privacyManager: contentBlocking.privacyConfigurationManager, log: .httpsUpgrade) + self.httpsUpgrade = HTTPSUpgrade(store: httpsUpgradeStore, privacyManager: contentBlocking.privacyConfigurationManager, logger: Logger.httpsUpgrade) } } diff --git a/DuckDuckGo/StateRestoration/AppStateRestorationManager.swift b/DuckDuckGo/StateRestoration/AppStateRestorationManager.swift index 6644e0ae71..3b6c91347a 100644 --- a/DuckDuckGo/StateRestoration/AppStateRestorationManager.swift +++ b/DuckDuckGo/StateRestoration/AppStateRestorationManager.swift @@ -20,6 +20,7 @@ import Foundation import Combine import Common import PixelKit +import os.log @MainActor final class AppStateRestorationManager: NSObject { @@ -72,7 +73,7 @@ final class AppStateRestorationManager: NSObject { } catch CocoaError.fileReadNoSuchFile { // ignore } catch { - os_log("App state could not be decoded: %s", "\(error)") + Logger.general.error("App state could not be decoded: \(error.localizedDescription)") PixelKit.fire(DebugEvent(GeneralPixel.appStateRestorationFailed, error: error), withAdditionalParameters: ["interactive": String(interactive)]) } @@ -141,7 +142,7 @@ final class AppStateRestorationManager: NSObject { } catch CocoaError.fileReadNoSuchFile { // ignore } catch { - os_log("Pinned tabs state could not be decoded: %s", "\(error)") + Logger.general.error("Pinned tabs state could not be decoded: \(error)") PixelKit.fire(DebugEvent(GeneralPixel.appStateRestorationFailed, error: error)) } } diff --git a/DuckDuckGo/StateRestoration/WindowManager+StateRestoration.swift b/DuckDuckGo/StateRestoration/WindowManager+StateRestoration.swift index aaa38da89c..c0f97116f0 100644 --- a/DuckDuckGo/StateRestoration/WindowManager+StateRestoration.swift +++ b/DuckDuckGo/StateRestoration/WindowManager+StateRestoration.swift @@ -18,6 +18,7 @@ import Cocoa import Common +import os.log extension WindowsManager { @@ -90,8 +91,7 @@ final class WindowManagerStateRestoration: NSObject, NSSecureCoding { init?(coder: NSCoder) { guard let restorationArray = coder.decodeObject(of: [NSArray.self, WindowRestorationItem.self], forKey: NSSecureCodingKeys.controllers) as? [WindowRestorationItem] else { - os_log("WindowsManager:initWithCoder: could not decode Restoration Array: %s", type: .error, - String(describing: coder.error)) + Logger.general.error("WindowsManager:initWithCoder: could not decode Restoration Array: \(String(describing: coder.error), privacy: .public)") return nil } self.windows = restorationArray @@ -159,7 +159,7 @@ final class WindowRestorationItem: NSObject, NSSecureCoding { required init?(coder: NSCoder) { guard let model = coder.decodeObject(of: TabCollectionViewModel.self, forKey: NSSecureCodingKeys.model) else { - os_log("WindowRestoration:initWithCoder: could not decode model object: %s", type: .error, String(describing: coder.error)) + Logger.general.error("WindowRestoration:initWithCoder: could not decode model object: \(String(describing: coder.error))") return nil } self.model = model diff --git a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift index af022508e6..368f26a504 100644 --- a/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift +++ b/DuckDuckGo/Statistics/ATB/StatisticsLoader.swift @@ -21,6 +21,7 @@ import Foundation import BrowserServicesKit import Networking import PixelKit +import os.log final class StatisticsLoader { @@ -87,19 +88,19 @@ final class StatisticsLoader { guard !isAppRetentionRequestInProgress else { return } isAppRetentionRequestInProgress = true - os_log("Requesting install statistics", log: .atb, type: .debug) + Logger.atb.debug("Requesting install statistics") let configuration = APIRequest.Configuration(url: URL.initialAtb) let request = APIRequest(configuration: configuration, urlSession: URLSession.session(useMainThreadCallbackQueue: true)) request.fetch { response, error in self.isAppRetentionRequestInProgress = false if let error = error { - os_log("Initial atb request failed with error %s", type: .error, error.localizedDescription) + Logger.atb.error("Initial atb request failed with error \(error.localizedDescription)") completion() return } - os_log("Install statistics request succeeded", log: .atb, type: .debug) + Logger.atb.debug("Install statistics request succeeded") if let data = response?.data, let atb = try? self.parser.convert(fromJsonData: data) { self.requestExti(atb: atb, completion: completion) @@ -116,7 +117,7 @@ final class StatisticsLoader { guard !isAppRetentionRequestInProgress else { return } self.isAppRetentionRequestInProgress = true - os_log("Requesting exti", log: .atb, type: .debug) + Logger.atb.debug("Requesting exti") let installAtb = atb.version + (statisticsStore.variant ?? "") @@ -125,12 +126,12 @@ final class StatisticsLoader { request.fetch { _, error in self.isAppRetentionRequestInProgress = false if let error = error { - os_log("Exti request failed with error %s", type: .error, error.localizedDescription) + Logger.atb.error("Extit request failed with error \(error.localizedDescription)") completion() return } - os_log("Exti request succeeded", log: .atb, type: .debug) + Logger.atb.debug("Exti request succeeded") assert(self.statisticsStore.atb == nil) assert(self.statisticsStore.installDate == nil) @@ -151,19 +152,19 @@ final class StatisticsLoader { return } - os_log("Requesting search retention ATB", log: .atb, type: .debug) + Logger.atb.debug("Requesting search retention ATB") let url = URL.searchAtb(atbWithVariant: atbWithVariant, setAtb: searchRetentionAtb, isSignedIntoEmailProtection: emailManager.isSignedIn) let configuration = APIRequest.Configuration(url: url) let request = APIRequest(configuration: configuration, urlSession: URLSession.session(useMainThreadCallbackQueue: true)) request.fetch { (response, error) in if let error = error { - os_log("Search atb request failed with error %s", type: .error, error.localizedDescription) + Logger.atb.error("Search atb request failed with error \(error.localizedDescription)") completion() return } - os_log("Search retention ATB request succeeded", log: .atb, type: .debug) + Logger.atb.debug("Search retention ATB request succeeded") if let data = response?.data, let atb = try? self.parser.convert(fromJsonData: data) { self.statisticsStore.searchRetentionAtb = atb.version @@ -186,7 +187,7 @@ final class StatisticsLoader { return } - os_log("Requesting app retention ATB", log: .atb, type: .debug) + Logger.atb.debug("Requesting app retention ATB") isAppRetentionRequestInProgress = true @@ -197,12 +198,12 @@ final class StatisticsLoader { self.isAppRetentionRequestInProgress = false if let error = error { - os_log("App atb request failed with error %s", type: .error, error.localizedDescription) + Logger.atb.error("App atb request failed with error \(error.localizedDescription)") completion() return } - os_log("App retention ATB request succeeded", log: .atb, type: .debug) + Logger.atb.debug("App retention ATB request succeeded") if let data = response?.data, let atb = try? self.parser.convert(fromJsonData: data) { self.statisticsStore.appRetentionAtb = atb.version diff --git a/DuckDuckGo/Statistics/ATB/VariantManager.swift b/DuckDuckGo/Statistics/ATB/VariantManager.swift index b63b38c301..060d05d3fc 100644 --- a/DuckDuckGo/Statistics/ATB/VariantManager.swift +++ b/DuckDuckGo/Statistics/ATB/VariantManager.swift @@ -19,6 +19,7 @@ import BrowserServicesKit import Common import Foundation +import os.log struct Variant: BrowserServicesKit.Variant { @@ -78,17 +79,17 @@ final class DefaultVariantManager: VariantManager { func assignVariantIfNeeded(_ newInstallCompletion: (VariantManager) -> Void) { guard !storage.hasInstallStatistics else { - os_log("ATB: No new variant needed for existing user", type: .debug) + Logger.atb.debug("ATB: No new variant needed for existing user") return } if let variant = currentVariant { - os_log("ATB: Already assigned variant: %s", type: .debug, String(describing: variant)) + Logger.atb.debug("ATB: Already assigned variant: \(String(describing: variant))") return } guard let variant = selectVariant() else { - os_log("ATB: Failed to assign variant", type: .debug) + Logger.atb.debug("ATB: Failed to assign variant") // it's possible this failed because there are none to assign, we should still let new install logic execute _ = newInstallCompletion(self) diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 94f4a59e46..1c9b8927c3 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -21,6 +21,7 @@ import Suggestions import Common import History import PixelKit +import os.log final class SuggestionContainer { @@ -63,18 +64,14 @@ final class SuggestionContainer { guard self?.latestQuery == query else { return } guard let result = result else { self?.result = nil - os_log("Suggestions: Failed to get suggestions - %s", - type: .error, - "\(String(describing: error))") + Logger.general.error("Suggestions: Failed to get suggestions - \(String(describing: error))") PixelKit.fire(DebugEvent(GeneralPixel.suggestionsFetchFailed, error: error)) return } if let error = error { // Fetching remote suggestions failed but local can be presented - os_log("Suggestions: Error when getting suggestions - %s", - type: .error, - "\(String(describing: error))") + Logger.general.error("Suggestions: Error when getting suggestions - \(error.localizedDescription)") } self?.result = result diff --git a/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift b/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift index b28e21545d..2f7a2823f0 100644 --- a/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift +++ b/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift @@ -18,6 +18,7 @@ import Cocoa import Common +import os.log final class SuggestionTableCellView: NSTableCellView { @@ -57,7 +58,7 @@ final class SuggestionTableCellView: NSTableCellView { private func updateTextField() { guard let attributedString = attributedString else { - os_log("SuggestionTableCellView: Attributed strings are nil", type: .error) + Logger.general.error("SuggestionTableCellView: Attributed strings are nil") return } if isSelected { diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift index 8ad5149a9b..2a90c1747d 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionContainerViewModel.swift @@ -20,6 +20,7 @@ import Foundation import Combine import Common import BrowserServicesKit +import os.log final class SuggestionContainerViewModel { @@ -110,7 +111,7 @@ final class SuggestionContainerViewModel { let items = suggestionContainer.result?.all ?? [] guard index < items.count else { - os_log("SuggestionContainerViewModel: Absolute index is out of bounds", type: .error) + Logger.general.error("SuggestionContainerViewModel: Absolute index is out of bounds") return nil } @@ -119,7 +120,7 @@ final class SuggestionContainerViewModel { func select(at index: Int) { guard index >= 0, index < numberOfSuggestions else { - os_log("SuggestionContainerViewModel: Index out of bounds", type: .error) + Logger.general.error("SuggestionContainerViewModel: Index out of bounds") selectionIndex = nil return } diff --git a/DuckDuckGo/Sync/SyncBookmarksAdapter.swift b/DuckDuckGo/Sync/SyncBookmarksAdapter.swift index 53fc8a10fb..f23ccc4357 100644 --- a/DuckDuckGo/Sync/SyncBookmarksAdapter.swift +++ b/DuckDuckGo/Sync/SyncBookmarksAdapter.swift @@ -23,6 +23,7 @@ import DDGSync import Persistence import SyncDataProviders import PixelKit +import os.log public class BookmarksFaviconsFetcherErrorHandler: EventMapping { @@ -74,8 +75,7 @@ final class SyncBookmarksAdapter { self.syncErrorHandler = syncErrorHandler databaseCleaner = BookmarkDatabaseCleaner( bookmarkDatabase: database, - errorEvents: BookmarksCleanupErrorHandling(), - log: .bookmarks + errorEvents: BookmarksCleanupErrorHandling() ) } @@ -106,7 +106,6 @@ final class SyncBookmarksAdapter { database: database, metadataStore: metadataStore, metricsEvents: metricsEventsHandler, - log: OSLog.sync, syncDidUpdateData: { [weak self] in self?.syncErrorHandler.syncBookmarksSucceded() guard let manager = self?.bookmarkManager as? LocalBookmarkManager else { return } @@ -142,7 +141,7 @@ final class SyncBookmarksAdapter { stateStore = try BookmarksFaviconsFetcherStateStore(applicationSupportURL: URL.sandboxApplicationSupportURL) } catch { PixelKit.fire(DebugEvent(GeneralPixel.bookmarksFaviconsFetcherStateStoreInitializationFailed, error: error)) - os_log(.error, log: OSLog.sync, "Failed to initialize BookmarksFaviconsFetcherStateStore: %{public}s", String(reflecting: error)) + Logger.sync.error("Failed to initialize BookmarksFaviconsFetcherStateStore: \(String(reflecting: error), privacy: .public)") return nil } @@ -151,8 +150,7 @@ final class SyncBookmarksAdapter { stateStore: stateStore, fetcher: FaviconFetcher(), faviconStore: FaviconManager.shared, - errorEvents: BookmarksFaviconsFetcherErrorHandler(), - log: .sync + errorEvents: BookmarksFaviconsFetcherErrorHandler() ) } diff --git a/DuckDuckGo/Sync/SyncCredentialsAdapter.swift b/DuckDuckGo/Sync/SyncCredentialsAdapter.swift index aa857a40cb..fe3c682a4a 100644 --- a/DuckDuckGo/Sync/SyncCredentialsAdapter.swift +++ b/DuckDuckGo/Sync/SyncCredentialsAdapter.swift @@ -66,7 +66,6 @@ final class SyncCredentialsAdapter { secureVaultErrorReporter: SecureVaultReporter.shared, metadataStore: metadataStore, metricsEvents: metricsEventsHandler, - log: OSLog.sync, syncDidUpdateData: { [weak self] in self?.syncDidCompleteSubject.send() self?.syncErrorHandler.syncCredentialsSucceded() diff --git a/DuckDuckGo/Sync/SyncErrorHandler.swift b/DuckDuckGo/Sync/SyncErrorHandler.swift index bb73ba5dcc..f44cf0edf1 100644 --- a/DuckDuckGo/Sync/SyncErrorHandler.swift +++ b/DuckDuckGo/Sync/SyncErrorHandler.swift @@ -23,6 +23,7 @@ import PixelKit import Persistence import Combine import SyncDataProviders +import os.log /// The SyncErrorHandling protocol defines methods for handling sync errors related to specific data types such as bookmarks and credentials. protocol SyncErrorHandling { @@ -252,7 +253,7 @@ extension SyncErrorHandler: SyncErrorHandling { PixelKit.fire(DebugEvent(modelType.syncFailedPixel, error: error), withAdditionalParameters: params) } let modelTypeString = modelType.rawValue.capitalized - os_log(.error, log: OSLog.sync, "%{public}@ Sync error: %{public}s", modelTypeString, String(reflecting: error)) + Logger.sync.error("\(modelTypeString, privacy: .public) Sync error: \(String(reflecting: error), privacy: .public)") } } diff --git a/DuckDuckGo/Sync/SyncSettingsAdapter.swift b/DuckDuckGo/Sync/SyncSettingsAdapter.swift index 4cac3ddcc3..300b8305fd 100644 --- a/DuckDuckGo/Sync/SyncSettingsAdapter.swift +++ b/DuckDuckGo/Sync/SyncSettingsAdapter.swift @@ -56,7 +56,6 @@ final class SyncSettingsAdapter { metadataStore: metadataStore, settingsHandlers: [FavoritesDisplayModeSyncHandler(), EmailProtectionSyncHandler(emailManager: emailManager)], metricsEvents: metricsEventsHandler, - log: OSLog.sync, syncDidUpdateData: { [weak self] in self?.syncDidCompleteSubject.send() } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index c556a18b0f..6ffad6c568 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -56,7 +56,7 @@ protocol NewWindowPolicyDecisionMaker { fileprivate weak var delegate: TabDelegate? func setDelegate(_ delegate: TabDelegate) { self.delegate = delegate } - private let navigationDelegate = DistributedNavigationDelegate(log: .navigation) + private let navigationDelegate = DistributedNavigationDelegate() private var newWindowPolicyDecisionMakers: [NewWindowPolicyDecisionMaker]? private var onNewWindow: ((WKNavigationAction?) -> NavigationDecision)? @@ -697,7 +697,7 @@ protocol NewWindowPolicyDecisionMaker { } guard let backForwardNavigation else { - os_log(.error, "item `\(item.title ?? "") – \(item.url?.absoluteString ?? "")` is not in the backForwardList") + Logger.navigation.error("item `\(item.title ?? "") – \(item.url?.absoluteString ?? "")` is not in the backForwardList") return nil } diff --git a/DuckDuckGo/Tab/Model/TabExtensionsBuilder.swift b/DuckDuckGo/Tab/Model/TabExtensionsBuilder.swift index 4dcb0f822d..72f6c579c5 100644 --- a/DuckDuckGo/Tab/Model/TabExtensionsBuilder.swift +++ b/DuckDuckGo/Tab/Model/TabExtensionsBuilder.swift @@ -20,6 +20,7 @@ import Combine import Foundation import AppKit import Common +import os.log protocol TabExtensionsBuilderProtocol { @MainActor @@ -237,7 +238,7 @@ struct TabExtensions { #if DEBUG assert(!NSApp.runType.requiresEnvironment || tabExtension != nil) #else - os_log("%s Tab Extension not initialised for Unit Tests, activate it in TabExtensions.swift", log: .autoconsent, type: .debug, "\(T.self)") + Logger.autoconsent.debug("Tab Extension not initialised for Unit Tests, activate it in TabExtensions.swift") #endif return tabExtension } diff --git a/DuckDuckGo/Tab/Services/WebsiteDataStore.swift b/DuckDuckGo/Tab/Services/WebsiteDataStore.swift index eb003c99fe..69504c873e 100644 --- a/DuckDuckGo/Tab/Services/WebsiteDataStore.swift +++ b/DuckDuckGo/Tab/Services/WebsiteDataStore.swift @@ -19,6 +19,7 @@ import Common import WebKit import GRDB +import os.log public protocol HTTPCookieStore { func allCookies() async -> [HTTPCookie] @@ -74,7 +75,7 @@ internal class WebCacheManager { do { try fm.createDirectory(at: tmpDir, withIntermediateDirectories: false, attributes: nil) } catch { - os_log("Could not create temporary directory: %s", type: .error, "\(error)") + Logger.general.error("Could not create temporary directory: \(error.localizedDescription)") return } @@ -104,7 +105,7 @@ internal class WebCacheManager { do { try fm.createDirectory(at: tmpDir, withIntermediateDirectories: false, attributes: nil) } catch { - os_log("Could not create temporary directory: %s", type: .error, "\(error)") + Logger.general.error("Could not create temporary directory: \(error.localizedDescription)") return } @@ -157,7 +158,7 @@ internal class WebCacheManager { } for cookie in cookiesToRemove { - os_log("Deleting cookie for %s named %s", log: .fire, cookie.domain, cookie.name) + Logger.fire.debug("Deleting cookie for \(cookie.domain) named \(cookie.name)") await cookieStore.deleteCookie(cookie) } } @@ -208,7 +209,7 @@ internal class WebCacheManager { } } } catch { - os_log("Failed to clear observations database: %s", log: .fire, error.localizedDescription) + Logger.fire.error("Failed to clear observations database: \(error.localizedDescription)") } } diff --git a/DuckDuckGo/Tab/TabExtensions/AdClickAttributionTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/AdClickAttributionTabExtension.swift index 89ebd0716b..abae248fef 100644 --- a/DuckDuckGo/Tab/TabExtensions/AdClickAttributionTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/AdClickAttributionTabExtension.swift @@ -84,7 +84,6 @@ final class AdClickAttributionTabExtension: TabExtension { tld: dependencies.tld, eventReporting: dependencies.attributionEvents, errorReporting: dependencies.attributionDebugEvents, - log: OSLog.attribution, cpmExperimentOn: cpmExperimentOn) detection.delegate = delegate return detection @@ -96,8 +95,7 @@ final class AdClickAttributionTabExtension: TabExtension { rulesProvider: dependencies.adClickAttributionRulesProvider, tld: dependencies.tld, eventReporting: dependencies.attributionEvents, - errorReporting: dependencies.attributionDebugEvents, - log: OSLog.attribution) + errorReporting: dependencies.attributionDebugEvents) } private static func makeAdClickAttribution(with dependencies: any AdClickAttributionDependencies, cpmExperimentOn: Bool?) -> (AdClickLogicProtocol, AdClickAttributionDetecting) { diff --git a/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift b/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift index 9374d573bf..24c999b913 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabSnapshotExtension.swift @@ -21,6 +21,7 @@ import Common import Foundation import Navigation import WebKit +import os.log final class TabSnapshotExtension { @@ -88,7 +89,7 @@ final class TabSnapshotExtension { guard let self = self else { return } guard let image = await self.store.loadSnapshot(for: identifier) as NSImage? else { - os_log("No snapshot restored", log: .tabSnapshots) + Logger.tabSnapshots.debug("No snapshot restored") return } @@ -101,7 +102,7 @@ final class TabSnapshotExtension { image: image, webviewBoundsSize: NSSize.zero, isRestored: true) - os_log("Snapshot restored", log: .tabSnapshots) + Logger.tabSnapshots.debug("Snapshot restored") self.renderSnapshotAfterLoad = false } @@ -168,7 +169,7 @@ final class TabSnapshotExtension { !userDidInteractWithWebsite, snapshotData.webviewBoundsSize == webView.bounds.size, snapshotData.url == url { - os_log("Skipping snapshot rendering, it is already rendered. url: \(url)", log: .tabSnapshots) + Logger.tabSnapshots.debug("Skipping snapshot rendering, it is already rendered. url: \(url)") return } @@ -195,7 +196,7 @@ final class TabSnapshotExtension { } snapshotData = SnapshotData.snapshotDataForRegularView(from: snapshot) - os_log("Snapshot of native page rendered", log: .tabSnapshots) + Logger.tabSnapshots.debug("Snapshot of native page rendered") } } @@ -231,7 +232,7 @@ extension TabSnapshotExtension: NSCodingExtension { guard let uuidString = decoder.decodeObject(of: NSString.self, forKey: NSSecureCodingKeys.tabSnapshotIdentifier), let identifier = UUID(uuidString: uuidString as String) else { - os_log("Snapshot not available in the session state", log: .tabSnapshots) + Logger.tabSnapshots.debug("Snapshot not available in the session state") return } diff --git a/DuckDuckGo/Tab/TabLazyLoader/TabLazyLoader.swift b/DuckDuckGo/Tab/TabLazyLoader/TabLazyLoader.swift index b87239b969..f3bb827a15 100644 --- a/DuckDuckGo/Tab/TabLazyLoader/TabLazyLoader.swift +++ b/DuckDuckGo/Tab/TabLazyLoader/TabLazyLoader.swift @@ -19,6 +19,7 @@ import Foundation import Combine import Common +import os.log final class TabLazyLoader { @@ -43,7 +44,7 @@ final class TabLazyLoader { init?(dataSource: DataSource) { guard dataSource.qualifiesForLazyLoading else { - os_log("Lazy loading not applicable", log: .tabLazyLoading, type: .debug) + Logger.tabLazyLoading.debug("Lazy loading not applicable") return nil } @@ -52,7 +53,7 @@ final class TabLazyLoader { if let selectedTabIndex = dataSource.selectedTabIndex, dataSource.tabs.filter({ $0.isUrl }).count > Const.maxNumberOfLazyLoadedTabs { - os_log("%d open URL tabs, will load adjacent tabs first", log: .tabLazyLoading, type: .debug, dataSource.tabs.count) + Logger.tabLazyLoading.debug("\(dataSource.tabs.count) open URL tabs, will load adjacent tabs first") shouldLoadAdjacentTabs = true // Adjacent tab loading only applies to non-pinned tabs. If a pinned tab is selected, @@ -65,7 +66,7 @@ final class TabLazyLoader { func scheduleLazyLoading() { guard let currentTab = dataSource.selectedTab else { - os_log("Lazy loading not applicable", log: .tabLazyLoading, type: .debug) + Logger.tabLazyLoading.debug("Lazy loading not applicable") lazyLoadingDidFinishSubject.send(false) return } @@ -115,7 +116,7 @@ final class TabLazyLoader { private func startLazyLoadingRecentlySelectedTabs() { guard hasAnyTabsToLoad() else { - os_log("No tabs to load", log: .tabLazyLoading, type: .debug) + Logger.tabLazyLoading.debug("No tabs to load") let loadedAnyTab = numberOfTabsRemaining < Const.maxNumberOfLazyLoadedTabs lazyLoadingDidFinishSubject.send(loadedAnyTab) return @@ -125,12 +126,12 @@ final class TabLazyLoader { .prefix(Const.maxNumberOfLazyLoadedTabs) .sink(receiveCompletion: { [weak self] _ in - os_log("Lazy tab loading finished, preloaded %d tabs", log: .tabLazyLoading, type: .debug, Const.maxNumberOfLazyLoadedTabs) + Logger.tabLazyLoading.debug("Lazy tab loading finished, preloaded \(Const.maxNumberOfLazyLoadedTabs) tabs") self?.lazyLoadingDidFinishSubject.send(true) }, receiveValue: { [weak self] tab in - os_log("Tab did finish loading %s", log: .tabLazyLoading, type: .debug, String(reflecting: tab.url)) + Logger.tabLazyLoading.debug("Tab did finish loading \(String(reflecting: tab.url))") self?.numberOfTabsInProgress.value -= 1 }) @@ -149,7 +150,7 @@ final class TabLazyLoader { guard let self = self else { return false } if self.dataSource.isSelectedTabLoading { - os_log("Selected tab is currently loading, pausing lazy loading until it finishes", log: .tabLazyLoading, type: .debug) + Logger.tabLazyLoading.debug("Selected tab is currently loading, pausing lazy loading until it finishes") self.isLazyLoadingPausedSubject.send(true) return false } @@ -169,7 +170,7 @@ final class TabLazyLoader { private func findAndReloadNextTab() { guard numberOfTabsRemaining > 0 else { - os_log("Maximum allowed tabs loaded (%d), skipping", log: .tabLazyLoading, type: .debug, Const.maxNumberOfLazyLoadedTabs) + Logger.tabLazyLoading.debug("Maximum allowed tabs loaded (\(Const.maxNumberOfLazyLoadedTabs), skipping") return } @@ -177,7 +178,7 @@ final class TabLazyLoader { switch (tabToLoad, numberOfTabsInProgress.value) { case (.none, 0): - os_log("No more tabs suitable for lazy loading", log: .tabLazyLoading, type: .debug) + Logger.tabLazyLoading.debug("No more tabs suitable for lazy loading") lazyLoadingDidFinishSubject.send(true) case (.none, _): break @@ -203,7 +204,7 @@ final class TabLazyLoader { if tab != nil { if !dryRun { - os_log("Will reload recently selected pinned tab", log: .tabLazyLoading, type: .debug) + Logger.tabLazyLoading.debug("Will reload recently selected pinned tab") } } else { if shouldLoadAdjacentTabs, numberOfAdjacentTabsRemaining > 0 { @@ -212,16 +213,14 @@ final class TabLazyLoader { adjacentItemEnumerator?.reset() } else if tab != nil { numberOfAdjacentTabsRemaining -= 1 - os_log("Will reload adjacent tab #%d of %d", log: .tabLazyLoading, type: .debug, - Const.maxNumberOfLazyLoadedAdjacentTabs - numberOfAdjacentTabsRemaining, - Const.maxNumberOfLazyLoadedAdjacentTabs) + Logger.tabLazyLoading.debug("Will reload adjacent tab #\(Const.maxNumberOfLazyLoadedAdjacentTabs - self.numberOfAdjacentTabsRemaining) of \(Const.maxNumberOfLazyLoadedAdjacentTabs)") } } if tab == nil { tab = findRecentlySelectedTabToLoad(from: dataSource.tabs) if !dryRun, tab != nil { - os_log("Will reload recently selected tab", log: .tabLazyLoading, type: .debug) + Logger.tabLazyLoading.debug("Will reload recently selected tab") } } } @@ -249,7 +248,7 @@ final class TabLazyLoader { } private func lazyLoadTab(_ tab: DataSource.Tab) { - os_log("Reloading %s", log: .tabLazyLoading, type: .debug, String(reflecting: tab.url)) + Logger.tabLazyLoading.debug("Reloading \(String(reflecting: tab.url))") subscribeToTabLoadingFinished(tab) idsOfTabsSelectedOrReloadedInThisSession.insert(tab.id) diff --git a/DuckDuckGo/Tab/UserScripts/DebugUserScript.swift b/DuckDuckGo/Tab/UserScripts/DebugUserScript.swift index a1d8d7a628..2fa41ddda3 100644 --- a/DuckDuckGo/Tab/UserScripts/DebugUserScript.swift +++ b/DuckDuckGo/Tab/UserScripts/DebugUserScript.swift @@ -18,6 +18,7 @@ import WebKit import UserScript +import os.log protocol TabInstrumentationProtocol: AnyObject { func request(url: String, allowedIn timeInMs: Double) @@ -66,7 +67,7 @@ final class DebugUserScript: NSObject, StaticUserScript { private func handleLog(message: WKScriptMessage) { // Used to log JS debug events. This is noisy every time a new tab is opened, so it's commented out unless needed. - // os_log("%s", type: .debug, String(describing: message.body)) +// Logger.general.debug("Handle log \(String(describing: message.body))") } private func handleSignpost(message: WKScriptMessage) { diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 3649438405..051665ebde 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -24,6 +24,7 @@ import SwiftUI import WebKit import Subscription import PixelKit +import os.log final class BrowserTabViewController: NSViewController { @@ -503,7 +504,7 @@ final class BrowserTabViewController: NSViewController { let contentView = viewToMakeFirstResponderAfterAdding?() else { return } guard contentView.window === window else { - os_log("BrowserTabViewController: Content view window is \(contentView.window?.description ?? "") but expected: \(window)", type: .error) + Logger.general.error("BrowserTabViewController: Content view window is \(contentView.window?.description ?? "") but expected: \(window)") return } viewToMakeFirstResponderAfterAdding = nil @@ -1201,7 +1202,7 @@ extension BrowserTabViewController { dispatchPrecondition(condition: .onQueue(.main)) guard let webView = webView else { - os_log("BrowserTabViewController: failed to create a snapshot of webView", type: .error) + Logger.general.error("BrowserTabViewController: failed to create a snapshot of webView") return } @@ -1210,7 +1211,7 @@ extension BrowserTabViewController { webView.takeSnapshot(with: config) { [weak self] image, _ in guard let image = image else { - os_log("BrowserTabViewController: failed to create a snapshot of webView", type: .error) + Logger.general.error("BrowserTabViewController: failed to create a snapshot of webView") return } self?.showWebViewSnapshot(with: image) diff --git a/DuckDuckGo/TabBar/View/TabBarCollectionView.swift b/DuckDuckGo/TabBar/View/TabBarCollectionView.swift index 6c24959c26..df3222decc 100644 --- a/DuckDuckGo/TabBar/View/TabBarCollectionView.swift +++ b/DuckDuckGo/TabBar/View/TabBarCollectionView.swift @@ -18,6 +18,7 @@ import Cocoa import Common +import os.log final class TabBarCollectionView: NSCollectionView { @@ -53,7 +54,7 @@ final class TabBarCollectionView: NSCollectionView { func scrollToSelected() { guard selectionIndexPaths.count == 1, let indexPath = selectionIndexPaths.first else { - os_log("TabBarCollectionView: More than 1 item or no item highlighted", type: .error) + Logger.general.error("TabBarCollectionView: More than 1 item or no item highlighted") return } scroll(to: indexPath) @@ -104,7 +105,7 @@ extension NSCollectionView { var isAtEndScrollPosition: Bool { guard let clipView = clipView else { - os_log("TabBarCollectionView: Clip view is nil", type: .error) + Logger.general.error("TabBarCollectionView: Clip view is nil") return false } @@ -113,7 +114,7 @@ extension NSCollectionView { var isAtStartScrollPosition: Bool { guard let clipView = clipView else { - os_log("TabBarCollectionView: Clip view is nil", type: .error) + Logger.general.error("TabBarCollectionView: Clip view is nil") return false } diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 58021902f5..89abfcf544 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -22,6 +22,7 @@ import Common import Lottie import SwiftUI import WebKit +import os.log final class TabBarViewController: NSViewController { @@ -312,13 +313,13 @@ final class TabBarViewController: NSViewController { duplicateTab(at: .pinned(index)) case let .bookmark(tab): guard let url = tab.url, let tabViewModel = tabCollectionViewModel.pinnedTabsManager?.tabViewModels[tab] else { - os_log("TabBarViewController: Failed to get url from tab") + Logger.general.debug("TabBarViewController: Failed to get url from tab") return } bookmarkTab(with: url, title: tabViewModel.title) case let .removeBookmark(tab): guard let url = tab.url else { - os_log("TabBarViewController: Failed to get url from tab") + Logger.general.debug("TabBarViewController: Failed to get url from tab") return } deleteBookmark(with: url) @@ -342,7 +343,7 @@ final class TabBarViewController: NSViewController { } guard let selectionIndex = tabCollectionViewModel.selectionIndex else { - os_log("TabBarViewController: Selection index is nil", type: .error) + Logger.general.error("TabBarViewController: Selection index is nil") return } @@ -581,7 +582,7 @@ final class TabBarViewController: NSViewController { let tabViewModel = tabCollectionViewModel.tabViewModel(at: indexPath.item), let clipView = collectionView.clipView else { - os_log("TabBarViewController: Showing tab preview window failed", type: .error) + Logger.general.error("TabBarViewController: Showing tab preview window failed") return } @@ -591,7 +592,7 @@ final class TabBarViewController: NSViewController { private func showPinnedTabPreview(at index: Int) { guard let tabViewModel = tabCollectionViewModel.pinnedTabsManager?.tabViewModel(at: index) else { - os_log("TabBarViewController: Showing pinned tab preview window failed", type: .error) + Logger.general.error("TabBarViewController: Showing pinned tab preview window failed") return } @@ -607,7 +608,7 @@ final class TabBarViewController: NSViewController { isSelected: isSelected) guard let window = view.window else { - os_log("TabBarViewController: Showing tab preview window failed", type: .error) + Logger.general.error("TabBarViewController: Showing tab preview window failed") return } @@ -788,7 +789,7 @@ extension TabBarViewController: TabCollectionViewModelDelegate { private func deleteBookmark(with url: URL) { guard let bookmark = bookmarkManager.getBookmark(for: url) else { - os_log("TabBarViewController: Failed to fetch bookmark for url \(url)", type: .error) + Logger.general.error("TabBarViewController: Failed to fetch bookmark for url \(url)") return } bookmarkManager.remove(bookmark: bookmark) @@ -796,7 +797,7 @@ extension TabBarViewController: TabCollectionViewModelDelegate { private func fireproof(_ tab: Tab) { guard let url = tab.url, let host = url.host else { - os_log("TabBarViewController: Failed to get url of tab bar view item", type: .error) + Logger.general.error("TabBarViewController: Failed to get url of tab bar view item") return } @@ -805,7 +806,7 @@ extension TabBarViewController: TabCollectionViewModelDelegate { private func removeFireproofing(from tab: Tab) { guard let host = tab.url?.host else { - os_log("TabBarViewController: Failed to get url of tab bar view item", type: .error) + Logger.general.error("TabBarViewController: Failed to get url of tab bar view item") return } @@ -820,7 +821,7 @@ extension TabBarViewController: TabCollectionViewModelDelegate { let tabViewModel = tabCollectionViewModel.tabViewModel(at: indexPath.item), let url = tabViewModel.tab.content.userEditableUrl else { - os_log("TabBarViewController: Failed to get index path of tab bar view item", type: .error) + Logger.general.error("TabBarViewController: Failed to get index path of tab bar view item") return nil } diff --git a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift index 68dc3ee7a4..59090bb527 100644 --- a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift +++ b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift @@ -21,6 +21,7 @@ import Foundation import Combine import History import PixelKit +import os.log /** * The delegate callbacks are triggered for events related to unpinned tabs only. @@ -154,7 +155,7 @@ final class TabCollectionViewModel: NSObject { func setUpLazyLoadingIfNeeded() { guard !isTabLazyLoadingRequested else { - os_log("Lazy loading already requested in this session, skipping.", log: .tabLazyLoading, type: .debug) + Logger.tabLazyLoading.debug("Lazy loading already requested in this session, skipping.") return } @@ -164,7 +165,7 @@ final class TabCollectionViewModel: NSObject { tabLazyLoader?.lazyLoadingDidFinishPublisher .sink { [weak self] _ in self?.tabLazyLoader = nil - os_log("Disposed of Tab Lazy Loader", log: .tabLazyLoading, type: .debug) + Logger.tabLazyLoading.debug("Disposed of Tab Lazy Loader") } .store(in: &cancellables) @@ -229,7 +230,7 @@ final class TabCollectionViewModel: NSObject { func selectNext() { guard changesEnabled else { return } guard allTabsCount > 0 else { - os_log("TabCollectionViewModel: No tabs for selection", type: .error) + Logger.tabLazyLoading.error("TabCollectionViewModel: No tabs for selection") return } @@ -243,7 +244,7 @@ final class TabCollectionViewModel: NSObject { func selectPrevious() { guard changesEnabled else { return } guard allTabsCount > 0 else { - os_log("TabCollectionViewModel: No tabs for selection", type: .error) + Logger.tabLazyLoading.debug("TabCollectionViewModel: No tabs for selection") return } @@ -257,7 +258,7 @@ final class TabCollectionViewModel: NSObject { @discardableResult private func selectUnpinnedTab(at index: Int, forceChange: Bool = false) -> Bool { guard changesEnabled || forceChange else { return false } guard index >= 0, index < tabCollection.tabs.count else { - os_log("TabCollectionViewModel: Index out of bounds", type: .error) + Logger.tabLazyLoading.error("TabCollectionViewModel: Index out of bounds") selectionIndex = nil return false } @@ -272,7 +273,7 @@ final class TabCollectionViewModel: NSObject { guard let pinnedTabsCollection = pinnedTabsCollection else { return false } guard index >= 0, index < pinnedTabsCollection.tabs.count else { - os_log("TabCollectionViewModel: Index out of bounds", type: .error) + Logger.tabLazyLoading.error("TabCollectionViewModel: Index out of bounds") selectionIndex = nil return false } @@ -330,7 +331,7 @@ final class TabCollectionViewModel: NSObject { func insert(_ tab: Tab, at index: TabIndex, selected: Bool = true) { guard changesEnabled else { return } guard let tabCollection = tabCollection(for: index) else { - os_log("TabCollectionViewModel: Tab collection for index %s not found", type: .error, String(describing: index)) + Logger.tabLazyLoading.error("TabCollectionViewModel: Tab collection for index \(String(describing: index)) not found") return } @@ -351,7 +352,7 @@ final class TabCollectionViewModel: NSObject { guard changesEnabled else { return } guard let parentTab = parentTab ?? tab.parentTab, let parentTabIndex = indexInAllTabs(of: parentTab) else { - os_log("TabCollection: No parent tab", type: .error) + Logger.tabLazyLoading.error("TabCollection: No parent tab") return } @@ -450,7 +451,7 @@ final class TabCollectionViewModel: NSObject { } guard let selectionIndex = selectionIndex else { - os_log("TabCollection: No tab selected", type: .error) + Logger.tabLazyLoading.error("TabCollection: No tab selected") notifyDelegate() return } @@ -547,7 +548,7 @@ final class TabCollectionViewModel: NSObject { func removeTabsAndAppendNew(at indexSet: IndexSet, forceChange: Bool = false) { guard !indexSet.isEmpty, changesEnabled || forceChange else { return } guard let selectionIndex = selectionIndex?.item else { - os_log("TabCollection: No tab selected", type: .error) + Logger.tabLazyLoading.error("TabCollection: No tab selected") return } @@ -573,7 +574,7 @@ final class TabCollectionViewModel: NSObject { guard changesEnabled || forceChange else { return } guard let selectionIndex = selectionIndex else { - os_log("TabCollectionViewModel: No tab selected", type: .error) + Logger.tabLazyLoading.error("TabCollectionViewModel: No tab selected") return } @@ -586,7 +587,7 @@ final class TabCollectionViewModel: NSObject { guard changesEnabled else { return } guard let tab = tab(at: tabIndex) else { - os_log("TabCollectionViewModel: Index out of bounds", type: .error) + Logger.tabLazyLoading.error("TabCollectionViewModel: Index out of bounds") return } @@ -606,7 +607,7 @@ final class TabCollectionViewModel: NSObject { guard let pinnedTabsCollection = pinnedTabsCollection else { return } guard index >= 0, index < tabCollection.tabs.count else { - os_log("TabCollectionViewModel: Index out of bounds", type: .error) + Logger.tabLazyLoading.error("TabCollectionViewModel: Index out of bounds") return } @@ -625,7 +626,7 @@ final class TabCollectionViewModel: NSObject { } guard let tab = pinnedTabsManager?.unpinTab(at: index, published: false) else { - os_log("Unable to unpin a tab", type: .error) + Logger.tabLazyLoading.error("Unable to unpin a tab") return } @@ -658,14 +659,14 @@ final class TabCollectionViewModel: NSObject { func replaceTab(at index: TabIndex, with tab: Tab, forceChange: Bool = false) { guard changesEnabled || forceChange else { return } guard let tabCollection = tabCollection(for: index) else { - os_log("TabCollectionViewModel: Tab collection for index %s not found", type: .error, String(describing: index)) + Logger.tabLazyLoading.error("TabCollectionViewModel: Tab collection for index \(String(describing: index)) not found") return } tabCollection.replaceTab(at: index.item, with: tab) guard let selectionIndex = selectionIndex else { - os_log("TabCollectionViewModel: No tab selected", type: .error) + Logger.tabLazyLoading.error("TabCollectionViewModel: No tab selected") return } select(at: selectionIndex, forceChange: forceChange) diff --git a/DuckDuckGo/TabPreview/Model/ViewSnapshotRenderer.swift b/DuckDuckGo/TabPreview/Model/ViewSnapshotRenderer.swift index d11cf0d4fc..33a76cf894 100644 --- a/DuckDuckGo/TabPreview/Model/ViewSnapshotRenderer.swift +++ b/DuckDuckGo/TabPreview/Model/ViewSnapshotRenderer.swift @@ -19,6 +19,7 @@ import Foundation import Common import WebKit +import os.log protocol ViewSnapshotRendering { @@ -40,11 +41,11 @@ final class ViewSnapshotRenderer: ViewSnapshotRendering { func renderSnapshot(view: NSView, completion: @escaping (NSImage?) -> Void) { let originalBounds = view.bounds - os_log("Native snapshot rendering started", log: .tabSnapshots) + Logger.tabSnapshots.debug("Native snapshot rendering started") DispatchQueue.global(qos: .userInitiated).async { guard let resizedImage = self.createResizedImage(from: view, with: originalBounds) else { DispatchQueue.main.async { - os_log("Native snapshot rendering failed", log: .tabSnapshots, type: .error) + Logger.tabSnapshots.error("Native snapshot rendering failed") completion(nil) } return @@ -53,7 +54,7 @@ final class ViewSnapshotRenderer: ViewSnapshotRendering { DispatchQueue.main.async { completion(resizedImage) - os_log("Snapshot of native page rendered", log: .tabSnapshots) + Logger.tabSnapshots.debug("Snapshot of native page rendered") } } } diff --git a/DuckDuckGo/TabPreview/Model/WebViewSnapshotRenderer.swift b/DuckDuckGo/TabPreview/Model/WebViewSnapshotRenderer.swift index 667a79ae03..d7bb9e33a4 100644 --- a/DuckDuckGo/TabPreview/Model/WebViewSnapshotRenderer.swift +++ b/DuckDuckGo/TabPreview/Model/WebViewSnapshotRenderer.swift @@ -19,6 +19,7 @@ import Foundation import Common import WebKit +import os.log protocol WebViewSnapshotRendering { @@ -32,17 +33,17 @@ final class WebViewSnapshotRenderer: WebViewSnapshotRendering { @MainActor func renderSnapshot(webView: WKWebView) async -> NSImage? { dispatchPrecondition(condition: .onQueue(.main)) - os_log("Preview rendering started for \(String(describing: webView.url))", log: .tabSnapshots) + Logger.tabSnapshots.debug("Preview rendering started for \(String(describing: webView.url))") let configuration = WKSnapshotConfiguration.makePreviewSnapshotConfiguration() do { let image = try await webView.takeSnapshot(configuration: configuration) - os_log("Preview rendered for \(String(describing: webView.url))", log: .tabSnapshots) + Logger.tabSnapshots.debug("Preview rendered for \(String(describing: webView.url))") return image } catch { - os_log("Failed to render snapshot for \(String(describing: webView.url))", log: .tabSnapshots, type: .error) + Logger.tabSnapshots.error("Failed to render snapshot for \(String(describing: webView.url))") return nil } } diff --git a/DuckDuckGo/TabPreview/Services/TabSnapshotStore.swift b/DuckDuckGo/TabPreview/Services/TabSnapshotStore.swift index 1b233bf766..accea10851 100644 --- a/DuckDuckGo/TabPreview/Services/TabSnapshotStore.swift +++ b/DuckDuckGo/TabPreview/Services/TabSnapshotStore.swift @@ -18,6 +18,7 @@ import Cocoa import Common +import os.log protocol TabSnapshotStoring { @@ -40,7 +41,7 @@ final class TabSnapshotStore: TabSnapshotStoring { func persistSnapshot(_ snapshot: NSImage, id: UUID) { guard let data = snapshot.tiffRepresentation else { - os_log("TabSnapshotPersistenceService: Failed to create tiff representation", type: .error) + Logger.tabSnapshots.error("TabSnapshotPersistenceService: Failed to create tiff representation") return } @@ -48,7 +49,7 @@ final class TabSnapshotStore: TabSnapshotStoring { let url = URL.persistenceLocation(for: id) createDirectoryIfNeeded() guard fileStore.persist(data, url: url) else { - os_log("TabSnapshotPersistenceService: Saving of snapshot failed", type: .error) + Logger.tabSnapshots.error("TabSnapshotPersistenceService: Saving of snapshot failed") return } } @@ -68,7 +69,7 @@ final class TabSnapshotStore: TabSnapshotStoring { let image = NSImage(data: data) { return image as NSImageSendable } else { - os_log("TabSnapshotPersistenceService: Loading of snapshot failed", type: .error) + Logger.tabSnapshots.error("TabSnapshotPersistenceService: Loading of snapshot failed") return nil } } @@ -101,7 +102,7 @@ final class TabSnapshotStore: TabSnapshotStoring { uuids.append(uuid) } } catch { - os_log("Failed to load stored snapshot ids: %@", type: .error, error.localizedDescription) + Logger.tabSnapshots.error("Failed to load stored snapshot ids: \(error.localizedDescription, privacy: .public)") } return uuids diff --git a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift index f9fca79fcd..c292546db8 100644 --- a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift @@ -18,6 +18,7 @@ import Cocoa import Common +import os.log final class TabPreviewWindowController: NSWindowController { @@ -82,7 +83,7 @@ final class TabPreviewWindowController: NSWindowController { guard let childWindows = parentWindow.childWindows, let tabPreviewWindow = self.window else { - os_log("TabPreviewWindowController: Showing tab preview window failed", type: .error) + Logger.general.error("TabPreviewWindowController: Showing tab preview window failed") return } diff --git a/DuckDuckGo/UnprotectedDomains/LocalUnprotectedDomains.swift b/DuckDuckGo/UnprotectedDomains/LocalUnprotectedDomains.swift index 86bdc1bcf6..a9103b81ca 100644 --- a/DuckDuckGo/UnprotectedDomains/LocalUnprotectedDomains.swift +++ b/DuckDuckGo/UnprotectedDomains/LocalUnprotectedDomains.swift @@ -19,6 +19,7 @@ import Common import Foundation import CoreData import BrowserServicesKit +import os.log typealias UnprotectedDomainsStore = CoreDataStore final class LocalUnprotectedDomains: DomainsProtectionStore { @@ -74,7 +75,7 @@ final class LocalUnprotectedDomains: DomainsProtectionStore { return try store.load(into: [:]) { $0[$1.value] = $1.id } } catch { - os_log("UnprotectedDomainStore: Failed to load Unprotected Domains", type: .error) + Logger.general.error("UnprotectedDomainStore: Failed to load Unprotected Domains") return [:] } } diff --git a/DuckDuckGo/Updates/BinaryOwnershipChecker.swift b/DuckDuckGo/Updates/BinaryOwnershipChecker.swift index acfda5b14c..e3825f92ae 100644 --- a/DuckDuckGo/Updates/BinaryOwnershipChecker.swift +++ b/DuckDuckGo/Updates/BinaryOwnershipChecker.swift @@ -18,6 +18,7 @@ import Foundation import Common +import os.log protocol BinaryOwnershipChecking { func isCurrentUserOwner() -> Bool @@ -43,7 +44,7 @@ final class BinaryOwnershipChecker: BinaryOwnershipChecking { } guard let binaryPath = Bundle.main.executablePath else { - os_log("Failed to get the binary path", log: .updates) + Logger.updates.debug("Failed to get the binary path") ownershipCache = false return false } @@ -56,9 +57,7 @@ final class BinaryOwnershipChecker: BinaryOwnershipChecking { return isOwner } } catch { - os_log("Failed to get binary file attributes: %{public}@", - log: .updates, - error.localizedDescription) + Logger.updates.error("Failed to get binary file attributes: \(error.localizedDescription, privacy: .public)") } ownershipCache = false diff --git a/DuckDuckGo/Updates/UpdateController.swift b/DuckDuckGo/Updates/UpdateController.swift index ddee5dcfa8..e7748d8689 100644 --- a/DuckDuckGo/Updates/UpdateController.swift +++ b/DuckDuckGo/Updates/UpdateController.swift @@ -24,6 +24,7 @@ import BrowserServicesKit import SwiftUIExtensions import PixelKit import SwiftUI +import os.log protocol UpdateControllerProtocol: AnyObject { @@ -98,7 +99,7 @@ final class UpdateController: NSObject, UpdateControllerProtocol { @UserDefaultsWrapper(key: .automaticUpdates, defaultValue: true) var areAutomaticUpdatesEnabled: Bool { didSet { - os_log("areAutomaticUpdatesEnabled: \(areAutomaticUpdatesEnabled)", log: .updates) + Logger.updates.debug("areAutomaticUpdatesEnabled: \(self.areAutomaticUpdatesEnabled)") if updater.updater.automaticallyDownloadsUpdates != areAutomaticUpdatesEnabled { updater.updater.automaticallyDownloadsUpdates = areAutomaticUpdatesEnabled @@ -151,13 +152,13 @@ final class UpdateController: NSObject, UpdateControllerProtocol { } func checkForUpdate() { - os_log("Checking for updates", log: .updates) + Logger.updates.debug("Checking for updates") updater.updater.checkForUpdates() } func checkForUpdateInBackground() { - os_log("Checking for updates in background", log: .updates) + Logger.updates.debug("Checking for updates in background") updater.updater.checkForUpdatesInBackground() } @@ -211,7 +212,7 @@ extension UpdateController: SPUStandardUserDriverDelegate { extension UpdateController: SPUUpdaterDelegate { func updater(_ updater: SPUUpdater, mayPerform updateCheck: SPUUpdateCheck) throws { - os_log("Updater started performing the update check. (isInternalUser: \(internalUserDecider.isInternalUser)", log: .updates) + Logger.updates.debug("Updater started performing the update check. (isInternalUser: \(self.internalUserDecider.isInternalUser)") onUpdateCheckStart() } @@ -234,10 +235,7 @@ extension UpdateController: SPUUpdaterDelegate { } func updater(_ updater: SPUUpdater, didAbortWithError error: Error) { - os_log("Updater did abort with error: %{public}@", - log: .updates, - error.localizedDescription) - + Logger.updates.error("Updater did abort with error: \(error.localizedDescription)") let errorCode = (error as NSError).code guard ![Int(Sparkle.SUError.noUpdateError.rawValue), Int(Sparkle.SUError.installationCanceledError.rawValue), @@ -250,9 +248,7 @@ extension UpdateController: SPUUpdaterDelegate { } func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) { - os_log("Updater did find valid update: %{public}@", - log: .updates, - "\(item.displayVersionString)(\(item.versionString))") + Logger.updates.debug("Updater did find valid update: \(item.displayVersionString)(\(item.versionString))") PixelKit.fire(DebugEvent(GeneralPixel.updaterDidFindUpdate)) @@ -265,9 +261,7 @@ extension UpdateController: SPUUpdaterDelegate { func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: any Error) { let item = (error as NSError).userInfo["SULatestAppcastItemFound"] as? SUAppcastItem - os_log("Updater did not find update: %{public}@", - log: .updates, - "\(item?.displayVersionString ?? "")(\(item?.versionString ?? ""))") + Logger.updates.debug("Updater did not find update: \(String(describing: item?.displayVersionString))(\(String(describing: item?.versionString)))") if let item { // User is running the latest version updateCheckResult = UpdateCheckResult(item: item, isInstalled: true) @@ -277,9 +271,7 @@ extension UpdateController: SPUUpdaterDelegate { } func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) { - os_log("Updater did download update: %{public}@", - log: .updates, - "\(item.displayVersionString)(\(item.versionString))") + Logger.updates.debug("Updater did download update: \(item.displayVersionString)(\(item.versionString))") if automaticUpdateFlow { // For automatic updates, the available item has to be downloaded @@ -291,7 +283,7 @@ extension UpdateController: SPUUpdaterDelegate { } func updater(_ updater: SPUUpdater, didFinishUpdateCycleFor updateCheck: SPUUpdateCheck, error: (any Error)?) { - os_log("Updater did finish update cycle", log: .updates) + Logger.updates.debug("Updater did finish update cycle") onUpdateCheckEnd() } diff --git a/DuckDuckGo/Updates/UpdateNotificationPresenter.swift b/DuckDuckGo/Updates/UpdateNotificationPresenter.swift index e11ea6929b..5c68f0c64b 100644 --- a/DuckDuckGo/Updates/UpdateNotificationPresenter.swift +++ b/DuckDuckGo/Updates/UpdateNotificationPresenter.swift @@ -19,13 +19,14 @@ import Cocoa import SwiftUI import Common +import os.log final class UpdateNotificationPresenter { static let presentationTimeInterval: TimeInterval = 10 func showUpdateNotification(icon: NSImage, text: String, buttonText: String? = nil, presentMultiline: Bool = false) { - os_log("Notification presented: \(text)", log: .updates) + Logger.updates.debug("Notification presented: \(text)") DispatchQueue.main.async { guard let windowController = WindowControllersManager.shared.lastKeyMainWindowController ?? WindowControllersManager.shared.mainWindowControllers.last, diff --git a/DuckDuckGo/Waitlist/IPCServiceLauncher.swift b/DuckDuckGo/Waitlist/IPCServiceLauncher.swift index f898790aa6..8c6aa5d593 100644 --- a/DuckDuckGo/Waitlist/IPCServiceLauncher.swift +++ b/DuckDuckGo/Waitlist/IPCServiceLauncher.swift @@ -69,7 +69,7 @@ final class IPCServiceLauncher { runningApplication = try await appLauncher.runApp(withCommand: UDSLaunchAppCommand()) case .loginItem(let loginItem, let loginItemsManager): - try loginItemsManager.throwingEnableLoginItems([loginItem], log: .disabled) + try loginItemsManager.throwingEnableLoginItems([loginItem]) } } diff --git a/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift b/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift index 64540b2924..1569d68f97 100644 --- a/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift +++ b/DuckDuckGo/Waitlist/VPNFeatureGatekeeper.swift @@ -47,7 +47,6 @@ struct DefaultVPNFeatureGatekeeper: VPNFeatureGatekeeper { init(networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), vpnUninstaller: VPNUninstalling = VPNUninstaller(), defaults: UserDefaults = .netP, - log: OSLog = .networkProtection, subscriptionManager: SubscriptionManager) { self.networkProtectionFeatureActivation = networkProtectionFeatureActivation diff --git a/DuckDuckGo/Waitlist/VPNUninstaller.swift b/DuckDuckGo/Waitlist/VPNUninstaller.swift index b8afa97d09..d164cf21b3 100644 --- a/DuckDuckGo/Waitlist/VPNUninstaller.swift +++ b/DuckDuckGo/Waitlist/VPNUninstaller.swift @@ -124,7 +124,6 @@ final class VPNUninstaller: VPNUninstalling { } } - private let log: OSLog private let ipcServiceLauncher: IPCServiceLauncher private let loginItemsManager: LoginItemsManaging private let pinningManager: LocalPinningManager @@ -144,16 +143,13 @@ final class VPNUninstaller: VPNUninstalling { settings: VPNSettings = .init(defaults: .netP), ipcClient: VPNControllerIPCClient = VPNControllerUDSClient(), vpnMenuLoginItem: LoginItem = .vpnMenu, - pixelKit: PixelFiring? = PixelKit.shared, - log: OSLog = .networkProtection) { + pixelKit: PixelFiring? = PixelKit.shared) { let vpnAgentBundleID = Bundle.main.vpnMenuAgentBundleId let appLauncher = AppLauncher(appBundleURL: Bundle.main.vpnMenuAgentURL) let ipcServiceLaunchMethod = IPCServiceLauncher.LaunchMethod.direct( bundleID: vpnAgentBundleID, appLauncher: appLauncher) - - self.log = log self.ipcServiceLauncher = ipcServiceLauncher ?? IPCServiceLauncher(launchMethod: ipcServiceLaunchMethod) self.loginItemsManager = loginItemsManager self.pinningManager = pinningManager diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index 4e9f8548b6..1f54a4936e 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -19,6 +19,7 @@ import Cocoa import Combine import Common +import os.log import BrowserServicesKit @MainActor @@ -95,7 +96,7 @@ final class WindowControllersManager: WindowControllersManagerProtocol { func unregister(_ windowController: MainWindowController) { guard let idx = mainWindowControllers.firstIndex(of: windowController) else { - os_log("WindowControllersManager: Window Controller not registered", type: .error) + Logger.general.error("WindowControllersManager: Window Controller not registered") return } mainWindowControllers.remove(at: idx) diff --git a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift index 96fc3c0878..df118fecf3 100644 --- a/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift +++ b/DuckDuckGoDBPBackgroundAgent/DuckDuckGoDBPBackgroundAgentAppDelegate.swift @@ -25,6 +25,7 @@ import BrowserServicesKit import PixelKit import Networking import Subscription +import os.log @objc(Application) final class DuckDuckGoDBPBackgroundAgentApplication: NSApplication { @@ -32,7 +33,7 @@ final class DuckDuckGoDBPBackgroundAgentApplication: NSApplication { private let subscriptionManager: SubscriptionManager override init() { - os_log(.error, log: .dbpBackgroundAgent, "🟢 DBP background Agent starting: %{public}d", NSRunningApplication.current.processIdentifier) + Logger.dbpBackgroundAgent.info("🟢 Starting: \(NSRunningApplication.current.processIdentifier, privacy: .public)") let dryRun: Bool #if DEBUG @@ -62,7 +63,7 @@ final class DuckDuckGoDBPBackgroundAgentApplication: NSApplication { // prevent agent from running twice if let anotherInstance = NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.bundleIdentifier!).first(where: { $0 != .current }) { - os_log(.error, log: .dbpBackgroundAgent, "🔴 Stopping: another instance is running: %{public}d.", anotherInstance.processIdentifier) + Logger.dbpBackgroundAgent.error("Stopping: another instance is running: \(anotherInstance.processIdentifier, privacy: .public).") pixelHandler.fire(.backgroundAgentStartedStoppingDueToAnotherInstanceRunning) exit(0) } @@ -96,7 +97,7 @@ final class DuckDuckGoDBPBackgroundAgentAppDelegate: NSObject, NSApplicationDele @MainActor func applicationDidFinishLaunching(_ aNotification: Notification) { - os_log("DuckDuckGoAgent started", log: .dbpBackgroundAgent, type: .info) + Logger.dbpBackgroundAgent.info("DuckDuckGoAgent started") let redeemUseCase = RedeemUseCase(authenticationService: AuthenticationService(), authenticationRepository: KeychainAuthenticationData()) diff --git a/DuckDuckGoDBPBackgroundAgent/Logger+DBPBackgroundAgent.swift b/DuckDuckGoDBPBackgroundAgent/Logger+DBPBackgroundAgent.swift new file mode 100644 index 0000000000..a3dc66ebdd --- /dev/null +++ b/DuckDuckGoDBPBackgroundAgent/Logger+DBPBackgroundAgent.swift @@ -0,0 +1,24 @@ +// +// Logger+DBPBackgroundAgent.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log + +extension Logger { + static var dbpBackgroundAgent = { Logger(subsystem: "DBP Background Agent", category: "") }() +} diff --git a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift index 236b5dad27..1263fc422f 100644 --- a/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift +++ b/DuckDuckGoNotifications/DuckDuckGoNotificationsAppDelegate.swift @@ -23,17 +23,18 @@ import Common import NetworkExtension import NetworkProtection import VPNAppLauncher +import os.log @objc(Application) final class DuckDuckGoNotificationsApplication: NSApplication { private let _delegate = DuckDuckGoNotificationsAppDelegate() override init() { - os_log(.error, log: .networkProtection, "🟢 Notifications Agent starting: %{public}d", ProcessInfo.processInfo.processIdentifier) + Logger.networkProtection.error("🟢 Notifications Agent starting: \(ProcessInfo.processInfo.processIdentifier, privacy: .public)") // prevent agent from running twice if let anotherInstance = NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.bundleIdentifier!).first(where: { $0 != .current }) { - os_log(.error, log: .networkProtection, "🔴 Stopping: another instance is running: %{public}d.", anotherInstance.processIdentifier) + Logger.networkProtection.error("Stopping: another instance is running: \(anotherInstance.processIdentifier, privacy: .public).") exit(EXIT_SUCCESS) } @@ -69,14 +70,14 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate private var cancellables = Set() func applicationDidFinishLaunching(_ aNotification: Notification) { - os_log("Login item finished launching", log: .networkProtectionLoginItemLog, type: .info) + Logger.networkProtection.info("Login item finished launching") startObservingVPNStatusChanges() - os_log("Login item listening") + Logger.networkProtection.info("Login item listening") } private func startObservingVPNStatusChanges() { - os_log("Register with sysex") + Logger.networkProtection.info("Register with sysex") distributedNotificationCenter.publisher(for: .showIssuesStartedNotification) .receive(on: DispatchQueue.main) @@ -110,7 +111,7 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate }.store(in: &cancellables) distributedNotificationCenter.publisher(for: .serverSelected).sink { [weak self] _ in - os_log("Got notification: listener started") + Logger.networkProtection.info("Got notification: listener started") self?.notificationsPresenter.requestAuthorization() }.store(in: &cancellables) @@ -128,33 +129,33 @@ final class DuckDuckGoNotificationsAppDelegate: NSObject, NSApplicationDelegate // MARK: - Showing Notifications func showConnectedNotification(serverLocation: String?) { - os_log("Presenting reconnected notification", log: .networkProtection, type: .info) + Logger.networkProtection.info("Presenting reconnected notification") notificationsPresenter.showConnectedNotification(serverLocation: serverLocation, snoozeEnded: false) } func showReconnectingNotification() { - os_log("Presenting reconnecting notification", log: .networkProtection, type: .info) + Logger.networkProtection.info("Presenting reconnecting notification") notificationsPresenter.showReconnectingNotification() } func showConnectionFailureNotification() { - os_log("Presenting failure notification", log: .networkProtection, type: .info) + Logger.networkProtection.info("Presenting failure notification") notificationsPresenter.showConnectionFailureNotification() } func showSupersededNotification() { - os_log("Presenting Superseded notification", log: .networkProtection, type: .info) + Logger.networkProtection.info("Presenting Superseded notification") notificationsPresenter.showSupersededNotification() } func showEntitlementNotification() { - os_log("Presenting Entitlements notification", log: .networkProtection, type: .info) + Logger.networkProtection.info("Presenting Entitlements notification") notificationsPresenter.showEntitlementNotification() } func showTestNotification() { - os_log("Presenting test notification", log: .networkProtection, type: .info) + Logger.networkProtection.info("Presenting test notification") notificationsPresenter.showTestNotification() } diff --git a/DuckDuckGoNotifications/Logging.swift b/DuckDuckGoNotifications/Logging.swift deleted file mode 100644 index 30c5d6fbe5..0000000000 --- a/DuckDuckGoNotifications/Logging.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Logging.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 os.log - -extension OSLog { - - public static var networkProtectionLoginItemLog: OSLog { - Logging.networkProtectionLoginItemLoggingEnabled ? Logging.networkProtectionLoginItemLog : .disabled - } - - public static var networkProtectionIPCLoginItemLog: OSLog { - Logging.networkProtectionIPCLoginItemLoggingEnabled ? Logging.networkProtectionIPCLoginItemLog : .disabled - } - - public static var networkProtectionMemoryLog: OSLog { - Logging.networkProtectionMemoryLoggingEnabled ? Logging.networkProtectionMemoryLog : .disabled - } -} - -struct Logging { - - static let subsystem = Bundle.main.bundleIdentifier ?? "DuckDuckGo" - - fileprivate static let networkProtectionLoginItemLoggingEnabled = false - fileprivate static let networkProtectionLoginItemLog: OSLog = OSLog(subsystem: subsystem, category: "VPN: Login Item") - - fileprivate static let networkProtectionIPCLoginItemLoggingEnabled = false - fileprivate static let networkProtectionIPCLoginItemLog: OSLog = OSLog(subsystem: subsystem, category: "VPN: IPC (Login Item)") - - fileprivate static let networkProtectionMemoryLoggingEnabled = false - fileprivate static let networkProtectionMemoryLog: OSLog = OSLog(subsystem: subsystem, category: "VPN: Memory") -} diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index 1951ee6650..f2c6e25861 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -30,6 +30,7 @@ import ServiceManagement import PixelKit import Subscription import VPNAppLauncher +import os.log @objc(Application) final class DuckDuckGoVPNApplication: NSApplication { @@ -38,16 +39,11 @@ final class DuckDuckGoVPNApplication: NSApplication { private let _delegate: DuckDuckGoVPNAppDelegate override init() { - os_log(.default, - log: .networkProtection, - "🟢 Status Bar Agent starting\nPath: (%{public}@)\nVersion: %{public}@\nPID: %{public}d", - Bundle.main.bundlePath, - "\(Bundle.main.versionNumber!).\(Bundle.main.buildNumber)", - NSRunningApplication.current.processIdentifier) + Logger.networkProtection.debug("🟢 Status Bar Agent starting\nPath: (\(Bundle.main.bundlePath, privacy: .public))\nVersion: \("\(Bundle.main.versionNumber!).\(Bundle.main.buildNumber)", privacy: .public)\nPID: \(NSRunningApplication.current.processIdentifier, privacy: .public)") // prevent agent from running twice if let anotherInstance = NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.bundleIdentifier!).first(where: { $0 != .current }) { - os_log(.error, log: .networkProtection, "🔴 Stopping: another instance is running: %{public}d.", anotherInstance.processIdentifier) + Logger.networkProtection.error("Stopping: another instance is running: \(anotherInstance.processIdentifier, privacy: .public).") exit(0) } @@ -76,9 +72,9 @@ final class DuckDuckGoVPNApplication: NSApplication { #if DEBUG if accountManager.accessToken != nil { - os_log(.error, log: .networkProtection, "🟢 VPN Agent found token") + Logger.networkProtection.debug("🟢 VPN Agent found token") } else { - os_log(.error, log: .networkProtection, "🔴 VPN Agent found no token") + Logger.networkProtection.error("VPN Agent found no token") } #endif } @@ -361,7 +357,7 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { APIRequest.Headers.setUserAgent(UserAgent.duckDuckGoUserAgent()) - os_log("DuckDuckGoVPN started", log: .networkProtectionLoginItemLog) + Logger.networkProtection.info("DuckDuckGoVPN started") setupMenuVisibility() diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift index c95c4fc0b1..0be0213a78 100644 --- a/DuckDuckGoVPN/TunnelControllerIPCService.swift +++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift @@ -89,7 +89,7 @@ final class TunnelControllerIPCService { self.defaults = defaults self.pixelKit = pixelKit - udsServer = UDSServer(socketFileURL: VPNIPCResources.socketFileURL, log: .networkProtectionIPCLog) + udsServer = UDSServer(socketFileURL: VPNIPCResources.socketFileURL) subscribeToErrorChanges() subscribeToStatusUpdates() diff --git a/DuckDuckGoVPN/VPNAppEventsHandler.swift b/DuckDuckGoVPN/VPNAppEventsHandler.swift index 5ac88e4a2b..612b10ccb0 100644 --- a/DuckDuckGoVPN/VPNAppEventsHandler.swift +++ b/DuckDuckGoVPN/VPNAppEventsHandler.swift @@ -19,6 +19,7 @@ import Foundation import Common import NetworkProtection +import os.log final class VPNAppEventsHandler { @@ -36,7 +37,7 @@ final class VPNAppEventsHandler { } let restartTunnel = { - os_log(.info, log: .networkProtection, "App updated from %{public}s to %{public}s: updating login items", versionStore.lastAgentVersionRun ?? "null", currentVersion) + Logger.networking.info("App updated from \(versionStore.lastAgentVersionRun ?? "null", privacy: .public) to \(currentVersion, privacy: .public): updating login items") self.restartTunnel() } diff --git a/IntegrationTests/AutoconsentIntegrationTests.swift b/IntegrationTests/AutoconsentIntegrationTests.swift index f0720c46ef..ce5be346e4 100644 --- a/IntegrationTests/AutoconsentIntegrationTests.swift +++ b/IntegrationTests/AutoconsentIntegrationTests.swift @@ -20,6 +20,7 @@ import Combine import Common import PrivacyDashboard import XCTest +import os.log @testable import DuckDuckGo_Privacy_Browser @@ -150,11 +151,11 @@ class AutoconsentIntegrationTests: XCTestCase { .first() .promise() - os_log("starting navigation to http://privacy-test-pages.site/features/autoconsent/banner.html") + Logger.general.debug("starting navigation to http://privacy-test-pages.site/features/autoconsent/banner.html") let navigation = tab.setUrl(url, source: .link) navigation?.appendResponder(navigationResponse: { response in - os_log("navigationResponse: %s", "\(String(describing: response))") + Logger.general.debug("navigationResponse: \(String(describing: response))") // cause UserScripts reload (ContentBlockingUpdating) WebTrackingProtectionPreferences.shared.isGPCEnabled = true @@ -164,7 +165,7 @@ class AutoconsentIntegrationTests: XCTestCase { }) _=await navigation?.result - os_log("navigation done") + Logger.general.debug("navigation done") let cookieConsentManaged = try await cookieConsentManagedPromise.value XCTAssertTrue(cookieConsentManaged == true) diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 3c05ff905c..42f9b92915 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: "187.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "188.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift index be97096c4e..4e5732d38c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionFeature.swift @@ -20,7 +20,7 @@ import Foundation import WebKit import BrowserServicesKit import UserScript -import Common +import os.log protocol CCFCommunicationDelegate: AnyObject { func loadURL(url: URL) async @@ -60,20 +60,20 @@ struct DataBrokerProtectionFeature: Subfeature { case .actionError: return onActionError } } else { - os_log("Cant parse method: %{public}@", log: .action, methodName) + Logger.action.debug("Cant parse method: \(methodName, privacy: .public)") return nil } } func onActionCompleted(params: Any, original: WKScriptMessage) async throws -> Encodable? { - os_log("Action completed", log: .action) + Logger.action.debug("Action completed") await parseActionCompleted(params: params) return nil } func parseActionCompleted(params: Any) async { - os_log("Parse action completed", log: .action) + Logger.action.debug("Parse action completed") guard let data = try? JSONSerialization.data(withJSONObject: params), let result = try? JSONDecoder().decode(CCFResult.self, from: data) else { @@ -91,7 +91,7 @@ struct DataBrokerProtectionFeature: Subfeature { } func parseSuccess(success: CCFSuccessResponse) async { - os_log("Parse success: %{public}@", log: .action, String(describing: success.actionType.rawValue)) + Logger.action.debug("Parse success: \(String(describing: success.actionType.rawValue), privacy: .public)") switch success.response { case .navigate(let navigate): @@ -114,7 +114,7 @@ struct DataBrokerProtectionFeature: Subfeature { func onActionError(params: Any, original: WKScriptMessage) async throws -> Encodable? { let error = DataBrokerProtectionError.parse(params: params) - os_log("Action Error: %{public}@", log: .action, String(describing: error.localizedDescription)) + Logger.action.debug("Action Error: \(String(describing: error.localizedDescription), privacy: .public)") await delegate?.onError(error: error) return nil @@ -129,7 +129,7 @@ struct DataBrokerProtectionFeature: Subfeature { assertionFailure("Cannot continue without broker instance") return } - os_log("Pushing into WebView: %@ params %@", log: .action, method.rawValue, String(describing: params)) + Logger.action.debug("Pushing into WebView: \(method.rawValue) params \(String(describing: params))") broker.push(method: method.rawValue, params: params, for: self, into: webView) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionUtils.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionUtils.swift index 245020941c..c67450cedb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionUtils.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/DataBrokerProtectionUtils.swift @@ -19,7 +19,7 @@ import Foundation import WebKit import BrowserServicesKit import UserScript -import Common +import os.log import Combine final class DataBrokerUserContentController: WKUserContentController { @@ -51,7 +51,7 @@ final class DataBrokerUserContentController: WKUserContentController { @MainActor public func cleanUpBeforeClosing() { - os_log("Cleaning up DBP user scripts", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Cleaning up DBP user scripts") self.removeAllUserScripts() self.removeAllScriptMessageHandlers() @@ -61,7 +61,7 @@ final class DataBrokerUserContentController: WKUserContentController { } deinit { - os_log("DataBrokerUserContentController Deinit", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("DataBrokerUserContentController Deinit") } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift index 26be41ebee..ebb3537640 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/CCF/WebViewHandler.swift @@ -21,6 +21,7 @@ import WebKit import BrowserServicesKit import UserScript import Common +import os.log protocol WebViewHandler: NSObject { func initializeWebView(showWebView: Bool) async @@ -82,7 +83,7 @@ final class DataBrokerProtectionWebViewHandler: NSObject, WebViewHandler { func load(url: URL) async throws { webView?.load(url) - os_log("Loading URL: %@", log: .action, String(describing: url.absoluteString)) + Logger.action.debug("Loading URL: \(String(describing: url.absoluteString))") try await waitForWebViewLoad() } @@ -93,11 +94,11 @@ final class DataBrokerProtectionWebViewHandler: NSObject, WebViewHandler { } func finish() { - os_log("WebViewHandler finished", log: .action) + Logger.action.debug("WebViewHandler finished") webView?.stopLoading() userContentController?.cleanUpBeforeClosing() WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0)) { - os_log("WKWebView data store deleted correctly", log: .action) + Logger.action.debug("WKWebView data store deleted correctly") } webViewConfiguration = nil @@ -107,7 +108,7 @@ final class DataBrokerProtectionWebViewHandler: NSObject, WebViewHandler { } deinit { - os_log("WebViewHandler Deinit", log: .action) + Logger.action.debug("WebViewHandler Deinit") } func waitForWebViewLoad() async throws { @@ -117,7 +118,7 @@ final class DataBrokerProtectionWebViewHandler: NSObject, WebViewHandler { } func execute(action: Action, data: CCFRequestData) { - os_log("Executing action: %{public}@", log: .action, String(describing: action.actionType.rawValue)) + Logger.action.debug("Executing action: \(String(describing: action.actionType.rawValue), privacy: .public)") userContentController?.dataBrokerUserScripts?.dataBrokerFeature.pushAction( method: .onActionReceived, @@ -154,7 +155,7 @@ final class DataBrokerProtectionWebViewHandler: NSObject, WebViewHandler { try htmlString.write(to: fileURL, atomically: true, encoding: .utf8) print("HTML content saved to file: \(fileURL)") } catch { - os_log(.error, "Error writing HTML content to file: \(error)") + Logger.action.error("Error writing HTML content to file: \(error)") } } @@ -200,20 +201,20 @@ extension DataBrokerProtectionWebViewHandler: WKNavigationDelegate { } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - os_log("WebViewHandler didFinish", log: .action) + Logger.action.debug("WebViewHandler didFinish") self.activeContinuation?.resume() self.activeContinuation = nil } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - os_log("WebViewHandler didFail: %{public}@", log: .action, String(describing: error.localizedDescription)) + Logger.action.error("WebViewHandler didFail: \(error.localizedDescription, privacy: .public)") self.activeContinuation?.resume(throwing: error) self.activeContinuation = nil } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - os_log("WebViewHandler didFailProvisionalNavigation: %{public}@", log: .action, String(describing: error.localizedDescription)) + Logger.action.error("WebViewHandler didFailProvisionalNavigation: \(error.localizedDescription, privacy: .public)") self.activeContinuation?.resume(throwing: error) self.activeContinuation = nil } @@ -225,7 +226,7 @@ extension DataBrokerProtectionWebViewHandler: WKNavigationDelegate { } if statusCode >= 400 { - os_log("WebViewHandler failed with status code: %{public}@", log: .action, String(describing: statusCode)) + Logger.action.debug("WebViewHandler failed with status code: \(String(describing: statusCode), privacy: .public)") self.activeContinuation?.resume(throwing: DataBrokerProtectionError.httpError(code: statusCode)) self.activeContinuation = nil } @@ -264,6 +265,6 @@ private class WebView: WKWebView { deinit { configuration.userContentController.removeAllUserScripts() - os_log("DBP WebView Deinit", log: .action) + Logger.action.debug("DBP WebView Deinit") } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift index 5a33e63831..00f51edc6f 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDataManager.swift @@ -18,6 +18,7 @@ import Foundation import Common +import os.log public protocol DataBrokerProtectionDataManaging { var cache: InMemoryDataCache { get } @@ -65,7 +66,7 @@ public class DataBrokerProtectionDataManager: DataBrokerProtectionDataManaging { public func fetchProfile() throws -> DataBrokerProtectionProfile? { if cache.profile != nil { - os_log("Returning cached profile", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Returning cached profile") return cache.profile } @@ -85,7 +86,7 @@ public class DataBrokerProtectionDataManager: DataBrokerProtectionDataManaging { cache.profile = profile return profile } else { - os_log("No profile found", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("No profile found") return nil } } @@ -94,13 +95,13 @@ public class DataBrokerProtectionDataManager: DataBrokerProtectionDataManaging { if let profile = try database.fetchProfile() { cache.profile = profile } else { - os_log("No profile found", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("No profile found") } } public func fetchBrokerProfileQueryData(ignoresCache: Bool = false) throws -> [BrokerProfileQueryData] { if !ignoresCache, !cache.brokerProfileQueryData.isEmpty { - os_log("Returning cached brokerProfileQueryData", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Returning cached brokerProfileQueryData") return cache.brokerProfileQueryData } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift index ea5afe6e22..fa24220206 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Database/DataBrokerProtectionDatabase.swift @@ -19,6 +19,7 @@ import Foundation import Common import SecureStorage +import os.log protocol DataBrokerProtectionRepository { func save(_ profile: DataBrokerProtectionProfile) async throws @@ -93,7 +94,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { try await saveNewProfile(profile, vault: vault) } } catch { - os_log("Database error: save profile, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: save profile, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.save profile")) throw error } @@ -104,7 +105,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.fetchProfile(with: Self.profileId) } catch { - os_log("Database error: fetchProfile, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: fetchProfile, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchProfile")) throw error } @@ -115,7 +116,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.deleteProfileData() } catch { - os_log("Database error: deleteProfileData, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: deleteProfileData, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.deleteProfileData")) throw error } @@ -126,7 +127,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let vault = try DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.fetchChildBrokers(for: parentBroker) } catch { - os_log("Database error: fetchChildBrokers, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: fetchChildBrokers, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchChildBrokers for parentBroker")) throw error } @@ -138,7 +139,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { return try vault.save(extractedProfile: extractedProfile, brokerId: brokerId, profileQueryId: profileQueryId) } catch { - os_log("Database error: extractedProfile, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: extractedProfile, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.save extractedProfile brokerId profileQueryId")) throw error } @@ -151,7 +152,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let profileQuery = try vault.fetchProfileQuery(with: profileQueryId), let scanJob = try vault.fetchScan(brokerId: brokerId, profileQueryId: profileQueryId) else { let error = DataBrokerProtectionError.dataNotInDatabase - os_log("Database error: brokerProfileQueryData, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: brokerProfileQueryData, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.brokerProfileQueryData for brokerId and profileQueryId")) throw error } @@ -165,7 +166,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { optOutJobData: optOutJobs ) } catch { - os_log("Database error: brokerProfileQueryData, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: brokerProfileQueryData, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.brokerProfileQueryData for brokerId and profileQueryId")) throw error } @@ -176,7 +177,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.fetchExtractedProfiles(for: brokerId) } catch { - os_log("Database error: fetchExtractedProfiles, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: fetchExtractedProfiles, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchExtractedProfiles for brokerId")) throw error } @@ -187,7 +188,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.updatePreferredRunDate(date, brokerId: brokerId, profileQueryId: profileQueryId) } catch { - os_log("Database error: updatePreferredRunDate without extractedProfileID, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: updatePreferredRunDate without extractedProfileID, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updatePreferredRunDate date brokerID profileQueryId")) throw error } @@ -203,7 +204,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) } catch { - os_log("Database error: updatePreferredRunDate with extractedProfileID, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: updatePreferredRunDate with extractedProfileID, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updatePreferredRunDate date brokerID profileQueryId extractedProfileID")) throw error } @@ -214,7 +215,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.updateLastRunDate(date, brokerId: brokerId, profileQueryId: profileQueryId) } catch { - os_log("Database error: updateLastRunDate without extractedProfileID, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: updateLastRunDate without extractedProfileID, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updateLastRunDate date brokerID profileQueryId")) throw error } @@ -231,7 +232,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { extractedProfileId: extractedProfileId ) } catch { - os_log("Database error: updateLastRunDate with extractedProfileID, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: updateLastRunDate with extractedProfileID, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updateLastRunDate date brokerId profileQueryId extractedProfileId")) throw error } @@ -251,7 +252,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { extractedProfileId: extractedProfileId ) } catch { - os_log("Database error: updateSubmittedSuccessfullyDate, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: updateSubmittedSuccessfullyDate, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updateSubmittedSuccessfullyDate date forBrokerId profileQueryId extractedProfileId")) throw error } @@ -271,7 +272,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { extractedProfileId: extractedProfileId ) } catch { - os_log("Database error: updateSevenDaysConfirmationPixelFired, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: updateSevenDaysConfirmationPixelFired, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updateSevenDaysConfirmationPixelFired pixelFired forBrokerId profileQueryId extractedProfileId")) throw error } @@ -291,7 +292,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { extractedProfileId: extractedProfileId ) } catch { - os_log("Database error: updateFourteenDaysConfirmationPixelFired, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: updateFourteenDaysConfirmationPixelFired, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updateFourteenDaysConfirmationPixelFired pixelFired forBrokerId profileQueryId extractedProfileId")) throw error } @@ -311,7 +312,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { extractedProfileId: extractedProfileId ) } catch { - os_log("Database error: updateTwentyOneDaysConfirmationPixelFired, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: updateTwentyOneDaysConfirmationPixelFired, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updateTwentyOneDaysConfirmationPixelFired pixelFired forBrokerId profileQueryId extractedProfileId")) throw error } @@ -322,7 +323,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) try vault.updateRemovedDate(for: extractedProfileId, with: date) } catch { - os_log("Database error: updateRemovedDate, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: updateRemovedDate, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.updateRemovedDate date on extractedProfileId")) throw error } @@ -338,7 +339,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { try vault.save(historyEvent: historyEvent, brokerId: historyEvent.brokerId, profileQueryId: historyEvent.profileQueryId) } } catch { - os_log("Database error: add historyEvent, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: add historyEvent, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.add historyEvent")) throw error } @@ -371,7 +372,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { return brokerProfileQueryDataList } catch { - os_log("Database error: fetchAllBrokerProfileQueryData, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: fetchAllBrokerProfileQueryData, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchAllBrokerProfileQueryData")) throw error } @@ -392,7 +393,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { fourteenDaysConfirmationPixelFired: optOut.fourteenDaysConfirmationPixelFired, twentyOneDaysConfirmationPixelFired: optOut.twentyOneDaysConfirmationPixelFired) } catch { - os_log("Database error: saveOptOutOperation, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: saveOptOutOperation, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.saveOptOutOperation optOut extractedProfile")) throw error } @@ -405,7 +406,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { return events.max(by: { $0.date < $1.date }) } catch { - os_log("Database error: fetchLastEvent, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: fetchLastEvent, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "fetchLastEvent brokerId profileQueryId")) throw error } @@ -416,7 +417,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.hasMatches() } catch { - os_log("Database error: hasMatches, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: hasMatches, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.hasMatches")) throw error } @@ -431,7 +432,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { return scan.historyEvents } catch { - os_log("Database error: fetchHistoryEvents, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: fetchHistoryEvents, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "fetchScanHistoryEvents brokerId profileQueryId")) throw error } @@ -446,7 +447,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { return optOut.historyEvents } catch { - os_log("Database error: fetchHistoryEvents, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: fetchHistoryEvents, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchOptOutHistoryEvents brokerId profileQueryId extractedProfileId")) throw error } @@ -457,7 +458,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { let vault = try self.vault ?? DataBrokerProtectionSecureVaultFactory.makeVault(reporter: secureVaultErrorReporter) return try vault.fetchAttemptInformation(for: extractedProfileId) } catch { - os_log("Database error: fetchAttemptInformation, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: fetchAttemptInformation, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.fetchAttemptInformation for extractedProfileId")) throw error } @@ -472,7 +473,7 @@ final class DataBrokerProtectionDatabase: DataBrokerProtectionRepository { lastStageDate: lastStageDate, startTime: startTime) } catch { - os_log("Database error: addAttempt, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: addAttempt, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionDatabase.addAttempt extractedProfileId attemptUUID dataBroker lastStageDate startTime")) throw error } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift index c32392586c..1a9064acee 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DataBrokerRunCustomJSONViewModel.swift @@ -21,6 +21,7 @@ import BrowserServicesKit import Common import ContentScopeScripts import Combine +import os.log struct ExtractedAddress: Codable { let state: String @@ -335,10 +336,10 @@ final class DataBrokerRunCustomJSONViewModel: ObservableObject { let fileURL = URL(fileURLWithPath: "\(path)/\(fileName)") try csv.write(to: fileURL, atomically: true, encoding: .utf8) } else { - os_log("Error getting path") + Logger.dataBrokerProtection.debug("Error getting path") } } catch { - os_log("Error writing to file: \(error)") + Logger.dataBrokerProtection.error("Error writing to file: \(error)") } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanJob.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanJob.swift index e8900027d3..c10044b1bb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanJob.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/DebugUI/DebugScanJob.swift @@ -21,6 +21,7 @@ import WebKit import BrowserServicesKit import UserScript import Common +import os.log struct DebugScanReturnValue { let brokerURL: String @@ -174,15 +175,15 @@ final class DebugScanJob: DataBrokerJob { func executeNextStep() async { retriesCountOnError = 0 // We reset the retries on error when it is successful - os_log("SCAN Waiting %{public}f seconds...", log: .action, operationAwaitTime) + Logger.action.debug("SCAN Waiting \(self.operationAwaitTime, privacy: .public) seconds...") try? await Task.sleep(nanoseconds: UInt64(operationAwaitTime) * 1_000_000_000) if let action = actionsHandler?.nextAction() { - os_log("Next action: %{public}@", log: .action, String(describing: action.actionType.rawValue)) + Logger.action.debug("Next action: \(String(describing: action.actionType.rawValue), privacy: .public)") await runNextAction(action) } else { - os_log("Releasing the web view", log: .action) + Logger.action.debug("Releasing the web view") await webViewHandler?.finish() // If we executed all steps we release the web view continuation = nil webViewHandler = nil @@ -200,6 +201,6 @@ final class DebugScanJob: DataBrokerJob { } deinit { - os_log("DebugScanOperation Deinit", log: .action) + Logger.action.debug("DebugScanOperation Deinit") } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift index 22ba75d0af..25d033a053 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/IPC/DataBrokerProtectionIPCClient.swift @@ -20,6 +20,7 @@ import Combine import Common import Foundation import XPCHelper +import os.log /// This protocol describes the server-side IPC interface for controlling the tunnel /// @@ -109,7 +110,7 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { xpc.execute(call: { server in server.openBrowser(domain: domain) }, xpcReplyErrorHandler: { error in - os_log("Error \(error.localizedDescription)") + Logger.dataBrokerProtection.error("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! }) @@ -119,7 +120,7 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { xpc.execute(call: { server in server.startImmediateOperations(showWebView: showWebView) }, xpcReplyErrorHandler: { error in - os_log("Error \(error.localizedDescription)") + Logger.dataBrokerProtection.error("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! }) @@ -129,7 +130,7 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { xpc.execute(call: { server in server.startScheduledOperations(showWebView: showWebView) }, xpcReplyErrorHandler: { error in - os_log("Error \(error.localizedDescription)") + Logger.dataBrokerProtection.error("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! }) } @@ -138,7 +139,7 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { xpc.execute(call: { server in server.runAllOptOuts(showWebView: showWebView) }, xpcReplyErrorHandler: { error in - os_log("Error \(error.localizedDescription)") + Logger.dataBrokerProtection.error("Error \(error.localizedDescription)") // Intentional no-op as there's no completion block // If you add a completion block, please remember to call it here too! }) @@ -151,7 +152,7 @@ extension DataBrokerProtectionIPCClient: IPCServerInterface { continuation.resume(returning: metaData) } }, xpcReplyErrorHandler: { error in - os_log("Error \(error.localizedDescription)") + Logger.dataBrokerProtection.error("Error \(error.localizedDescription)") continuation.resume(returning: nil) }) } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift index 926ee58947..18e5ef22b6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DBPUIViewModel.swift @@ -21,6 +21,7 @@ import Combine import WebKit import BrowserServicesKit import Common +import os.log protocol DBPUIScanOps: AnyObject { func updateCacheWithCurrentScans() async @@ -81,7 +82,7 @@ extension DBPUIViewModel: DBPUIScanOps { do { try dataManager.prepareBrokerProfileQueryDataCache() } catch { - os_log("DBPUIViewModel error: updateCacheWithCurrentScans, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("DBPUIViewModel error: updateCacheWithCurrentScans, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DBPUIViewModel.updateCacheWithCurrentScans")) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift index 2363fec57e..afb6d99efc 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/DataBroker.swift @@ -18,6 +18,7 @@ import Foundation import Common +import os.log struct DataBrokerScheduleConfig: Codable { let retryError: Int @@ -198,7 +199,7 @@ struct DataBroker: Codable, Sendable { let broker = try jsonDecoder.decode(DataBroker.self, from: data) return broker } catch { - os_log("DataBroker error: initFromResource, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("DataBroker error: initFromResource, error: \(error.localizedDescription, privacy: .public)") throw error } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunner.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunner.swift index 08916286f8..0c8cb4987d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunner.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerJobRunner.swift @@ -19,6 +19,7 @@ import Foundation import BrowserServicesKit import Common +import os.log protocol WebJobRunner { @@ -124,6 +125,6 @@ final class DataBrokerJobRunner: WebJobRunner { } deinit { - os_log("WebOperationRunner Deinit", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("WebOperationRunner Deinit") } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift index 5004e9c9da..bb9b72f09a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerOperation.swift @@ -18,6 +18,7 @@ import Foundation import Common +import os.log protocol DataBrokerOperationDependencies { var database: DataBrokerProtectionRepository { get } @@ -62,7 +63,7 @@ class DataBrokerOperation: Operation, @unchecked Sendable { private var _isFinished = false deinit { - os_log("Deinit operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) + Logger.dataBrokerProtection.debug("Deinit operation: \(String(describing: self.id.uuidString), privacy: .public)") } init(dataBrokerID: Int64, @@ -144,7 +145,7 @@ class DataBrokerOperation: Operation, @unchecked Sendable { do { allBrokerProfileQueryData = try operationDependencies.database.fetchAllBrokerProfileQueryData() } catch { - os_log("DataBrokerOperationsCollection error: runOperation, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("DataBrokerOperationsCollection error: runOperation, error: \(error.localizedDescription, privacy: .public)") return } @@ -154,11 +155,11 @@ class DataBrokerOperation: Operation, @unchecked Sendable { operationType: operationType, priorityDate: priorityDate) - os_log("filteredAndSortedOperationsData count: %{public}d for brokerID %{public}d", log: .dataBrokerProtection, filteredAndSortedOperationsData.count, dataBrokerID) + Logger.dataBrokerProtection.debug("filteredAndSortedOperationsData count: \(filteredAndSortedOperationsData.count, privacy: .public) for brokerID \(self.dataBrokerID, privacy: .public)") for operationData in filteredAndSortedOperationsData { if isCancelled { - os_log("Cancelled operation, returning...", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Cancelled operation, returning...") return } @@ -170,7 +171,7 @@ class DataBrokerOperation: Operation, @unchecked Sendable { continue } do { - os_log("Running operation: %{public}@", log: .dataBrokerProtection, String(describing: operationData)) + Logger.dataBrokerProtection.debug("Running operation: \(String(describing: operationData), privacy: .public)") try await DataBrokerProfileQueryOperationManager().runOperation(operationData: operationData, brokerProfileQueryData: brokerProfileData, @@ -187,10 +188,10 @@ class DataBrokerOperation: Operation, @unchecked Sendable { }) let sleepInterval = operationDependencies.config.intervalBetweenSameBrokerOperations - os_log("Waiting...: %{public}f", log: .dataBrokerProtection, sleepInterval) + Logger.dataBrokerProtection.debug("Waiting...: \(sleepInterval, privacy: .public)") try await Task.sleep(nanoseconds: UInt64(sleepInterval) * 1_000_000_000) } catch { - os_log("Error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) + Logger.dataBrokerProtection.error("Error: \(error.localizedDescription, privacy: .public)") errorDelegate?.dataBrokerOperationDidError(error, withBrokerName: brokerProfileQueriesData.first?.dataBroker.name) } @@ -209,7 +210,7 @@ class DataBrokerOperation: Operation, @unchecked Sendable { didChangeValue(forKey: #keyPath(isExecuting)) didChangeValue(forKey: #keyPath(isFinished)) - os_log("Finished operation: %{public}@", log: .dataBrokerProtection, String(describing: id.uuidString)) + Logger.dataBrokerProtection.debug("Finished operation: \(self.id.uuidString, privacy: .public)") } } // swiftlint:enable explicit_non_final_class diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift index 2e49861bff..126661c472 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProfileQueryOperationManager.swift @@ -18,6 +18,7 @@ import Foundation import Common +import os.log enum OperationsError: Error { case idsMissingForBrokerOrProfileQuery @@ -110,7 +111,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { isManual: Bool = false, userNotificationService: DataBrokerProtectionUserNotificationService, shouldRunNextStep: @escaping () -> Bool) async throws { - os_log("Running scan operation: %{public}@", log: .dataBrokerProtection, String(describing: brokerProfileQueryData.dataBroker.name)) + Logger.dataBrokerProtection.debug("Running scan operation: \(brokerProfileQueryData.dataBroker.name, privacy: .public)") guard let brokerId = brokerProfileQueryData.dataBroker.id, let profileQueryId = brokerProfileQueryData.profileQuery.id else { // Maybe send pixel? @@ -119,7 +120,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { defer { try? database.updateLastRunDate(Date(), brokerId: brokerId, profileQueryId: profileQueryId) - os_log("Finished scan operation: %{public}@", log: .dataBrokerProtection, String(describing: brokerProfileQueryData.dataBroker.name)) + Logger.dataBrokerProtection.debug("Finished scan operation: \(brokerProfileQueryData.dataBroker.name, privacy: .public)") notificationCenter.post(name: DataBrokerProtectionNotifications.didFinishScan, object: brokerProfileQueryData.dataBroker.name) } @@ -134,7 +135,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { try database.add(event) let extractedProfiles = try await runner.scan(brokerProfileQueryData, stageCalculator: stageCalculator, pixelHandler: pixelHandler, showWebView: showWebView, shouldRunNextStep: shouldRunNextStep) - os_log("Extracted profiles: %@", log: .dataBrokerProtection, extractedProfiles) + Logger.dataBrokerProtection.debug("Extracted profiles: \(extractedProfiles)") if !extractedProfiles.isEmpty { stageCalculator.fireScanSuccess(matchesFound: extractedProfiles.count) @@ -157,7 +158,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { try database.updateRemovedDate(nil, on: id) } - os_log("Extracted profile already exists in database: %@", log: .dataBrokerProtection, id.description) + Logger.dataBrokerProtection.debug("Extracted profile already exists in database: \(id.description)") } else { // If it's a new found profile, we'd like to opt-out ASAP // If this broker has a parent opt out, we set the preferred date to nil, as we will only perform the operation within the parent. @@ -183,7 +184,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { try database.saveOptOutJob(optOut: optOutJobData, extractedProfile: extractedProfile) - os_log("Creating new opt-out operation data for: %@", log: .dataBrokerProtection, String(describing: extractedProfile.name)) + Logger.dataBrokerProtection.debug("Creating new opt-out operation data for: \(String(describing: extractedProfile.name))") } } } else { @@ -216,7 +217,7 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { database: database ) - os_log("Profile removed from optOutsData: %@", log: .dataBrokerProtection, String(describing: removedProfile)) + Logger.dataBrokerProtection.debug("Profile removed from optOutsData: \(String(describing: removedProfile))") if let attempt = try database.fetchAttemptInformation(for: extractedProfileId), let attemptUUID = UUID(uuidString: attempt.attemptId) { let now = Date() @@ -289,12 +290,12 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { } guard extractedProfile.removedDate == nil else { - os_log("Profile already extracted, skipping...", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Profile already extracted, skipping...") return } guard let optOutStep = brokerProfileQueryData.dataBroker.optOutStep(), optOutStep.optOutType != .parentSiteOptOut else { - os_log("Broker opts out in parent, skipping...", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Broker opts out in parent, skipping...") return } @@ -303,10 +304,10 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { dataBrokerVersion: brokerProfileQueryData.dataBroker.version, handler: pixelHandler) stageDurationCalculator.fireOptOutStart() - os_log("Running opt-out operation: %{public}@", log: .dataBrokerProtection, String(describing: brokerProfileQueryData.dataBroker.name)) + Logger.dataBrokerProtection.debug("Running opt-out operation: \(brokerProfileQueryData.dataBroker.name, privacy: .public)") defer { - os_log("Finished opt-out operation: %{public}@", log: .dataBrokerProtection, String(describing: brokerProfileQueryData.dataBroker.name)) + Logger.dataBrokerProtection.debug("Finished opt-out operation: \(brokerProfileQueryData.dataBroker.name, privacy: .public)") try? database.updateLastRunDate(Date(), brokerId: brokerId, profileQueryId: profileQueryId, extractedProfileId: extractedProfileId) do { @@ -423,9 +424,9 @@ struct DataBrokerProfileQueryOperationManager: OperationsManager { database: database ) } catch { - os_log("Can't update operation date after error") + Logger.dataBrokerProtection.debug("Can't update operation date after error") } - os_log("Error on operation : %{public}@", log: .dataBrokerProtection, error.localizedDescription) + Logger.dataBrokerProtection.error("Error on operation : \(error.localizedDescription, privacy: .public)") } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift index 8e47a7b6a7..ffa67b2f73 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/DataBrokerProtectionBrokerUpdater.swift @@ -19,6 +19,7 @@ import Foundation import Common import SecureStorage +import os.log protocol ResourcesRepository { func fetchBrokerFromResourceFiles() throws -> [DataBroker]? @@ -38,8 +39,8 @@ final class FileResources: ResourcesRepository { func fetchBrokerFromResourceFiles() throws -> [DataBroker]? { guard let resourceURL = Bundle.module.resourceURL else { + Logger.dataBrokerProtection.fault("DataBrokerProtectionUpdater: error FileResources fetchBrokerFromResourceFiles, error: Bundle.module.resourceURL is nil") assertionFailure() - os_log("DataBrokerProtectionUpdater: error FileResources fetchBrokerFromResourceFiles, error: Bundle.module.resourceURL is nil", log: .error) throw FileResourcesError.bundleResourceURLNil } @@ -56,7 +57,7 @@ final class FileResources: ResourcesRepository { return try brokerJSONFiles.map(DataBroker.initFromResource(_:)) } catch { - os_log("DataBrokerProtectionUpdater: error FileResources error: fetchBrokerFromResourceFiles, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("DataBrokerProtectionUpdater: error FileResources error: fetchBrokerFromResourceFiles, error: \(error.localizedDescription, privacy: .public)") throw error } } @@ -129,7 +130,7 @@ public struct DefaultDataBrokerProtectionBrokerUpdater: DataBrokerProtectionBrok return DefaultDataBrokerProtectionBrokerUpdater(vault: vault) } - os_log("Error when trying to create vault for data broker protection updater debug menu item", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Error when trying to create vault for data broker protection updater debug menu item") return nil } @@ -138,7 +139,7 @@ public struct DefaultDataBrokerProtectionBrokerUpdater: DataBrokerProtectionBrok do { brokers = try resources.fetchBrokerFromResourceFiles() } catch { - os_log("DataBrokerProtectionBrokerUpdater updateBrokers, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("DataBrokerProtectionBrokerUpdater updateBrokers, error: \(error.localizedDescription, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionBrokerUpdater.updateBrokers")) return } @@ -148,7 +149,7 @@ public struct DefaultDataBrokerProtectionBrokerUpdater: DataBrokerProtectionBrok do { try update(broker) } catch { - os_log("Error updating broker: %{public}@, with version: %{public}@", log: .dataBrokerProtection, broker.name, broker.version) + Logger.dataBrokerProtection.debug("Error updating broker: \(broker.name, privacy: .public), with version: \(broker.version, privacy: .public)") pixelHandler.fire(.generalError(error: error, functionOccurredIn: "DataBrokerProtectionBrokerUpdater.updateBrokers")) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift index f0b49136fc..eb57e90e84 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OperationPreferredDateUpdater.swift @@ -18,6 +18,7 @@ import Foundation import Common +import os.log enum OperationPreferredDateUpdaterOrigin { case optOut @@ -83,7 +84,7 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { } } } catch { - os_log("OperationPreferredDateUpdaterUseCase error: updateChildrenBrokerForParentBroker, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("OperationPreferredDateUpdaterUseCase error: updateChildrenBrokerForParentBroker, error: \(error.localizedDescription, privacy: .public)") throw error } } @@ -168,10 +169,10 @@ struct OperationPreferredDateUpdaterUseCase: OperationPreferredDateUpdater { try database.updatePreferredRunDate(date, brokerId: brokerId, profileQueryId: profileQueryId) } } catch { - os_log("OperationPreferredDateUpdaterUseCase error: updatePreferredRunDate, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("OperationPreferredDateUpdaterUseCase error: updatePreferredRunDate, error: \(error.localizedDescription, privacy: .public)") throw error } - os_log("Updating preferredRunDate on operation with brokerId %{public}@ and profileQueryId %{public}@", log: .dataBrokerProtection, brokerId.description, profileQueryId.description) + Logger.dataBrokerProtection.debug("Updating preferredRunDate on operation with brokerId \(brokerId.description, privacy: .public) and profileQueryId \(profileQueryId.description, privacy: .public)") } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutJob.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutJob.swift index b1d32657e2..2adbab2c2c 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutJob.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/OptOutJob.swift @@ -20,6 +20,7 @@ import Foundation import WebKit import BrowserServicesKit import UserScript +import os.log import Common final class OptOutJob: DataBrokerJob { @@ -118,7 +119,7 @@ final class OptOutJob: DataBrokerJob { func executeNextStep() async { retriesCountOnError = 0 // We reset the retries on error when it is successful - os_log("OPTOUT Waiting %{public}f seconds...", log: .action, operationAwaitTime) + Logger.action.debug("OPTOUT Waiting \(self.operationAwaitTime, privacy: .public) seconds...") try? await Task.sleep(nanoseconds: UInt64(operationAwaitTime) * 1_000_000_000) if let action = actionsHandler?.nextAction(), self.shouldRunNextStep() { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift index ae72a5ae18..e0a19d727b 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ParentChildRelationship/MismatchCalculator.swift @@ -17,8 +17,9 @@ // import Foundation -import Common import BrowserServicesKit +import os.log +import Common enum MismatchValues: Int { case parentSiteHasMoreMatches @@ -50,7 +51,7 @@ struct DefaultMismatchCalculator: MismatchCalculator { do { brokerProfileQueryData = try database.fetchAllBrokerProfileQueryData() } catch { - os_log("MismatchCalculatorUseCase error: calculateMismatches, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("MismatchCalculatorUseCase error: calculateMismatches, error: \(error.localizedDescription, privacy: .public)") return } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanJob.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanJob.swift index 334017ae18..2ac6dd27b2 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanJob.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Operations/ScanJob.swift @@ -21,6 +21,7 @@ import WebKit import BrowserServicesKit import UserScript import Common +import os.log final class ScanJob: DataBrokerJob { typealias ReturnValue = [ExtractedProfile] @@ -107,15 +108,15 @@ final class ScanJob: DataBrokerJob { func executeNextStep() async { retriesCountOnError = 0 // We reset the retries on error when it is successful - os_log("SCAN Waiting %{public}f seconds...", log: .action, operationAwaitTime) + Logger.action.debug("SCAN Waiting \(self.operationAwaitTime, privacy: .public) seconds...") try? await Task.sleep(nanoseconds: UInt64(operationAwaitTime) * 1_000_000_000) if let action = actionsHandler?.nextAction() { - os_log("Next action: %{public}@", log: .action, String(describing: action.actionType.rawValue)) + Logger.action.debug("Next action: \(String(describing: action.actionType.rawValue), privacy: .public)") await runNextAction(action) } else { - os_log("Releasing the web view", log: .action) + Logger.action.debug("Releasing the web view") await webViewHandler?.finish() // If we executed all steps we release the web view } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift index 918edd298f..72daf99102 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEngagementPixels.swift @@ -17,9 +17,10 @@ // import Foundation -import Common +import os.log import BrowserServicesKit import PixelKit +import Common protocol DataBrokerProtectionEngagementPixelsRepository { func markDailyPixelSent() @@ -106,7 +107,7 @@ final class DataBrokerProtectionEngagementPixels { func fireEngagementPixel(currentDate: Date = Date()) { guard (try? database.fetchProfile()) != nil else { - os_log("No profile. We do not fire any pixel because we do not consider it an engaged user.", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("No profile. We do not fire any pixel because we do not consider it an engaged user.") return } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift index ea371e0d27..7f82c0513d 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Pixels/DataBrokerProtectionEventPixels.swift @@ -17,9 +17,10 @@ // import Foundation -import Common +import os.log import BrowserServicesKit import PixelKit +import Common protocol DataBrokerProtectionEventPixelsRepository { func markWeeklyPixelSent() @@ -92,7 +93,7 @@ final class DataBrokerProtectionEventPixels { do { data = try database.fetchAllBrokerProfileQueryData() } catch { - os_log("Database error: when attempting to fireWeeklyReportPixels, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: when attempting to fireWeeklyReportPixels, error: \(error.localizedDescription, privacy: .public)") return } let dataInThePastWeek = data.filter(hadScanThisWeek(_:)) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift index d6b4da3b93..facbfb9dad 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionAgentManager.swift @@ -20,6 +20,7 @@ import Foundation import Common import BrowserServicesKit import PixelKit +import os.log // This is to avoid exposing all the dependancies outside of the DBP package public class DataBrokerProtectionAgentManagerProvider { @@ -202,15 +203,15 @@ extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { switch oneTimeError { case DataBrokerProtectionQueueError.interrupted: self.pixelHandler.fire(.ipcServerImmediateScansInterrupted) - os_log("Interrupted during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + Logger.dataBrokerProtection.debug("Interrupted during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted(), error: \(oneTimeError.localizedDescription, privacy: .public)") default: self.pixelHandler.fire(.ipcServerImmediateScansFinishedWithError(error: oneTimeError)) - os_log("Error during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + Logger.dataBrokerProtection.debug("Error during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, error: \(oneTimeError.localizedDescription, privacy: .public)") } } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + Logger.dataBrokerProtection.debug("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, count: \(operationErrors.count, privacy: .public)") } } @@ -240,18 +241,18 @@ extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { switch oneTimeError { case DataBrokerProtectionQueueError.interrupted: self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansInterrupted) - os_log("Interrupted during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted(), error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + Logger.dataBrokerProtection.debug("Interrupted during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted(), error: \(oneTimeError.localizedDescription, privacy: .public)") case DataBrokerProtectionQueueError.cannotInterrupt: self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansBlocked) - os_log("Cannot interrupt during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted()") + Logger.dataBrokerProtection.debug("Cannot interrupt during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted()") default: self.pixelHandler.fire(.ipcServerAppLaunchedScheduledScansFinishedWithError(error: oneTimeError)) - os_log("Error during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted, error: %{public}@", log: .dataBrokerProtection, oneTimeError.localizedDescription) + Logger.dataBrokerProtection.debug("Error during DataBrokerProtectionAgentManager.appLaunched in queueManager.startScheduledOperationsIfPermitted, error: \(oneTimeError.localizedDescription, privacy: .public)") } } if let operationErrors = errors.operationErrors, operationErrors.count != 0 { - os_log("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, count: %{public}d", log: .dataBrokerProtection, operationErrors.count) + Logger.dataBrokerProtection.debug("Operation error(s) during DataBrokerProtectionAgentManager.profileSaved in queueManager.startImmediateOperationsIfPermitted, count: \(operationErrors.count, privacy: .public)") } } @@ -268,7 +269,7 @@ extension DataBrokerProtectionAgentManager: DataBrokerProtectionAgentAppEvents { self.pixelHandler.fire(.initialScanTotalDuration(duration: durationSinceStart.rounded(.towardZero), profileQueries: profileQueries)) } catch { - os_log("Initial Scans Error when trying to fetch the profile to get the profile queries", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Initial Scans Error when trying to fetch the profile to get the profile queries") } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift index 8df894d8c1..d98adfdeda 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionBackgroundActivityScheduler.swift @@ -19,6 +19,7 @@ import Foundation import Common import BrowserServicesKit +import os.log public protocol DataBrokerProtectionBackgroundActivityScheduler { func startScheduler() @@ -51,9 +52,9 @@ public final class DefaultDataBrokerProtectionBackgroundActivityScheduler: DataB activity.schedule { completion in self.lastTriggerTimestamp = Date() - os_log("Scheduler running...", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Scheduler running...") self.delegate?.dataBrokerProtectionBackgroundActivitySchedulerDidTrigger(self) { - os_log("Scheduler finished...", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Scheduler finished...") completion(.finished) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift index 297b3bc094..dd312afbb1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Scheduler/DataBrokerProtectionQueueManager.swift @@ -18,6 +18,7 @@ import Common import Foundation +import os.log protocol DataBrokerProtectionOperationQueue { var maxConcurrentOperationCount: Int { get set } @@ -226,7 +227,7 @@ private extension DefaultDataBrokerProtectionQueueManager { operationQueue.addOperation(collection) } } catch { - os_log("DataBrokerProtectionProcessor error: addOperations, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("DataBrokerProtectionProcessor error: addOperations, error: \(error.localizedDescription, privacy: .public)") completion?(DataBrokerProtectionAgentErrorCollection(oneTimeError: error)) return } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift index b29112dab0..762afd13b7 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/CaptchaService.swift @@ -18,6 +18,7 @@ import Foundation import Common +import os.log typealias CaptchaTransactionId = String typealias CaptchaResolveData = String @@ -183,7 +184,7 @@ struct CaptchaService: CaptchaServiceProtocol { throw CaptchaServiceError.cantGenerateCaptchaServiceURL } - os_log("Submitting captcha request ...", log: .service) + Logger.service.debug("Submitting captcha request ...") var request = URLRequest(url: url) guard let authHeader = authenticationManager.getAuthHeader() else { @@ -232,13 +233,13 @@ struct CaptchaService: CaptchaServiceProtocol { switch captchaResolveResult.message { case .ready: if let data = captchaResolveResult.data { - os_log("Captcha ready ...", log: .service) + Logger.service.debug("Captcha ready ...") return data } else { throw CaptchaServiceError.nilDataWhenFetchingCaptchaResult } case .notReady: - os_log("Captcha not ready ...", log: .service) + Logger.service.debug("Captcha not ready ...") if retries == 0 { throw CaptchaServiceError.timedOutWhenFetchingCaptchaResult } @@ -249,10 +250,10 @@ struct CaptchaService: CaptchaServiceProtocol { attemptId: attemptId, shouldRunNextStep: shouldRunNextStep) case .failure: - os_log("Captcha failure ...", log: .service) + Logger.service.debug("Captcha failure ...") throw CaptchaServiceError.failureWhenFetchingCaptchaResult case .invalidRequest: - os_log("Captcha invalid request ...", log: .service) + Logger.service.debug("Captcha invalid request ...") throw CaptchaServiceError.invalidRequestWhenFetchingCaptchaResult } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift index 48cd0364e7..731ac86fe1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Services/EmailService.swift @@ -18,6 +18,7 @@ import Foundation import Common +import os.log public enum EmailError: Error, Equatable, Codable { case cantGenerateURL @@ -125,17 +126,17 @@ struct EmailService: EmailServiceProtocol { switch emailResult.status { case .ready: if let link = emailResult.link, let url = URL(string: link) { - os_log("Email received", log: .service) + Logger.service.debug("Email received") return url } else { - os_log("Invalid email link", log: .service) + Logger.service.debug("Invalid email link") throw EmailError.invalidEmailLink } case .pending: if numberOfRetries == 0 { throw EmailError.linkExtractionTimedOut } - os_log("No email yet. Waiting for a new request ...", log: .service) + Logger.service.debug("No email yet. Waiting for a new request ...") try await Task.sleep(nanoseconds: pollingTimeInNanoSecondsSeconds) return try await getConfirmationLink(from: email, numberOfRetries: numberOfRetries - 1, diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseMigrationsProvider.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseMigrationsProvider.swift index ee5693929b..ed5deccc2a 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseMigrationsProvider.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionDatabaseMigrationsProvider.swift @@ -19,6 +19,7 @@ import Foundation import GRDB import Common +import os.log enum DataBrokerProtectionDatabaseMigrationErrors: Error { case deleteOrphanedRecordFailed @@ -324,7 +325,7 @@ final class DefaultDataBrokerProtectionDatabaseMigrationsProvider: DataBrokerPro try database.execute(sql: sql, arguments: [violation.originRowID]) } } catch { - os_log("Database error: error cleaning up foreign key violations, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Database error: error cleaning up foreign key violations, error: \(error.localizedDescription, privacy: .public)") } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionKeyStoreProvider.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionKeyStoreProvider.swift index 24971a4cba..e903375725 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionKeyStoreProvider.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Storage/DataBrokerProtectionKeyStoreProvider.swift @@ -20,6 +20,7 @@ import Common import Foundation import BrowserServicesKit import SecureStorage +import os.log final class DataBrokerProtectionKeyStoreProvider: SecureStorageKeyStoreProvider { @@ -57,17 +58,11 @@ final class DataBrokerProtectionKeyStoreProvider: SecureStorageKeyStoreProvider let keychainService: KeychainService private let groupNameProvider: GroupNameProviding - private let getLog: () -> OSLog - private var log: OSLog { - getLog() - } init(keychainService: KeychainService = DefaultKeychainService(), - groupNameProvider: GroupNameProviding = Bundle.main, - log: @escaping @autoclosure () -> OSLog = .disabled) { + groupNameProvider: GroupNameProviding = Bundle.main) { self.keychainService = keychainService self.groupNameProvider = groupNameProvider - self.getLog = log } func readData(named: String, serviceName: String) throws -> Data? { @@ -109,7 +104,7 @@ private extension DataBrokerProtectionKeyStoreProvider { let legacyAttributes = whenUnlockedQueryAttributes(named: name, serviceName: serviceName) let accessibilityValueString = legacyAttributes[kSecAttrAccessible as String] as? String ?? "[value unavailable]" - os_log("Attempting read and migrate of DBP Keychain data with kSecAttrAccessible value of \(accessibilityValueString)", log: .dataBrokerProtection, type: .debug) + Logger.dataBrokerProtection.debug("Attempting read and migrate of DBP Keychain data with kSecAttrAccessible value of \(accessibilityValueString)") if let data = try read(serviceName: serviceName, queryAttributes: legacyAttributes) { // We found Keychain data, so update it's `kSecAttrAccessible` value to `kSecAttrAccessibleAfterFirstUnlock` @@ -169,7 +164,7 @@ private extension DataBrokerProtectionKeyStoreProvider { } let accessibilityValueString = attributeUpdate[kSecAttrAccessible as String] as? String ?? "[value unavailable]" - os_log("Updated DBP Keychain data kSecAttrAccessible value to \(accessibilityValueString)", log: .dataBrokerProtection, type: .debug) + Logger.dataBrokerProtection.debug("Updated DBP Keychain data kSecAttrAccessible value to \(accessibilityValueString)") } func afterFirstUnlockQueryAttributes(named name: String, serviceName: String) -> [String: Any] { diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift index 3df461b1ed..7a45fc4aa1 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/DBPUICommunicationLayer.swift @@ -21,6 +21,7 @@ import WebKit import BrowserServicesKit import UserScript import Common +import os.log protocol DBPUICommunicationDelegate: AnyObject { func saveProfile() async throws @@ -85,7 +86,7 @@ struct DBPUICommunicationLayer: Subfeature { func handler(forMethodNamed methodName: String) -> Handler? { guard let actionResult = DBPUIReceivedMethodName(rawValue: methodName) else { - os_log("Cant parse method: %{public}@", log: .dataBrokerProtection, methodName) + Logger.dataBrokerProtection.debug("Cant parse method: \(methodName, privacy: .public)") return nil } @@ -113,27 +114,27 @@ struct DBPUICommunicationLayer: Subfeature { func handshake(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let data = try? JSONSerialization.data(withJSONObject: params), let result = try? JSONDecoder().decode(DBPUIHandshake.self, from: data) else { - os_log("Failed to parse handshake message", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Failed to parse handshake message") throw DBPUIError.malformedRequest } if result.version != Constants.version { - os_log("Incorrect protocol version presented by UI", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Incorrect protocol version presented by UI") return DBPUIStandardResponse(version: Constants.version, success: false) } - os_log("Successful handshake made by UI", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Successful handshake made by UI") return DBPUIStandardResponse(version: Constants.version, success: true) } func saveProfile(params: Any, original: WKScriptMessage) async throws -> Encodable? { - os_log("Web UI requested to save the profile", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Web UI requested to save the profile") do { try await delegate?.saveProfile() return DBPUIStandardResponse(version: Constants.version, success: true) } catch { - os_log("DBPUICommunicationLayer saveProfile, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("DBPUICommunicationLayer saveProfile, error: \(error.localizedDescription, privacy: .public)") return DBPUIStandardResponse(version: Constants.version, success: false) } } @@ -151,7 +152,7 @@ struct DBPUICommunicationLayer: Subfeature { try delegate?.deleteProfileData() return DBPUIStandardResponse(version: Constants.version, success: true) } catch { - os_log("DBPUICommunicationLayer deleteUserProfileData, error: %{public}@", log: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("DBPUICommunicationLayer deleteUserProfileData, error: \(error.localizedDescription, privacy: .public)") return DBPUIStandardResponse(version: Constants.version, success: false) } } @@ -159,7 +160,7 @@ struct DBPUICommunicationLayer: Subfeature { func addNameToCurrentUserProfile(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let data = try? JSONSerialization.data(withJSONObject: params), let result = try? JSONDecoder().decode(DBPUIUserProfileName.self, from: data) else { - os_log("Failed to parse addNameToCurrentUserProfile message", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Failed to parse addNameToCurrentUserProfile message") throw DBPUIError.malformedRequest } @@ -173,7 +174,7 @@ struct DBPUICommunicationLayer: Subfeature { func setNameAtIndexInCurrentUserProfile(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let data = try? JSONSerialization.data(withJSONObject: params), let result = try? JSONDecoder().decode(DBPUINameAtIndex.self, from: data) else { - os_log("Failed to parse removeNameFromCurrentUserProfile message", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Failed to parse removeNameFromCurrentUserProfile message") throw DBPUIError.malformedRequest } @@ -187,7 +188,7 @@ struct DBPUICommunicationLayer: Subfeature { func removeNameAtIndexFromCurrentUserProfile(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let data = try? JSONSerialization.data(withJSONObject: params), let result = try? JSONDecoder().decode(DBPUIIndex.self, from: data) else { - os_log("Failed to parse removeNameAtIndexFromCurrentUserProfile message", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Failed to parse removeNameAtIndexFromCurrentUserProfile message") throw DBPUIError.malformedRequest } @@ -201,7 +202,7 @@ struct DBPUICommunicationLayer: Subfeature { func setBirthYearForCurrentUserProfile(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let data = try? JSONSerialization.data(withJSONObject: params), let result = try? JSONDecoder().decode(DBPUIBirthYear.self, from: data) else { - os_log("Failed to parse setBirthYearForCurrentUserProfile message", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Failed to parse setBirthYearForCurrentUserProfile message") throw DBPUIError.malformedRequest } @@ -215,7 +216,7 @@ struct DBPUICommunicationLayer: Subfeature { func addAddressToCurrentUserProfile(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let data = try? JSONSerialization.data(withJSONObject: params), let result = try? JSONDecoder().decode(DBPUIUserProfileAddress.self, from: data) else { - os_log("Failed to parse addAddressToCurrentUserProfile message", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Failed to parse addAddressToCurrentUserProfile message") throw DBPUIError.malformedRequest } @@ -229,7 +230,7 @@ struct DBPUICommunicationLayer: Subfeature { func setAddressAtIndexInCurrentUserProfile(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let data = try? JSONSerialization.data(withJSONObject: params), let result = try? JSONDecoder().decode(DBPUIAddressAtIndex.self, from: data) else { - os_log("Failed to parse removeAddressFromCurrentUserProfile message", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Failed to parse removeAddressFromCurrentUserProfile message") throw DBPUIError.malformedRequest } @@ -243,7 +244,7 @@ struct DBPUICommunicationLayer: Subfeature { func removeAddressAtIndexFromCurrentUserProfile(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let data = try? JSONSerialization.data(withJSONObject: params), let result = try? JSONDecoder().decode(DBPUIIndex.self, from: data) else { - os_log("Failed to parse removeNameAtIndexFromCurrentUserProfile message", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Failed to parse removeNameAtIndexFromCurrentUserProfile message") throw DBPUIError.malformedRequest } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift index 04001ca81e..5a7bbf5571 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UI/UIMapper.swift @@ -18,6 +18,7 @@ import Foundation import Common +import os.log struct MapperToUI { @@ -259,10 +260,10 @@ struct MapperToUI { encoder.outputFormatting = .prettyPrinted let jsonData = try encoder.encode(metadataUI) if let jsonString = String(data: jsonData, encoding: .utf8) { - os_log("Metadata: %{public}s", log: OSLog.default, type: .info, jsonString) + Logger.dataBrokerProtection.debug("Metadata: \(jsonString, privacy: .public)") } } catch { - os_log("Error encoding struct to JSON: %{public}@", log: OSLog.default, type: .error, error.localizedDescription) + Logger.dataBrokerProtection.error("Error encoding struct to JSON: \(error.localizedDescription, privacy: .public)") } #endif diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift index 4208867430..ef8599d437 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/UserNotifications/DataBrokerProtectionUserNotificationService.swift @@ -20,6 +20,7 @@ import Foundation import UserNotifications import Common import AppKit +import os.log public enum DataBrokerProtectionNotificationCommand: String { case showDashboard = "databrokerprotection://show_dashboard" @@ -76,7 +77,7 @@ public class DefaultDataBrokerProtectionUserNotificationService: NSObject, DataB if let days = days { let calendar = Calendar.current guard let date = calendar.date(byAdding: .day, value: days, to: Date()) else { - os_log("Notification scheduled for an invalid date", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Notification scheduled for an invalid date") return } let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) @@ -89,9 +90,9 @@ public class DefaultDataBrokerProtectionUserNotificationService: NSObject, DataB userNotificationCenter.add(request) { error in if error == nil { if days != nil { - os_log("Notification scheduled", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Notification scheduled") } else { - os_log("Notification sent", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Notification sent") } } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift index fe6c7734be..747f2b18a6 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionAgentStopper.swift @@ -17,6 +17,7 @@ // import Foundation +import os.log import Common protocol DataBrokerProtectionAgentStopper { @@ -51,13 +52,13 @@ struct DefaultDataBrokerProtectionAgentStopper: DataBrokerProtectionAgentStopper do { guard try dataManager.fetchProfile() != nil, authenticationManager.isUserAuthenticated else { - os_log("Prerequisites are invalid", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Prerequisites are invalid") stopAgent() return } - os_log("Prerequisites are valid", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Prerequisites are valid") } catch { - os_log("Error validating prerequisites, error: %{public}@", log: .dataBrokerProtection, error.localizedDescription) + Logger.dataBrokerProtection.error("Error validating prerequisites, error: \(error.localizedDescription, privacy: .public)") stopAgent() } @@ -83,14 +84,14 @@ struct DefaultDataBrokerProtectionAgentStopper: DataBrokerProtectionAgentStopper private func stopAgentBasedOnEntitlementCheckResult(_ result: DataBrokerProtectionEntitlementMonitorResult) { switch result { case .enabled: - os_log("Valid entitlement", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Valid entitlement") pixelHandler.fire(.entitlementCheckValid) case .disabled: - os_log("Invalid entitlement", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Invalid entitlement") pixelHandler.fire(.entitlementCheckInvalid) stopAgent() case .error: - os_log("Error when checking entitlement", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Error when checking entitlement") /// We don't want to disable the agent in case of an error while checking for entitlements. /// Since this is a destructive action, the only situation that should cause the data to be deleted and the agent to be removed is .success(false) pixelHandler.fire(.entitlementCheckError) @@ -104,7 +105,7 @@ protocol DataProtectionStopAction { struct DefaultDataProtectionStopAction: DataProtectionStopAction { func stopAgent() { - os_log("Stopping DataBrokerProtection Agent", log: .dataBrokerProtection) + Logger.dataBrokerProtection.debug("Stopping DataBrokerProtection Agent") exit(EXIT_SUCCESS) } } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSleepObserver.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSleepObserver.swift index a38a78ec29..69088e8cb9 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSleepObserver.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/DataBrokerProtectionSleepObserver.swift @@ -19,6 +19,7 @@ import Foundation import Cocoa import Common +import os.log protocol SleepObserver { func totalSleepTime() -> TimeInterval @@ -40,7 +41,7 @@ final class DataBrokerProtectionSleepObserver: SleepObserver { } deinit { - os_log(.debug, log: .dataBrokerProtection, "SleepObserver: Deinit %{public}s %{public}s %{public}s", brokerProfileQueryData.dataBroker.name, brokerProfileQueryData.profileQuery.firstName, brokerProfileQueryData.profileQuery.city) + Logger.dataBrokerProtection.debug("SleepObserver: Deinit \(self.brokerProfileQueryData.dataBroker.name, privacy: .public) \(self.brokerProfileQueryData.profileQuery.firstName, privacy: .public) \(self.brokerProfileQueryData.profileQuery.city, privacy: .public)") NotificationCenter.default.removeObserver(self) } @@ -49,18 +50,18 @@ final class DataBrokerProtectionSleepObserver: SleepObserver { return 0 } - os_log(.debug, log: .dataBrokerProtection, "SleepObserver: Total Sleep time more than zero: %{public}s", String(totalSleepTime)) + Logger.dataBrokerProtection.debug("SleepObserver: Total Sleep time more than zero: \(String(totalSleepTime), privacy: .public)") return totalSleepTime } @objc func willSleepNotification(_ notification: Notification) { - os_log(.debug, log: .dataBrokerProtection, "SleepObserver: Computer will sleep on %{public}s %{public}s %{public}s %{public}s", brokerProfileQueryData.dataBroker.name, brokerProfileQueryData.profileQuery.firstName, brokerProfileQueryData.profileQuery.city) + Logger.dataBrokerProtection.debug("SleepObserver: Computer will sleep on \(self.brokerProfileQueryData.dataBroker.name, privacy: .public) \(self.brokerProfileQueryData.profileQuery.firstName, privacy: .public) \(self.brokerProfileQueryData.profileQuery.city, privacy: .public)") startSleepTime = Date() } @objc func didWakeNotification(_ notification: Notification) { - os_log(.debug, log: .dataBrokerProtection, "SleepObserver: Computer waking up %{public}s %{public}s %{public}s", brokerProfileQueryData.dataBroker.name, brokerProfileQueryData.profileQuery.firstName, brokerProfileQueryData.profileQuery.city) + Logger.dataBrokerProtection.debug("SleepObserver: Computer waking up \(self.brokerProfileQueryData.dataBroker.name, privacy: .public) \(self.brokerProfileQueryData.profileQuery.firstName, privacy: .public) \(self.brokerProfileQueryData.profileQuery.city, privacy: .public)") guard let startSleepTime = self.startSleepTime else { return } diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/Logger+DataBrokerProtection.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/Logger+DataBrokerProtection.swift new file mode 100644 index 0000000000..7d8609c26c --- /dev/null +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/Logger+DataBrokerProtection.swift @@ -0,0 +1,31 @@ +// +// Logger+DataBrokerProtection.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log + +public extension Logger { + fileprivate static let subsystem = "com.duckduckgo.macos.browser.databroker-protection" + + static var dataBrokerProtection = { Logger(subsystem: subsystem, category: "") }() + static var action = { Logger(subsystem: subsystem, category: "Action") }() + static var service = { Logger(subsystem: subsystem, category: "Service") }() + static var backgroundAgent = { Logger(subsystem: subsystem, category: "Background Agent") }() + static var backgroundAgentMemoryManagement = { Logger(subsystem: subsystem, category: "Background Agent Memory Management") }() + static var pixel = { Logger(subsystem: subsystem, category: "Pixel") }() +} diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/Logging.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/Logging.swift deleted file mode 100644 index 2b8016c99a..0000000000 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Utils/Logging.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// Logging.swift -// -// Copyright © 2021 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import Common - -struct Logging { - - static let subsystem = "com.duckduckgo.macos.browser.databroker-protection" - - fileprivate static let dataBrokerProtectionLoggingEnabled = true - fileprivate static let dataBrokerProtection: OSLog = OSLog(subsystem: subsystem, category: "Data Broker Protection") - - fileprivate static let actionLoggingEnabled = true - fileprivate static let action: OSLog = OSLog(subsystem: subsystem, category: "Data Broker Protection Action") - - fileprivate static let serviceLoggingEnabled = true - fileprivate static let service: OSLog = OSLog(subsystem: subsystem, category: "Data Broker Protection Service") - - fileprivate static let errorsLoggingEnabled = true - fileprivate static let error: OSLog = OSLog(subsystem: subsystem, category: "Data Broker Protection Errors") - - fileprivate static let backgroundAgentLoggingEnabled = true - fileprivate static let backgroundAgent: OSLog = OSLog(subsystem: subsystem, category: "Data Broker Protection Background Agent") - - fileprivate static let backgroundAgentMemoryManagementLoggingEnabled = true - fileprivate static let backgroundAgentMemoryManagement: OSLog = OSLog(subsystem: subsystem, category: "Data Broker Protection Background Agent Memory Management") - - fileprivate static let backgroundAgentPixelLoggingEnabled = true - fileprivate static let backgroundAgentPixel: OSLog = OSLog(subsystem: subsystem, category: "Data Broker Protection Background Agent Pixel") -} - -extension OSLog { - - public static var dataBrokerProtection: OSLog { - Logging.dataBrokerProtectionLoggingEnabled ? Logging.dataBrokerProtection : .disabled - } - - public static var action: OSLog { - Logging.actionLoggingEnabled ? Logging.action : .disabled - } - - public static var service: OSLog { - Logging.serviceLoggingEnabled ? Logging.service : .disabled - } - - public static var error: OSLog { - Logging.errorsLoggingEnabled ? Logging.error : .disabled - } - - public static var dbpBackgroundAgent: OSLog { - Logging.backgroundAgentLoggingEnabled ? Logging.backgroundAgent : .disabled - } - - public static var dbpBackgroundAgentMemoryManagement: OSLog { - Logging.backgroundAgentMemoryManagementLoggingEnabled ? Logging.backgroundAgentMemoryManagement : .disabled - } - - public static var dbpBackgroundAgentPixel: OSLog { - Logging.backgroundAgentPixelLoggingEnabled ? Logging.backgroundAgentPixel : .disabled - } -} diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStatsPixelsTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStatsPixelsTests.swift index bcc6879ce9..69e3c2ffd5 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStatsPixelsTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerProtectionStatsPixelsTests.swift @@ -19,7 +19,6 @@ import XCTest import Foundation @testable import DataBrokerProtection -import PixelKitTestingUtilities @testable import PixelKit final class DataBrokerProtectionStatsPixelsTests: XCTestCase { diff --git a/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift b/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift index eecea5a8ba..db30866437 100644 --- a/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift +++ b/LocalPackages/LoginItems/Sources/LoginItems/LoginItem.swift @@ -17,9 +17,9 @@ // import AppKit -import os.log import Foundation import ServiceManagement +import os.log public enum SMLoginItemSetEnabledError: Error { case failed @@ -28,11 +28,10 @@ public enum SMLoginItemSetEnabledError: Error { /// Takes care of enabling and disabling a login item. /// public struct LoginItem: Equatable, Hashable { - public let agentBundleID: String private let launchInformation: LoginItemLaunchInformation private let defaults: UserDefaults - private let log: OSLog + private let logger: Logger public var isRunning: Bool { !runningApplications.isEmpty @@ -74,21 +73,21 @@ public struct LoginItem: Equatable, Hashable { $0["Label"] as? String == agentBundleID }) else { return .notRegistered } - os_log("🟢 found login item job: %{public}@", log: log, job.debugDescription) + logger.debug("🟢 found login item job: \(job.debugDescription, privacy: .public)") return job["OnDemand"] as? Bool == true ? .enabled : .requiresApproval } return Status(SMAppService.loginItem(identifier: agentBundleID).status) } - public init(bundleId: String, defaults: UserDefaults, log: OSLog = .disabled) { + public init(bundleId: String, defaults: UserDefaults, logger: Logger) { self.agentBundleID = bundleId self.defaults = defaults self.launchInformation = LoginItemLaunchInformation(agentBundleID: bundleId, defaults: defaults) - self.log = log + self.logger = logger } public func enable() throws { - os_log("🟢 registering login item %{public}@", log: log, self.debugDescription) + logger.debug("🟢 registering login item \(self.debugDescription, privacy: .public)") if #available(macOS 13.0, *) { try SMAppService.loginItem(identifier: agentBundleID).register() @@ -103,7 +102,7 @@ public struct LoginItem: Equatable, Hashable { } public func disable() throws { - os_log("🟢 unregistering login item %{public}@", log: log, self.debugDescription) + logger.debug("🟢 unregistering login item \(self.debugDescription, privacy: .public)") if #available(macOS 13.0, *) { try SMAppService.loginItem(identifier: agentBundleID).unregister() @@ -121,7 +120,7 @@ public struct LoginItem: Equatable, Hashable { /// public func restart() throws { guard [.enabled].contains(status) else { - os_log("🟢 restart not needed for login item %{public}@", log: log, self.debugDescription) + logger.debug("🟢 restart not needed for login item \(self.debugDescription, privacy: .public)") return } try? disable() @@ -130,9 +129,18 @@ public struct LoginItem: Equatable, Hashable { public func forceStop() { let runningApplications = runningApplications - os_log("🟢 stopping %{public}@", log: log, runningApplications.map { $0.processIdentifier }.description) + logger.debug("🟢 stopping \(runningApplications.map { $0.processIdentifier }.description, privacy: .public)") runningApplications.forEach { $0.terminate() } } + + public static func == (lhs: LoginItem, rhs: LoginItem) -> Bool { + lhs.agentBundleID == rhs.agentBundleID && lhs.launchInformation == rhs.launchInformation + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(agentBundleID) + hasher.combine(launchInformation) + } } extension LoginItem: CustomDebugStringConvertible { diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index d34968d47d..d2ef3e55af 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "187.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "188.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift index dcfb727d6b..b94170fadd 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionProxy/FlowManagers/TCPFlowManager.swift @@ -82,12 +82,12 @@ final class TCPFlowManager { do { try await startDataCopyLoop(for: remoteConnection) - logger.log("🔴 Stopping proxy connection to \(remoteEndpoint, privacy: .public)") + logger.log("Stopping proxy connection to \(remoteEndpoint, privacy: .public)") remoteConnection.cancel() flow.closeReadWithError(nil) flow.closeWriteWithError(nil) } catch { - logger.log("🔴 Stopping proxy connection to \(remoteEndpoint, privacy: .public) with error \(String(reflecting: error), privacy: .public)") + logger.log("Stopping proxy connection to \(remoteEndpoint, privacy: .public) with error \(String(reflecting: error), privacy: .public)") remoteConnection.cancel() flow.closeReadWithError(error) diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 9169d430dd..5509929cd4 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "187.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "188.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/LocalPackages/UDSHelper/Sources/UDSHelper/Logger+UDSHelper.swift b/LocalPackages/UDSHelper/Sources/UDSHelper/Logger+UDSHelper.swift new file mode 100644 index 0000000000..5a4d5b9675 --- /dev/null +++ b/LocalPackages/UDSHelper/Sources/UDSHelper/Logger+UDSHelper.swift @@ -0,0 +1,24 @@ +// +// Logger+UDSHelper.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import os.log + +extension Logger { + static var udsHelper = { Logger(subsystem: "UDS Helper", category: "") }() +} diff --git a/LocalPackages/UDSHelper/Sources/UDSHelper/UDSClient.swift b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSClient.swift index 1c3f153176..58dfde0afe 100644 --- a/LocalPackages/UDSHelper/Sources/UDSHelper/UDSClient.swift +++ b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSClient.swift @@ -33,7 +33,6 @@ public actor UDSClient { private let socketFileURL: URL private let receiver: UDSReceiver private let queue = DispatchQueue(label: "com.duckduckgo.UDSConnection.queue.\(UUID().uuidString)") - private let log: OSLog private let payloadHandler: PayloadHandler? // MARK: - Message completion callbacks @@ -47,15 +46,12 @@ public actor UDSClient { /// This should not be called directly because the socketFileURL needs to comply with some requirements in terms of /// maximum length of the path. Use any public factory method provided below instead. /// - public init(socketFileURL: URL, - log: OSLog, - payloadHandler: PayloadHandler? = nil) { + public init(socketFileURL: URL, payloadHandler: PayloadHandler? = nil) { - os_log("UDSClient - Initialized with path: %{public}@", log: log, type: .info, socketFileURL.path) + Logger.udsHelper.info("UDSClient - Initialized with path: \(socketFileURL.path, privacy: .public)") - self.receiver = UDSReceiver(log: log) + self.receiver = UDSReceiver() self.socketFileURL = socketFileURL - self.log = log self.payloadHandler = payloadHandler } @@ -106,19 +102,19 @@ public actor UDSClient { private func statusUpdateHandler(_ state: NWConnection.State) { switch state { case .cancelled: - os_log("UDSClient - Connection cancelled", log: self.log, type: .info) + Logger.udsHelper.info("UDSClient - Connection cancelled") self.releaseConnection() case .failed(let error): - os_log("UDSClient - Connection failed with error: %{public}@", log: self.log, type: .error, String(describing: error)) + Logger.udsHelper.error("UDSClient - Connection failed with error: \(error.localizedDescription, privacy: .public)") self.releaseConnection() case .ready: - os_log("UDSClient - Connection ready", log: self.log, type: .info) + Logger.udsHelper.info("UDSClient - Connection ready") case .waiting(let error): - os_log("UDSClient - Waiting to connect... %{public}@", log: self.log, type: .info, String(describing: error)) + Logger.udsHelper.error("UDSClient - Waiting to connect... \(error.localizedDescription, privacy: .public)") default: - os_log("UDSClient - Unexpected state", log: self.log, type: .info) + Logger.udsHelper.info("UDSClient - Unexpected state") } } @@ -171,12 +167,12 @@ public actor UDSClient { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in connection.send(content: payload, completion: .contentProcessed { error in if let error { - os_log("UDSClient - Send Error %{public}@", log: self.log, String(describing: error)) + Logger.udsHelper.error("UDSClient - Send Error \(error.localizedDescription, privacy: .public)") continuation.resume(throwing: error) return } - os_log("UDSClient - Send Success", log: self.log) + Logger.udsHelper.info("UDSClient - Send Success") continuation.resume() }) } diff --git a/LocalPackages/UDSHelper/Sources/UDSHelper/UDSReceiver.swift b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSReceiver.swift index 6ad9e5bef6..425292af7e 100644 --- a/LocalPackages/UDSHelper/Sources/UDSHelper/UDSReceiver.swift +++ b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSReceiver.swift @@ -35,12 +35,6 @@ struct UDSReceiver { case connectionClosed } - private let log: OSLog - - init(log: OSLog) { - self.log = log - } - /// Starts receiveing messages for a specific connection /// /// - Parameters: @@ -66,37 +60,25 @@ struct UDSReceiver { } catch { switch error { case ReadError.notEnoughData(let expected, let received): - os_log("UDSServer - Connection closing due to error: Not enough data (expected: %{public}@, received: %{public}@", - log: log, - type: .error, - String(describing: expected), - String(describing: received)) + Logger.udsHelper.error("UDSServer - Connection closing due to error: Not enough data (expected: \(String(describing: expected), privacy: .public), received: \(String(describing: received), privacy: .public)") guard await errorHandler(error) else { return } case ReadError.connectionError(let error): - os_log("UDSServer - Connection closing due to a connection error: %{public}@", - log: log, - type: .error, - String(describing: error)) + Logger.udsHelper.error("UDSServer - Connection closing due to a connection error: \(error.localizedDescription, privacy: .public)") guard await errorHandler(error) else { return } case ReadError.connectionClosed: - os_log("UDSServer - Connection closing: End of file reached", - log: log, - type: .info) + Logger.udsHelper.info("UDSServer - Connection closing: End of file reached") guard await errorHandler(error) else { return } default: - os_log("UDSServer - Connection closing due to error: %{public}@", - log: log, - type: .error, - String(describing: error)) + Logger.udsHelper.error("UDSServer - Connection closing due to error: \(error.localizedDescription, privacy: .public)") guard await errorHandler(error) else { return diff --git a/LocalPackages/UDSHelper/Sources/UDSHelper/UDSServer.swift b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSServer.swift index ed72e65fb7..f56ddd57ef 100644 --- a/LocalPackages/UDSHelper/Sources/UDSHelper/UDSServer.swift +++ b/LocalPackages/UDSHelper/Sources/UDSHelper/UDSServer.swift @@ -73,7 +73,6 @@ public final class UDSServer { private let fileManager: FileManager private let socketFileURL: URL - private let log: OSLog /// Default initializer /// @@ -82,13 +81,11 @@ public final class UDSServer { /// to share this socket with other apps in the same app group, this path should be in an app group /// that both apps have access to. /// - socketFileName: the name of the socket file - /// - log: the log to use /// - public init(socketFileURL: URL, fileManager: FileManager = .default, log: OSLog) { + public init(socketFileURL: URL, fileManager: FileManager = .default) { self.fileManager = fileManager self.socketFileURL = socketFileURL - self.log = log - self.receiver = UDSReceiver(log: log) + self.receiver = UDSReceiver() do { try fileManager.removeItem(at: socketFileURL) @@ -96,7 +93,7 @@ public final class UDSServer { print(error) } - os_log("UDSServer - Initialized with path: %{public}@", log: log, type: .info, socketFileURL.path) + Logger.udsHelper.info("UDSServer - Initialized with path: \(socketFileURL.path, privacy: .public)") } public func start(messageHandler: @escaping (Data) async throws -> Data?) throws { @@ -114,10 +111,7 @@ public final class UDSServer { listener = try NWListener(using: params) self.listener = listener } catch { - os_log("UDSServer - Error creating listener: %{public}@", - log: log, - type: .error, - String(describing: error)) + Logger.udsHelper.error("UDSServer - Error creating listener: \(error.localizedDescription, privacy: .public)") throw error } @@ -130,12 +124,12 @@ public final class UDSServer { switch state { case .ready: - os_log("UDSServer - Listener is ready", log: log, type: .info) + Logger.udsHelper.info("UDSServer - Listener is ready") case .failed(let error): - os_log("UDSServer - Listener failed with error: %{public}@", log: log, type: .error, String(describing: error)) + Logger.udsHelper.error("UDSServer - Listener failed with error: \(error.localizedDescription, privacy: .public)") stop() case .cancelled: - os_log("UDSServer - Listener cancelled", log: log, type: .info) + Logger.udsHelper.info("UDSServer - Listener cancelled") default: break } @@ -168,23 +162,20 @@ public final class UDSServer { private func handleNewConnection(_ connection: NWConnection, messageHandler: @escaping (Data) async throws -> Data?) { Task { - os_log("UDSServer - New connection: %{public}@", - log: log, - type: .info, - String(describing: connection.hashValue)) + Logger.udsHelper.info("UDSServer - New connection: \(String(describing: connection.hashValue), privacy: .public)") connection.stateUpdateHandler = { [weak self] state in guard let self else { return } switch state { case .ready: - os_log("UDSServer - Client connection is ready", log: log, type: .info) + Logger.udsHelper.info("UDSServer - Client connection is ready") self.startReceivingMessages(on: connection, messageHandler: messageHandler) case .failed(let error): - os_log("UDSServer - Client connection failed with error: %{public}@", log: log, type: .error, String(describing: error)) + Logger.udsHelper.error("UDSServer - Client connection failed with error: \(error.localizedDescription, privacy: .public)") self.closeConnection(connection) case .cancelled: - os_log("UDSServer - Client connection cancelled", log: log, type: .info) + Logger.udsHelper.info("UDSServer - Client connection cancelled") default: break } @@ -262,12 +253,12 @@ public final class UDSServer { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in connection.send(content: lengthData + data, completion: .contentProcessed { error in if let error { - os_log("UDSServer - Send Error %{public}@", log: self.log, String(describing: error)) + Logger.udsHelper.error("UDSServer - Send Error \(error.localizedDescription, privacy: .public)") continuation.resume(throwing: error) return } - os_log("UDSServer - Send Success", log: self.log) + Logger.udsHelper.info("UDSServer - Send Success") continuation.resume() }) } diff --git a/Submodules/privacy-reference-tests b/Submodules/privacy-reference-tests index 6133e7d9d9..afb4f6128a 160000 --- a/Submodules/privacy-reference-tests +++ b/Submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit 6133e7d9d9cd5f1b925cab1971b4d785dc639df7 +Subproject commit afb4f6128a3b50d53ddcb1897ea1fb4df6858aa1 diff --git a/UnitTests/PrivacyReferenceTests/BrokenSiteReportingReferenceTests.swift b/UnitTests/PrivacyReferenceTests/BrokenSiteReportingReferenceTests.swift index 5b1eed5211..939334597c 100644 --- a/UnitTests/PrivacyReferenceTests/BrokenSiteReportingReferenceTests.swift +++ b/UnitTests/PrivacyReferenceTests/BrokenSiteReportingReferenceTests.swift @@ -63,11 +63,11 @@ final class BrokenSiteReportingReferenceTests: XCTestCase { for test in testData.reportURL.tests { if test.exceptPlatforms.contains(PrivacyReferenceTestHelper.privacyReferenceTestPlatformName) { - os_log("Skipping test, ignore platform for [%s]", type: .info, test.name) + Logger.general.debug("Skipping test, ignore platform for [\(test.name)]") continue } - os_log("Testing [%s]", type: .info, test.name) + Logger.general.debug("Testing [\(test.name)]") var errors: [Error]? if let errs = test.errorDescriptions { diff --git a/UnitTests/PrivacyReferenceTests/FireproofingReferenceTests.swift b/UnitTests/PrivacyReferenceTests/FireproofingReferenceTests.swift index ef5f327672..a4c8691b83 100644 --- a/UnitTests/PrivacyReferenceTests/FireproofingReferenceTests.swift +++ b/UnitTests/PrivacyReferenceTests/FireproofingReferenceTests.swift @@ -59,7 +59,7 @@ final class FireproofingReferenceTests: XCTestCase { @MainActor private func runReferenceTest(_ test: Test) async { - os_log("Testing %s", test.name) + Logger.general.debug("Testing \(test.name)") let loginDomains = testData.fireButtonFireproofing.fireproofedSites.map { sanitizedSite($0) } let logins = MockPreservedLogins(domains: loginDomains, tld: ContentBlocking.shared.tld) diff --git a/sandbox-test-tool/SandboxTestTool.swift b/sandbox-test-tool/SandboxTestTool.swift index 1234426615..d7f0e3a6b2 100644 --- a/sandbox-test-tool/SandboxTestTool.swift +++ b/sandbox-test-tool/SandboxTestTool.swift @@ -20,6 +20,7 @@ import AppKit import Combine import Common import Foundation +import os.log @main struct SandboxTestTool { @@ -46,60 +47,9 @@ final class SandboxTestToolApp: NSApplication { } -extension FileLogger: FilePresenterLogger { - func log(_ message: @autoclosure () -> String) { - log(message(), includeTimestamp: true) - } -} - -final class FileLogger { - static let shared = FileLogger() - - private init() { - if !FileManager.default.fileExists(atPath: fileURL.path) { - FileManager.default.createFile(atPath: fileURL.path, contents: nil) - } - } - - private let pid = ProcessInfo().processIdentifier - - private let fileURL: URL = { - let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - return documentsURL.appendingPathComponent("logfile.txt") - }() - - private let dateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "HH:mm:ss.SSS" - return formatter - }() - - private let queue = DispatchQueue(label: "log queue") - private lazy var fileHandle: FileHandle = { - let fileHandle = (try? FileHandle(forWritingTo: fileURL))! - fileHandle.seekToEndOfFile() - return fileHandle - }() - - func log(_ message: String, includeTimestamp: Bool) { - os_log("%{public}s", message) - queue.sync { - let logMessage = includeTimestamp ? "[\(pid)] \(dateFormatter.string(from: Date())): \(message)\n" : (message + "\n") - - fileHandle.write(logMessage.data(using: .utf8)!) - } - } - -} - final class SandboxTestToolAppDelegate: NSObject, NSApplicationDelegate { - // uncomment these for logging -// #if CI - let logger = OSLog.disabled -// #else -// let logger = FileLogger.shared -// #endif + let logger = Logger(subsystem: "SandboxTestTool", category: "") override init() { logger.log("\n\n\n🚦 starting…\n") @@ -180,7 +130,7 @@ final class SandboxTestToolAppDelegate: NSObject, NSApplicationDelegate { return } do { - let filePresenter = try BookmarkFilePresenter(fileBookmarkData: bookmark, logger: logger) + let filePresenter = try BookmarkFilePresenter(fileBookmarkData: bookmark) guard let url = filePresenter.url else { throw NSError(domain: "SandboxTestTool", code: -1, userInfo: [NSLocalizedDescriptionKey: "FilePresenter URL is nil"]) } filePresenter.urlPublisher.dropFirst().sink { [unowned self] url in From 60df7568dc2fab96109ce5f95d83bebe35378207 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:38:04 +0200 Subject: [PATCH 04/14] onboarding dax dialogs (#3149) Task/Issue URL: https://app.asana.com/0/1204186595873227/1208077416568671/f Tech Design URL: Look at BSK branch **Description**: Adds Contextual Dax Dialogs --- DuckDuckGo.xcodeproj/project.pbxproj | 42 + DuckDuckGo/Common/Localizables/UserText.swift | 24 + DuckDuckGo/Localizable.xcstrings | 1200 +++++++++++++++++ .../ContextualOnboardingDialogs.swift | 278 ++++ .../OnboardingSuggestedSearchesProvider.swift | 79 ++ ...ardingSuggestedSearchesProviderTests.swift | 77 ++ 6 files changed, 1700 insertions(+) create mode 100644 DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingDialogs.swift create mode 100644 DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingSuggestedSearchesProvider.swift create mode 100644 UnitTests/Onboarding/OnboardingSuggestedSearchesProviderTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 35783aeca4..a94f9351cc 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1428,6 +1428,12 @@ 560C3FFD2BC9911000F589CE /* PermanentSurveyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFB2BC9911000F589CE /* PermanentSurveyManagerTests.swift */; }; 560C3FFF2BCD5A1E00F589CE /* PermanentSurveyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFE2BCD5A1E00F589CE /* PermanentSurveyManager.swift */; }; 560C40002BCD5A1E00F589CE /* PermanentSurveyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C3FFE2BCD5A1E00F589CE /* PermanentSurveyManager.swift */; }; + 560EB9322C78946F0080DBC8 /* ContextualOnboardingDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EB9312C78946F0080DBC8 /* ContextualOnboardingDialogs.swift */; }; + 560EB9332C78946F0080DBC8 /* ContextualOnboardingDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EB9312C78946F0080DBC8 /* ContextualOnboardingDialogs.swift */; }; + 560EB9352C7897370080DBC8 /* Onboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 560EB9342C7897370080DBC8 /* Onboarding */; }; + 560EB9372C78974C0080DBC8 /* Onboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 560EB9362C78974C0080DBC8 /* Onboarding */; }; + 560EB9392C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EB9382C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift */; }; + 560EB93A2C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560EB9382C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift */; }; 5614B3A12BBD639D009B5031 /* ZoomPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5614B3A02BBD639D009B5031 /* ZoomPopover.swift */; }; 5614B3A22BBD639D009B5031 /* ZoomPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5614B3A02BBD639D009B5031 /* ZoomPopover.swift */; }; 561D29C22BDA745A007B91D0 /* MockSyncPausedStateManaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 561D29C02BDA7430007B91D0 /* MockSyncPausedStateManaging.swift */; }; @@ -1517,6 +1523,8 @@ 56BA1E882BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */; }; 56BA1E8A2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; 56BA1E8B2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; + 56CE77612C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */; }; + 56CE77622C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */; }; 56CEE90E2B7A725B00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; 56CEE90F2B7A725C00CF10AA /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */; }; 56D145E829E6BB6300E3488A /* CapturingDataImportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D145E729E6BB6300E3488A /* CapturingDataImportProvider.swift */; }; @@ -3491,6 +3499,8 @@ 5603D90529B7B746007F9F01 /* MockTabViewItemDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTabViewItemDelegate.swift; sourceTree = ""; }; 560C3FFB2BC9911000F589CE /* PermanentSurveyManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermanentSurveyManagerTests.swift; sourceTree = ""; }; 560C3FFE2BCD5A1E00F589CE /* PermanentSurveyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermanentSurveyManager.swift; sourceTree = ""; }; + 560EB9312C78946F0080DBC8 /* ContextualOnboardingDialogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextualOnboardingDialogs.swift; sourceTree = ""; }; + 560EB9382C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestedSearchesProvider.swift; sourceTree = ""; }; 5614B3A02BBD639D009B5031 /* ZoomPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomPopover.swift; sourceTree = ""; }; 561D29C02BDA7430007B91D0 /* MockSyncPausedStateManaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSyncPausedStateManaging.swift; sourceTree = ""; }; 561D29C42BDA749A007B91D0 /* MockDDGSyncing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDDGSyncing.swift; sourceTree = ""; }; @@ -3537,6 +3547,7 @@ 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageUserScript.swift; sourceTree = ""; }; 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageUserScriptTests.swift; sourceTree = ""; }; 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateTrustEvaluator.swift; sourceTree = ""; }; + 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestedSearchesProviderTests.swift; sourceTree = ""; }; 56CEE9092B7A66C500CF10AA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56CEE90D2B7A6DE100CF10AA /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; 56D145E729E6BB6300E3488A /* CapturingDataImportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturingDataImportProvider.swift; sourceTree = ""; }; @@ -4400,6 +4411,7 @@ 3706FCAA293F65D500E42796 /* UserScript in Frameworks */, 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */, 4BF97AD52B43C43F00EB4240 /* NetworkProtection in Frameworks */, + 560EB9372C78974C0080DBC8 /* Onboarding in Frameworks */, 3739326529AE4B39009346AE /* DDGSync in Frameworks */, D6BC8AC82C5A95B10025375B /* DuckPlayer in Frameworks */, 37DF000729F9C061002B7D3E /* SyncDataProviders in Frameworks */, @@ -4586,6 +4598,7 @@ files = ( C18BF9CC2C73678500ED6B8A /* Freemium in Frameworks */, F1DF95E32BD1807C0045E591 /* Crashes in Frameworks */, + 560EB9352C7897370080DBC8 /* Onboarding in Frameworks */, 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */, 1EA7B8D32B7E078C000330A4 /* SubscriptionUI in Frameworks */, B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */, @@ -6079,6 +6092,15 @@ path = View; sourceTree = ""; }; + 560EB9302C78943E0080DBC8 /* ContextualOnboarding */ = { + isa = PBXGroup; + children = ( + 560EB9312C78946F0080DBC8 /* ContextualOnboardingDialogs.swift */, + 560EB9382C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift */, + ); + path = ContextualOnboarding; + sourceTree = ""; + }; 561D29BF2BDA7419007B91D0 /* Mocks */ = { isa = PBXGroup; children = ( @@ -6496,6 +6518,7 @@ 85B7184727677A7D00B4277F /* Onboarding */ = { isa = PBXGroup; children = ( + 560EB9302C78943E0080DBC8 /* ContextualOnboarding */, 85707F2F276A7DB000DC0649 /* ViewModel */, 85B7184827677A9200B4277F /* View */, 56A053FB2C19E8F7007D8FAB /* OnboardingActionsManager.swift */, @@ -6608,6 +6631,7 @@ 56A0542F2C2043C8007D8FAB /* OnboardingTabExtensionTests.swift */, 56A054402C22438C007D8FAB /* OnboardingNavigatingTests.swift */, 56A054432C2252CE007D8FAB /* OnboardingUserScriptTests.swift */, + 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */, ); path = Onboarding; sourceTree = ""; @@ -8745,6 +8769,7 @@ 371209242C232E6C003ADF3D /* RemoteMessaging */, D6BC8AC72C5A95B10025375B /* DuckPlayer */, 9D9DE5742C63AA0C00D20B15 /* AppKitExtensions */, + 560EB9362C78974C0080DBC8 /* Onboarding */, C18BF9CD2C73678C00ED6B8A /* Freemium */, ); productName = DuckDuckGo; @@ -9162,6 +9187,7 @@ 371209222C232E66003ADF3D /* RemoteMessaging */, D6BC8AC52C5A95AA0025375B /* DuckPlayer */, 9D9DE5722C63AA0700D20B15 /* AppKitExtensions */, + 560EB9342C7897370080DBC8 /* Onboarding */, C18BF9CB2C73678500ED6B8A /* Freemium */, ); productName = DuckDuckGo; @@ -10043,6 +10069,7 @@ 3706FAB9293F65D500E42796 /* TabBarViewController.swift in Sources */, 56A054202C1CA1F5007D8FAB /* OnboardingTabExtension.swift in Sources */, 3706FABA293F65D500E42796 /* BookmarkOutlineViewDataSource.swift in Sources */, + 560EB9332C78946F0080DBC8 /* ContextualOnboardingDialogs.swift in Sources */, 3706FABB293F65D500E42796 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D220BF92B86192200F8BBC6 /* PreferencesEmailProtectionView.swift in Sources */, 9FA173E42B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift in Sources */, @@ -10649,6 +10676,7 @@ 3706FC47293F65D500E42796 /* FavoritesView.swift in Sources */, 3706FC48293F65D500E42796 /* HomePage.swift in Sources */, 56A0543F2C215FB3007D8FAB /* OnboardingUserScript.swift in Sources */, + 560EB93A2C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift in Sources */, 3706FC49293F65D500E42796 /* RoundedSelectionRowView.swift in Sources */, 4B9DB01E2A983B24000927DB /* Waitlist.swift in Sources */, 3706FC4A293F65D500E42796 /* LocalStatisticsStore.swift in Sources */, @@ -10864,6 +10892,7 @@ 3706FE03293F661700E42796 /* CoreDataStoreTests.swift in Sources */, 1D9FDEC42B9B63C90040B78C /* DataClearingPreferencesTests.swift in Sources */, 3706FE04293F661700E42796 /* TreeControllerTests.swift in Sources */, + 56CE77622C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */, 3706FE05293F661700E42796 /* DownloadsWebViewMock.m in Sources */, 3706FE06293F661700E42796 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, 56BA1E7F2BAB2D29001CF69F /* ErrorPageTabExtensionTest.swift in Sources */, @@ -11668,6 +11697,7 @@ B6AAAC2D260330580029438D /* PublishedAfter.swift in Sources */, 3701C9CE29BD040C00305B15 /* FirefoxBerkeleyDatabaseReader.swift in Sources */, 37054FCE2876472D00033B6F /* WebViewSnapshotView.swift in Sources */, + 560EB9392C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift in Sources */, 4BBC16A027C4859400E00A38 /* DeviceAuthenticationService.swift in Sources */, CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */, 1DEF3BAD2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */, @@ -11928,6 +11958,7 @@ C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */, 4BB88B5B25B7BA50006F6B06 /* Instruments.swift in Sources */, 3768D8442C2CC884004120AE /* RemoteMessagingConfigMatcherProvider.swift in Sources */, + 560EB9322C78946F0080DBC8 /* ContextualOnboardingDialogs.swift in Sources */, 9812D895276CEDA5004B6181 /* ContentBlockerRulesLists.swift in Sources */, 4B0511E2262CAA8600F6079C /* NSViewControllerExtension.swift in Sources */, C16127EE2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */, @@ -12295,6 +12326,7 @@ 4B9292BB2667103100AD2C21 /* BookmarkNodeTests.swift in Sources */, 4B0219A825E0646500ED7DEA /* WebsiteDataStoreTests.swift in Sources */, AAC9C01E24CB6BEB00AD1325 /* TabCollectionViewModelTests.swift in Sources */, + 56CE77612C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */, B662D3DE275613BB0035D4D6 /* EncryptionKeyStoreMock.swift in Sources */, 1D3B1ABF29369FC8006F4388 /* BWEncryptionTests.swift in Sources */, B6F56567299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */, @@ -13893,6 +13925,16 @@ package = 4311906792B7676CE9535D76 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Crashes; }; + 560EB9342C7897370080DBC8 /* Onboarding */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Onboarding; + }; + 560EB9362C78974C0080DBC8 /* Onboarding */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Onboarding; + }; 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionProxy; diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index b2dfd2c47c..0378703b02 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1246,6 +1246,30 @@ struct UserText { } } } + + // MARK: - Onboarding + enum ContextualOnboarding { + static let onboardingTryASearchTitle = NSLocalizedString("contextual.onboarding.try-a-search.title", value: "Try a search!", comment: "Title of a popover on the browser that invites the user to try a search") + static let onboardingTryASearchMessage = NSLocalizedString("contextual.onboarding.try-a-search.message", value: "Your DuckDuckGo searches are always anonymous.", comment: "Message of a popover on the browser that invites the user to try a search explaining that their searches are anonymous") + static let onboardingTryASiteTitle = NSLocalizedString("contextual.onboarding.try-a-site.title", value: "Next, try visiting a site!", comment: "Title of a popover on the browser that invites the user to try a visiting a website") + static let onboardingTryASiteNTPTitle = NSLocalizedString("contextual.onboarding.ntp.try-a-site.title", value: "Try visiting a site!", comment: "Title of a popover on the new tab page browser that invites the user to try a visiting a website") + static let onboardingTryASiteMessage = NSLocalizedString("contextual.onboarding.try-a-site.message", value: "I’ll block trackers so they can’t spy on you.", comment: "Message of a popover on the browser that invites the user to try visiting a website to explain that we block trackers") + static let onboardingTryFireButtonMessage = NSLocalizedString("contextual.onboarding.try-fire-button.message", value: "Instantly clear your browsing activity with the Fire Button.\n\nGive it a try! 🔥", comment: "Message of a popover on the browser that invites the user to try visiting the browser Fire Button. Please leave the line break") + static let onboardingGotItButton = NSLocalizedString("contextual.onboarding.got-it.button", value: "Got it", comment: "During onboarding steps this button is shown and takes either to the next steps or closes the onboarding.") + static let onboardingFirstSearchDoneTitle = NSLocalizedString("contextual.onboarding.first-search-done.title", value: "That’s DuckDuckGo Search.", comment: "After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo") + static let onboardingFirstSearchDoneMessage = NSLocalizedString("contextual.onboarding.first-search-done.message", value: "Private. Fast. Fewer ads.", comment: "After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo") + static let onboardingFinalScreenTitle = NSLocalizedString("contextual.onboarding.final-screen.title", value: "You’ve got this!", comment: "Title of the last screen of the onboarding to the browser app") + static let onboardingFinalScreenMessage = NSLocalizedString("contextual.onboarding.final-screen.message", value: "Remember: every time you browse with me a creepy ad loses its wings. 👌", comment: "Message of the last screen of the onboarding to the browser app.") + static let onboardingFinalScreenButton = NSLocalizedString("contextual.onboarding.final-screen.button", value: "High five!", comment: "Button on the last screen of the onboarding, it will dismiss the onboarding screen.") + static let tryASearchOption1English = NSLocalizedString("contextual.onboarding.try-search.option1-English", value: "how to say “duck” in spanish", comment: "Browser Search query for how to say duck in english") + static let tryASearchOption1International = NSLocalizedString("contextual.onboarding.try-search.option1international", value: "how to say “duck” in english", comment: "Browser Search query for how to say duck in english") + static let tryASearchOption2English = NSLocalizedString("contextual.onboarding.try-search.option2-english", value: "mighty ducks cast", comment: "Search query for the cast of Mighty Ducks") + static let tryASearchOption2International = NSLocalizedString("contextual.onboarding.try-search.option2-international", value: "cast of avatar", comment: "Search query for the cast of Avatar") + static let tryASearchOption3 = NSLocalizedString("contextual.onboarding.try-search.option3", value: "local weather", comment: "Browser Search query for local weather") + static let tryASearchOptionSurpriseMeTitle = NSLocalizedString("contextual.onboarding.try-search.surprise-me-title", value: "Surprise me!", comment: "Title for a button that triggers an unknown search query for the user.") + static let tryASearchOptionSurpriseMeEnglish = NSLocalizedString("contextual.onboarding.try-search.surprise-me-english", value: "chocolate chip cookie recipes", comment: "Browser Search query for chocolate chip cookie recipes") + static let tryASearchOptionSurpriseMeInternational = NSLocalizedString("contextual.onboarding.try-search.surprise-me-international", value: "dinner recipes", comment: "Browser Search query for dinner recipes") + } // Key: "subscription.menu.item" // Comment: "Title for Subscription item in the options menu" diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index ccd1671697..374d3ae6f2 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -13390,6 +13390,1206 @@ } } }, + "contextual.onboarding.final-screen.button" : { + "comment" : "Button on the last screen of the onboarding, it will dismiss the onboarding screen.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Schlag ein!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "High five!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Choca esos cinco!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bien joué !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Batti cinque!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "High five!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Piątka!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dá cá cinco!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дай пять!" + } + } + } + }, + "contextual.onboarding.final-screen.message" : { + "comment" : "Message of the last screen of the onboarding to the browser app.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hinweis: Jedes Mal, wenn du mit mir browst, verliert eine gruselige Anzeige ihren Schrecken. 👌" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Remember: every time you browse with me a creepy ad loses its wings. 👌" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recuerda: cada vez que navegas conmigo corto las alas a un anuncio horrible. 👌" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pensez-y : chaque fois que vous naviguez avec moi, une publicité douteuse disparaît. 👌" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ricorda: quando navighi con me gli annunci inquietanti non possono seguirti. 👌" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Denk eraan: elke keer als je met mij browset, verliest een enge advertentie zijn vleugels. 👌" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pamiętaj: za każdym razem, gdy przeglądasz ze mną Internet, jakaś wstrętna reklama przestaje działać. 👌" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lembra-te: sempre que navegas comigo, um anúncio assustador perde as suas asas. 👌" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Бродить по сайтам с нами — значит подрезать крылья назойливой рекламе. 👌" + } + } + } + }, + "contextual.onboarding.final-screen.title" : { + "comment" : "Title of the last screen of the onboarding to the browser app", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gut gemacht!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You’ve got this!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Lo estás haciendo muy bien!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bien joué !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ben fatto!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je kunt het!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Udało się!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Você consegue!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Проще некуда!" + } + } + } + }, + "contextual.onboarding.first-search-done.message" : { + "comment" : "After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privat. Schnell. Weniger Werbung." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Private. Fast. Fewer ads." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privado. Rápido. Menos anuncios." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privé. Rapide. Moins de publicités." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privato. Veloce. Meno annunci." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privé. Snel. Minder advertenties." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prywatna. Szybka. Z mniejszą liczbą reklam." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privado. Rápido. Menos anúncios." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Надежно. Быстро. Меньше рекламы." + } + } + } + }, + "contextual.onboarding.first-search-done.title" : { + "comment" : "After the user performs their first search using the browser, this dialog explains the advantages of using DuckDuckGo", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Das ist DuckDuckGo Search." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "That’s DuckDuckGo Search." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eso es DuckDuckGo Search." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "C'est DuckDuckGo Search." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "È DuckDuckGo Search." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dat is DuckDuckGo Search." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "To DuckDuckGo Search." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "É a DuckDuckGo Search." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Это — DuckDuckGo Search." + } + } + } + }, + "contextual.onboarding.got-it.button" : { + "comment" : "During onboarding steps this button is shown and takes either to the next steps or closes the onboarding.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verstanden" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Got it" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entendido" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "J'ai compris" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ho capito" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ik snap het" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rozumiem" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Entendi" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Понятно" + } + } + } + }, + "contextual.onboarding.ntp.try-a-site.title" : { + "comment" : "Title of a popover on the new tab page browser that invites the user to try a visiting a website", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versuche, eine Website zu besuchen!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Try visiting a site!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Intenta visitar un sitio!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Essayez de visiter un site !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prova a visitare un sito!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bezoek eens een site!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spróbuj odwiedzić witrynę!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Experimenta visitar um site!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Попробуйте посетить сайт!" + } + } + } + }, + "contextual.onboarding.try-a-search.message" : { + "comment" : "Message of a popover on the browser that invites the user to try a search explaining that their searches are anonymous", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine DuckDuckGo-Suchanfragen sind immer anonym." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your DuckDuckGo searches are always anonymous." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tus búsquedas en DuckDuckGo son siempre anónimas." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vos recherches sur DuckDuckGo sont toujours anonymes." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le tue ricerche su DuckDuckGo sono sempre anonime." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je DuckDuckGo-zoekopdrachten zijn altijd anoniem." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wyszukiwania w DuckDuckGo zawsze są anonimowe." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "As tuas pesquisas no DuckDuckGo são sempre anónimas." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ваши поисковые запросы в DuckDuckGo всегда анонимны." + } + } + } + }, + "contextual.onboarding.try-a-search.title" : { + "comment" : "Title of a popover on the browser that invites the user to try a search", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Probiere eine Suche aus!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Try a search!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Prueba con una búsqueda!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Essayez une recherche !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Prova una ricerca!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Probeer een zoekopdracht!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Spróbuj coś wyszukać!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Experimenta fazer uma pesquisa!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Попробуйте ввести запрос!" + } + } + } + }, + "contextual.onboarding.try-a-site.message" : { + "comment" : "Message of a popover on the browser that invites the user to try visiting a website to explain that we block trackers", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ich blockiere Tracker, damit sie dich nicht ausspionieren können." + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "I’ll block trackers so they can’t spy on you." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquearé los rastreadores para que no puedan espiarte." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Je bloquerai les traqueurs afin qu'ils ne puissent pas vous espionner." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloccherò i sistemi di tracciamento in modo che non possano spiarti e" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ik blokkeer trackers zodat ze je niet kunnen bespioneren." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zablokuję skrypty śledzące, aby nie mogły Cię szpiegować." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bloquearei rastreadores para que não possam espiá-lo." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Мы заблокируем трекеры и пресечем слежку." + } + } + } + }, + "contextual.onboarding.try-a-site.title" : { + "comment" : "Title of a popover on the browser that invites the user to try a visiting a website", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Versuche als nächstes, eine Website zu besuchen!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Next, try visiting a site!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡A continuación, intenta visitar un sitio!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ensuite, essayez de visiter un site !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "In seguito, prova a visitare un sito!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Probeer nu een site te bezoeken!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Następnie spróbuj odwiedzić witrynę!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Em seguida, experimenta visitar um site!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "А теперь попробуйте посетить сайт!" + } + } + } + }, + "contextual.onboarding.try-fire-button.message" : { + "comment" : "Message of a popover on the browser that invites the user to try visiting the browser Fire Button. Please leave the line break", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lösche deine Browseraktivitäten sofort mit dem Fire Button.\n\nProbier’s doch mal aus! 🔥" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Instantly clear your browsing activity with the Fire Button.\n\nGive it a try! 🔥" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Borra al instante tu actividad de navegación con el Fire Button.\n\n¡Pruébalo! 🔥" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Effacez instantanément votre activité de navigation avec le Fire Button.\n\nEssayez par vous-même ! 🔥" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancella istantaneamente la tua attività di navigazione con il Fire Button.\n\nProvalo! 🔥" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wis je browser-activiteit direct met de Fire Button.\n\nProbeer het maar! 🔥" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Natychmiast wyczyść swoją aktywność związaną z przeglądaniem za pomocą przycisku Fire Button.\n\nSpróbuj! 🔥" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Limpa instantaneamente a tua atividade de navegação com o Fire Button.\n\nExperimenta! 🔥" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Кнопка Fire Button моментально стирает из браузера данные о посещении сайтов.\n\nУбедитесь сами! 🔥" + } + } + } + }, + "contextual.onboarding.try-search.option1-English" : { + "comment" : "Browser Search query for how to say duck in english", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "wie sagt man „Ente“ auf Spanisch" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "how to say “duck” in spanish" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cómo se dice «pato» en inglés" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "comment dire « duck » en espagnol" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "come si dice \"anatra\" in spagnolo" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "hoe zeg je 'eend' in het Spaans?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "jak się mówi „kaczka” po hiszpańsku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "como dizer \"pato\" em espanhol" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Как сказать «утка» по-испански?" + } + } + } + }, + "contextual.onboarding.try-search.option1international" : { + "comment" : "Browser Search query for how to say duck in english", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "wie sagt man „Ente“ auf Englisch" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "how to say “duck” in english" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "cómo se dice «pato» en inglés" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "comment dire « canard » en anglais" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "come si dice \"anatra\" in inglese" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "hoe zeg je 'eend' in het Engels?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "jak się mówi „kaczka” po angielsku" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "como dizer \"pato\" em inglês" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Как сказать «утка» по-английски?" + } + } + } + }, + "contextual.onboarding.try-search.option2-english" : { + "comment" : "Search query for the cast of Mighty Ducks", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Besetzung von Mighty Ducks" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "mighty ducks cast" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "reparto de Mighty Ducks" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "casting de mighty ducks" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "cast delle papere potenti" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "cast van Mighty Ducks" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "obsada potężnych kaczorów" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "elenco do filme A Hora dos Campeões" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Кто играет главные роли в фильме «Могучие утята»?" + } + } + } + }, + "contextual.onboarding.try-search.option2-international" : { + "comment" : "Search query for the cast of Avatar", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Besetzung von Avatar" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "cast of avatar" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "reparto de Avatar" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "casting d'avatar" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "cast di avatar" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "cast van avatar" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "obsada avatara" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "elenco de avatar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "актерский состав аватара" + } + } + } + }, + "contextual.onboarding.try-search.option3" : { + "comment" : "Browser Search query for local weather", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lokales Wetter" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "local weather" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "el tiempo local" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "météo locale" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "meteo locale" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "lokaal weer" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "pogoda lokalna" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "meteorologia local" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Местная погода" + } + } + } + }, + "contextual.onboarding.try-search.surprise-me-english" : { + "comment" : "Browser Search query for chocolate chip cookie recipes", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rezepte für Schokoladenkekse" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "chocolate chip cookie recipes" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "recetas de galletas con pepitas de chocolate" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "recettes de cookies aux pépites de chocolat" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "ricette di biscotti con gocce di cioccolato" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recepten voor chocoladekoekjes" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "przepisy na ciastka z kawałkami czekolady" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "receitas de biscoitos de chocolate" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "рецепты печенья с шоколадной крошкой" + } + } + } + }, + "contextual.onboarding.try-search.surprise-me-international" : { + "comment" : "Browser Search query for dinner recipes", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dinner-Rezepte" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "dinner recipes" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "recetas para la cena" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "recettes pour le dîner" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "ricette per la cena" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "recepten voor het avondeten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "przepisy na obiad" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "receitas de jantar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "рецепты на ужин" + } + } + } + }, + "contextual.onboarding.try-search.surprise-me-title" : { + "comment" : "Title for a button that triggers an unknown search query for the user.", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Überrasche mich!" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Surprise me!" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¡Sorpréndeme!" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Surprenez-moi !" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sorprendimi!" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verras me!" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zaskocz mnie!" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Surpreende-me!" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Удиви меня!" + } + } + } + }, "copy" : { "comment" : "Copy button", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingDialogs.swift b/DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingDialogs.swift new file mode 100644 index 0000000000..63981b9303 --- /dev/null +++ b/DuckDuckGo/Onboarding/ContextualOnboarding/ContextualOnboardingDialogs.swift @@ -0,0 +1,278 @@ +// +// ContextualOnboardingDialogs.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI +import Onboarding +import SwiftUIExtensions + +struct OnboardingDialogsContants { + static let titleFont = Font.system(size: 20, weight: .bold, design: .rounded) + static let messageFont = Font.system(size: Self.messageFontSize, weight: .regular, design: .rounded) + static let messageFontSize = 16.0 +} + +struct OnboardingTrySearchDialog: View { + let title = UserText.ContextualOnboarding.onboardingTryASearchTitle + let message = NSAttributedString(string: UserText.ContextualOnboarding.onboardingTryASearchMessage) + let viewModel: OnboardingSearchSuggestionsViewModel + + var body: some View { + DaxDialogView(logoPosition: .left) { + ContextualDaxDialogContent( + orientation: .horizontalStack(alignment: .top), + title: title, + titleFont: OnboardingDialogsContants.titleFont, + message: message, + messageFont: OnboardingDialogsContants.messageFont, + list: viewModel.itemsList, + listAction: viewModel.listItemPressed + ) + } + .padding() + } + +} + +struct OnboardingTryVisitingSiteDialog: View { + let viewModel: OnboardingSiteSuggestionsViewModel + + var body: some View { + DaxDialogView(logoPosition: .left) { + OnboardingTryVisitingSiteDialogContent(viewModel: viewModel) + } + .padding() + + } +} + +struct OnboardingTryVisitingSiteDialogContent: View { + let message = NSAttributedString(string: UserText.ContextualOnboarding.onboardingTryASiteMessage) + + let viewModel: OnboardingSiteSuggestionsViewModel + + var body: some View { + ContextualDaxDialogContent( + orientation: .horizontalStack(alignment: .top), + title: viewModel.title, + titleFont: OnboardingDialogsContants.titleFont, + message: message, + messageFont: OnboardingDialogsContants.messageFont, + list: viewModel.itemsList, + listAction: viewModel.listItemPressed) + } +} + +struct OnboardingFirstSearchDoneDialog: View { + let title = UserText.ContextualOnboarding.onboardingFirstSearchDoneTitle + let message = NSAttributedString(string: UserText.ContextualOnboarding.onboardingFirstSearchDoneMessage) + let cta = UserText.ContextualOnboarding.onboardingGotItButton + + @State private var showNextScreen: Bool = false + + let shouldFollowUp: Bool + let viewModel: OnboardingSiteSuggestionsViewModel + let gotItAction: () -> Void + + var body: some View { + DaxDialogView(logoPosition: .left) { + VStack { + if showNextScreen { + OnboardingTryVisitingSiteDialogContent(viewModel: viewModel) + } else { + ContextualDaxDialogContent( + orientation: .horizontalStack(alignment: .center), + title: title, + titleFont: OnboardingDialogsContants.titleFont, + message: message, + messageFont: OnboardingDialogsContants.messageFont, + customActionView: AnyView( + OnboardingPrimaryCTAButton(title: cta) { + gotItAction() + withAnimation { + if shouldFollowUp { + showNextScreen = true + } + } + } + ) + ) + } + } + } + .padding() + + } +} + +struct OnboardingFireButtonDialogContent: View { + private let attributedMessage: NSAttributedString = { + let firstString = UserText.ContextualOnboarding.onboardingTryFireButtonMessage + let boldString = "Fire Button." + let attributedString = NSMutableAttributedString(string: firstString) + let boldFontAttribute: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: OnboardingDialogsContants.messageFontSize, weight: .bold) + ] + if let boldRange = firstString.range(of: boldString) { + let nsBoldRange = NSRange(boldRange, in: firstString) + attributedString.addAttributes(boldFontAttribute, range: nsBoldRange) + } + + return attributedString + }() + + var body: some View { + ContextualDaxDialogContent( + orientation: .horizontalStack(alignment: .center), + message: attributedMessage, + messageFont: OnboardingDialogsContants.messageFont, + customActionView: AnyView(actionView)) + } + + @ViewBuilder + private var actionView: some View { + VStack { + OnboardingPrimaryCTAButton(title: "Try it", action: {}) + OnboardingSecondaryCTAButton(title: "Skip", action: {}) + } + } + +} + +struct OnboardingFireDialog: View { + + var body: some View { + DaxDialogView(logoPosition: .left) { + VStack { + OnboardingFireButtonDialogContent() + } + } + .padding() + + } +} + +struct OnboardingTrackersDoneDialog: View { + let cta = UserText.ContextualOnboarding.onboardingGotItButton + + @State private var showNextScreen: Bool = false + + let shouldFollowUp: Bool + let message: NSAttributedString + let blockedTrackersCTAAction: () -> Void + + var body: some View { + DaxDialogView(logoPosition: .left) { + VStack { + if showNextScreen { + OnboardingFireButtonDialogContent() + } else { + ContextualDaxDialogContent( + orientation: .horizontalStack(alignment: .center), + message: message, + messageFont: OnboardingDialogsContants.messageFont, + customActionView: AnyView( + OnboardingPrimaryCTAButton(title: cta) { + blockedTrackersCTAAction() + if shouldFollowUp { + withAnimation { + showNextScreen = true + } + } + } + ) + ) + } + } + } + .padding() + + } +} + +struct OnboardingPrimaryCTAButton: View { + let title: String + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .padding(.vertical, 3) + .padding(.horizontal, 24) + } + .buttonStyle(DefaultActionButtonStyle(enabled: true)) + .shadow(radius: 1, x: -0.6, y: +0.6) + } + +} + +struct OnboardingSecondaryCTAButton: View { + @Environment(\.colorScheme) var colorScheme + + let title: String + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .padding(.vertical, 3) + .padding(.horizontal, 26) + } + .buttonStyle(DismissActionButtonStyle()) + } + +} + +// MARK: - Preview + +#Preview("Try Search") { + OnboardingTrySearchDialog(viewModel: OnboardingSearchSuggestionsViewModel(suggestedSearchesProvider: OnboardingSuggestedSearchesProvider(), pixelReporter: OnboardingPixelReporter())) + .padding() +} + +final class OnboardingPixelReporter: OnboardingSearchSuggestionsPixelReporting, OnboardingSiteSuggestionsPixelReporting { + func trackSiteSuggetionOptionTapped() { + } + func trackSearchSuggetionOptionTapped() { + } +} + +#Preview("Try Site") { + OnboardingTryVisitingSiteDialog(viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter())) + .padding() +} + +#Preview("First Search Dialog") { + OnboardingFirstSearchDoneDialog(shouldFollowUp: true, viewModel: OnboardingSiteSuggestionsViewModel(title: UserText.ContextualOnboarding.onboardingTryASiteTitle, suggestedSitesProvider: OnboardingSuggestedSitesProvider(surpriseItemTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle), pixelReporter: OnboardingPixelReporter()), gotItAction: {}) + .padding() +} + +#Preview("Try Fire Button") { + DaxDialogView(logoPosition: .left) { + OnboardingFireButtonDialogContent() + } + .padding() +} + +#Preview("Trackers Dialog") { + var message: NSAttributedString = { + let firstString = UserText.ContextualOnboarding.onboardingTryFireButtonMessage + return NSMutableAttributedString(string: firstString) + }() + return OnboardingTrackersDoneDialog(shouldFollowUp: true, message: message, blockedTrackersCTAAction: {}) + .padding() +} diff --git a/DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingSuggestedSearchesProvider.swift b/DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingSuggestedSearchesProvider.swift new file mode 100644 index 0000000000..a51454e8de --- /dev/null +++ b/DuckDuckGo/Onboarding/ContextualOnboarding/OnboardingSuggestedSearchesProvider.swift @@ -0,0 +1,79 @@ +// +// OnboardingSuggestedSearchesProvider.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Onboarding + +struct OnboardingSuggestedSearchesProvider: OnboardingSuggestionsItemsProviding { + private let countryAndLanguageProvider: OnboardingRegionAndLanguageProvider + + init(countryAndLanguageProvider: OnboardingRegionAndLanguageProvider = Locale.current) { + self.countryAndLanguageProvider = countryAndLanguageProvider + } + + var list: [ContextualOnboardingListItem] { + return [ + option1, + option2, + surpriseMe + ] + } + + private var country: String? { + countryAndLanguageProvider.regionCode + } + private var language: String? { + countryAndLanguageProvider.languageCode + } + + private var option1: ContextualOnboardingListItem { + var search: String + if language == "en" { + search = UserText.ContextualOnboarding.tryASearchOption1English + } else { + search = UserText.ContextualOnboarding.tryASearchOption1International + } + return ContextualOnboardingListItem.search(title: search) + } + + private var option2: ContextualOnboardingListItem { + var search: String + if country == "us" { + search = UserText.ContextualOnboarding.tryASearchOption2English + } else { + search = UserText.ContextualOnboarding.tryASearchOption2International + } + return ContextualOnboardingListItem.search(title: search) + } + + private var option3: ContextualOnboardingListItem { + let search = UserText.ContextualOnboarding.tryASearchOption3 + return ContextualOnboardingListItem.search(title: search) + } + + private var surpriseMe: ContextualOnboardingListItem { + var search: String + if country == "us" { + search = UserText.ContextualOnboarding.tryASearchOptionSurpriseMeEnglish + } else { + search = UserText.ContextualOnboarding.tryASearchOptionSurpriseMeInternational + } + return ContextualOnboardingListItem.surprise(title: search, visibleTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) + } + +} diff --git a/UnitTests/Onboarding/OnboardingSuggestedSearchesProviderTests.swift b/UnitTests/Onboarding/OnboardingSuggestedSearchesProviderTests.swift new file mode 100644 index 0000000000..405f242732 --- /dev/null +++ b/UnitTests/Onboarding/OnboardingSuggestedSearchesProviderTests.swift @@ -0,0 +1,77 @@ +// +// OnboardingSuggestedSearchesProviderTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +import XCTest +import Onboarding +@testable import DuckDuckGo_Privacy_Browser + +class OnboardingSuggestedSearchesProviderTests: XCTestCase { + + let userText = UserText.ContextualOnboarding.self + + func testSearchesListForEnglishLanguageAndUsRegion() { + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "en") + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + + let expectedSearches = [ + ContextualOnboardingListItem.search(title: userText.tryASearchOption1English), + ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), + ContextualOnboardingListItem.surprise(title: userText.tryASearchOptionSurpriseMeEnglish, visibleTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) + ] + + XCTAssertEqual(provider.list, expectedSearches) + } + + func testSearchesListForNonEnglishLanguageAndNonUSRegion() { + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "fr", languageCode: "fr") + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + + let expectedSearches = [ + ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), + ContextualOnboardingListItem.search(title: userText.tryASearchOption2International), + ContextualOnboardingListItem.surprise(title: userText.tryASearchOptionSurpriseMeInternational, visibleTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) + ] + + XCTAssertEqual(provider.list, expectedSearches) + } + + func testSearchesListForUSRegionAndNonEnglishLanguage() { + let mockProvider = MockOnboardingRegionAndLanguageProvider(regionCode: "us", languageCode: "es") + let provider = OnboardingSuggestedSearchesProvider(countryAndLanguageProvider: mockProvider) + + let expectedSearches = [ + ContextualOnboardingListItem.search(title: userText.tryASearchOption1International), + ContextualOnboardingListItem.search(title: userText.tryASearchOption2English), + ContextualOnboardingListItem.surprise(title: userText.tryASearchOptionSurpriseMeEnglish, visibleTitle: UserText.ContextualOnboarding.tryASearchOptionSurpriseMeTitle) + ] + + XCTAssertEqual(provider.list, expectedSearches) + } +} + +class MockOnboardingRegionAndLanguageProvider: OnboardingRegionAndLanguageProvider { + var regionCode: String? + var languageCode: String? + + init(regionCode: String?, languageCode: String?) { + self.regionCode = regionCode + self.languageCode = languageCode + } +} From da7daf03f766bc303fc8608660c33ebafe7d95e3 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 28 Aug 2024 21:11:41 +0200 Subject: [PATCH 05/14] Add error pixels for Subscription keychain access errors (#3147) Task/Issue URL: https://app.asana.com/0/1201037661562251/1208146974665104/f **Description**: Add missing pixel reporting for subscription related keychain access errors. --- DuckDuckGo.xcodeproj/project.pbxproj | 6 ++++- .../xcshareddata/swiftpm/Package.resolved | 4 +-- .../MacPacketTunnelProvider.swift | 15 ++++++++--- DuckDuckGo/Statistics/PrivacyProPixel.swift | 27 +++++++++++++++++++ ...riptionManager+StandardConfiguration.swift | 11 ++++++++ DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift | 17 +++++++++--- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 9 files changed, 73 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a94f9351cc..4218b95de4 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -185,6 +185,8 @@ 1EA7B8D52B7E078C000330A4 /* Subscription in Frameworks */ = {isa = PBXBuildFile; productRef = 1EA7B8D42B7E078C000330A4 /* Subscription */; }; 1ED910D52B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; 1ED910D62B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED910D42B63BFB300936947 /* IdentityTheftRestorationPagesUserScript.swift */; }; + 1EFA1A072C7C7F0E0099F508 /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; + 1EFA1A082C7C7F0F0099F508 /* PrivacyProPixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F188267F2BBEB58100D9AC4F /* PrivacyProPixel.swift */; }; 310E79BF294A19A8007C49E8 /* FireproofingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */; }; 311B262728E73E0A00FD181A /* TabShadowConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311B262628E73E0A00FD181A /* TabShadowConfig.swift */; }; 31267C692B640C4200FEF811 /* DataBrokerProtectionFeatureGatekeeper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C5FFB82AF64D120008A79F /* DataBrokerProtectionFeatureGatekeeper.swift */; }; @@ -11414,6 +11416,7 @@ 31A83FB72BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, F1D042942BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, F1C70D822BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, + 1EFA1A072C7C7F0E0099F508 /* PrivacyProPixel.swift in Sources */, 9D9AE91D2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9212AAA3B450026E7DC /* UserText.swift in Sources */, 31ECDA132BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, @@ -11430,6 +11433,7 @@ 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */, F1D042952BFBA12300A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, F1C70D832BFF510000599292 /* SubscriptionEnvironment+Default.swift in Sources */, + 1EFA1A082C7C7F0F0099F508 /* PrivacyProPixel.swift in Sources */, 9D9AE91E2AAA3B450026E7DC /* DuckDuckGoDBPBackgroundAgentAppDelegate.swift in Sources */, 9D9AE9222AAA3B450026E7DC /* UserText.swift in Sources */, 31ECDA142BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, @@ -13594,7 +13598,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 188.0.0; + version = 188.1.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0898a2acca..396e8be9f2 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "faf25f57d1d61ff855216178c454616031585c07", - "version" : "188.0.0" + "revision" : "ce1b7228a38d2b18525590256051a012109cfee6", + "version" : "188.1.0" } }, { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index baceae66e6..e8c83fb1ce 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -448,9 +448,9 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { let subscriptionEndpointService = DefaultSubscriptionEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) let authEndpointService = DefaultAuthEndpointService(currentServiceEnvironment: subscriptionEnvironment.serviceEnvironment) let accountManager = DefaultAccountManager(accessTokenStorage: tokenStore, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionEndpointService, - authEndpointService: authEndpointService) + entitlementsCache: entitlementsCache, + subscriptionEndpointService: subscriptionEndpointService, + authEndpointService: authEndpointService) let entitlementsCheck = { await accountManager.hasEntitlement(forProductName: .networkProtection, cachePolicy: .reloadIgnoringLocalCacheData) @@ -474,6 +474,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { entitlementCheck: entitlementsCheck) setupPixels() + accountManager.delegate = self observeServerChanges() observeStatusUpdateRequests() } @@ -659,3 +660,11 @@ final class DefaultWireGuardInterface: WireGuardInterface { wgSetLogger(context, logFunction) } } + +extension MacPacketTunnelProvider: AccountManagerKeychainAccessDelegate { + + public func accountManagerKeychainAccessFailed(accessType: AccountKeychainAccessType, error: AccountKeychainAccessError) { + PixelKit.fire(PrivacyProErrorPixel.privacyProKeychainAccessError(accessType: accessType, accessError: error), + frequency: .dailyAndCount) + } +} diff --git a/DuckDuckGo/Statistics/PrivacyProPixel.swift b/DuckDuckGo/Statistics/PrivacyProPixel.swift index 5609e24ed1..c4a88a6b52 100644 --- a/DuckDuckGo/Statistics/PrivacyProPixel.swift +++ b/DuckDuckGo/Statistics/PrivacyProPixel.swift @@ -17,6 +17,7 @@ // import Foundation +import Subscription import PixelKit // swiftlint:disable private_over_fileprivate @@ -119,3 +120,29 @@ enum PrivacyProPixel: PixelKitEventV2 { return nil } } + +enum PrivacyProErrorPixel: PixelKitEventV2 { + + case privacyProKeychainAccessError(accessType: AccountKeychainAccessType, accessError: AccountKeychainAccessError) + + var name: String { + switch self { + case .privacyProKeychainAccessError: return "m_mac_privacy-pro_keychain_access_error" + } + } + + var parameters: [String: String]? { + switch self { + case .privacyProKeychainAccessError(let accessType, let accessError): + return [ + "type": accessType.rawValue, + "error": accessError.errorDescription + ] + } + } + + var error: (any Error)? { + return nil + } + +} diff --git a/DuckDuckGo/Subscription/SubscriptionManager+StandardConfiguration.swift b/DuckDuckGo/Subscription/SubscriptionManager+StandardConfiguration.swift index e9562194eb..c92a66516d 100644 --- a/DuckDuckGo/Subscription/SubscriptionManager+StandardConfiguration.swift +++ b/DuckDuckGo/Subscription/SubscriptionManager+StandardConfiguration.swift @@ -19,6 +19,7 @@ import Foundation import Subscription import Common +import PixelKit extension DefaultSubscriptionManager { @@ -53,5 +54,15 @@ extension DefaultSubscriptionManager { authEndpointService: authEndpointService, subscriptionEnvironment: subscriptionEnvironment) } + + accountManager.delegate = self + } +} + +extension DefaultSubscriptionManager: AccountManagerKeychainAccessDelegate { + + public func accountManagerKeychainAccessFailed(accessType: AccountKeychainAccessType, error: AccountKeychainAccessError) { + PixelKit.fire(PrivacyProErrorPixel.privacyProKeychainAccessError(accessType: accessType, accessError: error), + frequency: .dailyAndCount) } } diff --git a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift index f2c6e25861..f271288894 100644 --- a/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift +++ b/DuckDuckGoVPN/DuckDuckGoVPNAppDelegate.swift @@ -35,7 +35,7 @@ import os.log @objc(Application) final class DuckDuckGoVPNApplication: NSApplication { - public let accountManager: AccountManager + public var accountManager: AccountManager private let _delegate: DuckDuckGoVPNAppDelegate override init() { @@ -58,9 +58,9 @@ final class DuckDuckGoVPNApplication: NSApplication { settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) let accessTokenStorage = SubscriptionTokenKeychainStorage(keychainType: .dataProtection(.named(subscriptionAppGroup))) accountManager = DefaultAccountManager(accessTokenStorage: accessTokenStorage, - entitlementsCache: entitlementsCache, - subscriptionEndpointService: subscriptionEndpointService, - authEndpointService: authEndpointService) + entitlementsCache: entitlementsCache, + subscriptionEndpointService: subscriptionEndpointService, + authEndpointService: authEndpointService) _delegate = DuckDuckGoVPNAppDelegate(accountManager: accountManager, accessTokenStorage: accessTokenStorage, @@ -69,6 +69,7 @@ final class DuckDuckGoVPNApplication: NSApplication { setupPixelKit() self.delegate = _delegate + accountManager.delegate = _delegate #if DEBUG if accountManager.accessToken != nil { @@ -436,3 +437,11 @@ final class DuckDuckGoVPNAppDelegate: NSObject, NSApplicationDelegate { } } } + +extension DuckDuckGoVPNAppDelegate: AccountManagerKeychainAccessDelegate { + + public func accountManagerKeychainAccessFailed(accessType: AccountKeychainAccessType, error: AccountKeychainAccessError) { + PixelKit.fire(PrivacyProErrorPixel.privacyProKeychainAccessError(accessType: accessType, accessError: error), + frequency: .dailyAndCount) + } +} diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 42f9b92915..b13d7fd5d7 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: "188.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "188.1.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index d2ef3e55af..0b727a7e12 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "188.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "188.1.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 5509929cd4..bee1dc2536 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "188.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "188.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 959decf924eb8cc4ebbf5e12ce7eb165e0f82a69 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 29 Aug 2024 00:13:59 +0200 Subject: [PATCH 06/14] Migrate asana-extract-task-assignee from GHA to fastlane action (#3167) Task/Issue URL: https://app.asana.com/0/1203301625297703/1208137627434486/f Description: This change removes asana-extract-task-assignee action, replacing it with a fastlane action from the automation plugin. --- .../asana-extract-task-assignee/action.yml | 25 ------------------- .../action.yml | 10 ++++---- Gemfile.lock | 6 ++--- fastlane/Pluginfile | 2 +- 4 files changed, 9 insertions(+), 34 deletions(-) delete mode 100644 .github/actions/asana-extract-task-assignee/action.yml diff --git a/.github/actions/asana-extract-task-assignee/action.yml b/.github/actions/asana-extract-task-assignee/action.yml deleted file mode 100644 index e50e105889..0000000000 --- a/.github/actions/asana-extract-task-assignee/action.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Extract Asana Task Assignee -description: Returns the Asana task assignee, if task is assigned -inputs: - access-token: - description: "Asana access token" - required: true - type: string - task-id: - description: "Asana task ID" - required: true - type: string -outputs: - assignee-id: - description: "Assignee ID" - value: ${{ steps.extract-assignee-id.outputs.assignee-id }} -runs: - using: "composite" - steps: - - id: extract-assignee-id - run: | - assignee_id=$(curl -fLSs "https://app.asana.com/api/1.0/tasks/${{ inputs.task-id }}?opt_fields=assignee" \ - -H "Authorization: Bearer ${{ inputs.access-token }}" \ - | jq -r '.data.assignee.gid') - echo "assignee-id=${assignee_id}" >> $GITHUB_OUTPUT - shell: bash diff --git a/.github/actions/asana-get-release-automation-subtask-id/action.yml b/.github/actions/asana-get-release-automation-subtask-id/action.yml index 750a960984..0b50aa37bc 100644 --- a/.github/actions/asana-get-release-automation-subtask-id/action.yml +++ b/.github/actions/asana-get-release-automation-subtask-id/action.yml @@ -15,7 +15,7 @@ outputs: value: ${{ steps.extract-automation-task-id.outputs.automation-task-id }} assignee-id: description: "Release task assignee ID" - value: ${{ steps.extract-assignee-id.outputs.assignee-id }} + value: ${{ steps.extract-assignee-id.outputs.asana_assignee_id }} runs: using: "composite" steps: @@ -24,10 +24,10 @@ runs: run: bundle exec fastlane run asana_extract_task_id task_url:"${{ inputs.task-url }}" - id: extract-assignee-id - uses: ./.github/actions/asana-extract-task-assignee - with: - task-id: ${{ steps.extract-task-id.outputs.asana_task_id }} - access-token: ${{ inputs.access-token }} + shell: bash + env: + ASANA_ACCESS_TOKEN: ${{ inputs.access-token }} + run: bundle exec fastlane run asana_extract_task_assignee task_id:"${{ steps.extract-task-id.outputs.asana_task_id }}" - id: extract-automation-task-id run: | diff --git a/Gemfile.lock b/Gemfile.lock index 33a08b76f8..1277000bcc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ GIT remote: https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation - revision: 766668da77fe379e9cae0ae183083ca82fe1b663 - tag: 0.1.0 + revision: 5d267b66e9c63f879eecbb4ae1f40cb42547026e + tag: 0.2.0 specs: - fastlane-plugin-ddg_apple_automation (0.1.0) + fastlane-plugin-ddg_apple_automation (0.2.0) GEM remote: https://rubygems.org/ diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile index 3f02b29113..cd249a9c24 100644 --- a/fastlane/Pluginfile +++ b/fastlane/Pluginfile @@ -2,4 +2,4 @@ # # Ensure this file is checked in to source control! -gem 'fastlane-plugin-ddg_apple_automation', git: 'https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation', tag: '0.1.0' +gem 'fastlane-plugin-ddg_apple_automation', git: 'https://github.com/duckduckgo/fastlane-plugin-ddg_apple_automation', tag: '0.2.0' From be5f2c88101e9782afa55fc3651b6bbf11be739b Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 28 Aug 2024 16:55:51 -0700 Subject: [PATCH 07/14] Allow using a production subscription with staging VPN environment (#3109) Task/Issue URL: https://app.asana.com/0/1193060753475688/1208066081988374/f Tech Design URL: CC: Description: This PR allows internal users to swap to the staging VPN environment with a production subscription. --- ...rkProtection+ConvenienceInitializers.swift | 10 ------ .../NetworkProtectionDebugMenu.swift | 36 +++++++++++++++---- .../VPNSettings+Environment.swift | 4 ++- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift index 2d57ecb38d..124e34cb37 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift @@ -38,16 +38,6 @@ extension NetworkProtectionDeviceManager { } } -extension NetworkProtectionCodeRedemptionCoordinator { - convenience init() { - let settings = Application.appDelegate.vpnSettings - self.init(environment: settings.selectedEnvironment, - tokenStore: NetworkProtectionKeychainTokenStore(), - errorEvents: .networkProtectionAppDebugEvents, - isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable) - } -} - extension NetworkProtectionKeychainTokenStore { convenience init() { self.init(isSubscriptionEnabled: DefaultSubscriptionFeatureAvailability().isFeatureAvailable) diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift index 46710a6d96..47ac1acf82 100644 --- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift +++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionDebugMenu.swift @@ -322,9 +322,11 @@ final class NetworkProtectionDebugMenu: NSMenu { private func populateNetworkProtectionEnvironmentListMenuItems() { environmentMenu.items = [ - NSMenuItem(title: "⚠️ The environment can be set in the Subscription > Environment menu", action: nil, target: nil), - NSMenuItem(title: "Production", action: nil, target: nil, keyEquivalent: ""), - NSMenuItem(title: "Staging", action: nil, target: nil, keyEquivalent: ""), + NSMenuItem(title: "⚠️ A staging subscription can be used for the staging VPN environment, a production subscription can be used for both", action: nil, target: nil), + NSMenuItem(title: "⚠️ Please restart the browser after changing environment", action: nil, target: nil), + NSMenuItem.separator(), + NSMenuItem(title: "Production", action: #selector(setSelectedEnvironment(_:)), target: self, keyEquivalent: ""), + NSMenuItem(title: "Staging", action: #selector(setSelectedEnvironment(_:)), target: self, keyEquivalent: ""), ] } @@ -407,10 +409,10 @@ final class NetworkProtectionDebugMenu: NSMenu { private func updateEnvironmentMenu() { let selectedEnvironment = settings.selectedEnvironment - guard environmentMenu.items.count == 3 else { return } + guard environmentMenu.items.count == 5 else { return } - environmentMenu.items[1].state = selectedEnvironment == .production ? .on: .off - environmentMenu.items[2].state = selectedEnvironment == .staging ? .on: .off + environmentMenu.items[3].state = selectedEnvironment == .production ? .on : .off + environmentMenu.items[4].state = selectedEnvironment == .staging ? .on : .off } private func updatePreferredServerMenu() { @@ -481,6 +483,28 @@ final class NetworkProtectionDebugMenu: NSMenu { @objc private func toggleExcludeDDGBrowser() { transparentProxySettings.toggleExclusion(for: ddgBrowserAppIdentifier) } + + // MARK: Environment + + @objc func setSelectedEnvironment(_ menuItem: NSMenuItem) { + let title = menuItem.title + let selectedEnvironment: VPNSettings.SelectedEnvironment + + if title == "Staging" { + selectedEnvironment = .staging + } else { + selectedEnvironment = .production + } + + settings.selectedEnvironment = selectedEnvironment + + Task { + _ = try await NetworkProtectionDeviceManager.create().refreshServerList() + try? await populateNetworkProtectionServerListMenuItems() + + settings.selectedServer = .automatic + } + } } #if DEBUG diff --git a/DuckDuckGo/Subscription/VPNSettings+Environment.swift b/DuckDuckGo/Subscription/VPNSettings+Environment.swift index 4e046637ed..cffa7251a6 100644 --- a/DuckDuckGo/Subscription/VPNSettings+Environment.swift +++ b/DuckDuckGo/Subscription/VPNSettings+Environment.swift @@ -26,8 +26,10 @@ public extension VPNSettings { func alignTo(subscriptionEnvironment: SubscriptionEnvironment) { switch subscriptionEnvironment.serviceEnvironment { case .production: - self.selectedEnvironment = .production + // Do nothing for a production subscription, as it can be used for both VPN environments. + break case .staging: + // If using a staging subscription, force the staging VPN environment as it is not compatible with anything else. self.selectedEnvironment = .staging } } From 3b474a0443dd1db6c9a53a44164f8cd83a641b6d Mon Sep 17 00:00:00 2001 From: Diego Rey Mendez Date: Thu, 29 Aug 2024 02:52:48 +0200 Subject: [PATCH 08/14] VPN Domain Exclusions pixel changes (#3103) Task/Issue URL: https://app.asana.com/0/0/1208044141877984/f BSK PR: duckduckgo/BrowserServicesKit#945 iOS PR: duckduckgo/iOS#3242 Description Adds engagement pixels for VPN domain exclusions. --- .../ExcludedDomainsModel.swift | 13 +++- .../ExcludedDomainsViewController.swift | 2 + .../NetworkProtectionMac/Package.swift | 13 ++++ LocalPackages/NetworkProtectionMac/README.md | 1 + .../SiteIssuesReporter.swift | 60 +++++++++++++++++ .../SiteTroubleshootingViewModel.swift | 21 +++++- .../DomainExclusionsEngagementPixel.swift | 64 +++++++++++++++++++ .../VPNPixels/SiteTroubleshootingPixel.swift | 52 +++++++++++++++ .../Sources/VPNPixels/VPNPixel.swift | 44 +++++++++++++ 9 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteIssuesReporter/SiteIssuesReporter.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/VPNPixels/DomainExclusionsEngagementPixel.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/VPNPixels/SiteTroubleshootingPixel.swift create mode 100644 LocalPackages/NetworkProtectionMac/Sources/VPNPixels/VPNPixel.swift diff --git a/DuckDuckGo/NetworkProtection/ExcludedDomains/ExcludedDomainsModel.swift b/DuckDuckGo/NetworkProtection/ExcludedDomains/ExcludedDomainsModel.swift index b3cbdbd75e..bd19fdd10b 100644 --- a/DuckDuckGo/NetworkProtection/ExcludedDomains/ExcludedDomainsModel.swift +++ b/DuckDuckGo/NetworkProtection/ExcludedDomains/ExcludedDomainsModel.swift @@ -18,18 +18,24 @@ import Foundation import NetworkProtectionProxy +import NetworkProtectionUI +import PixelKit protocol ExcludedDomainsViewModel { var domains: [String] { get } func add(domain: String) func remove(domain: String) + + func askUserToReportIssues(withDomain domain: String) } final class DefaultExcludedDomainsViewModel { let proxySettings = TransparentProxySettings(defaults: .netP) + private let pixelKit: PixelFiring? - init() { + init(pixelKit: PixelFiring? = PixelKit.shared) { + self.pixelKit = pixelKit } } @@ -51,4 +57,9 @@ extension DefaultExcludedDomainsViewModel: ExcludedDomainsViewModel { domain == cursor } } + + func askUserToReportIssues(withDomain domain: String) { + let siteIssuesReporter = SiteIssuesReporter(pixelKit: pixelKit) + siteIssuesReporter.askUserToReportIssues(withDomain: domain) + } } diff --git a/DuckDuckGo/NetworkProtection/ExcludedDomains/ExcludedDomainsViewController.swift b/DuckDuckGo/NetworkProtection/ExcludedDomains/ExcludedDomainsViewController.swift index 6192405f21..0806bd2621 100644 --- a/DuckDuckGo/NetworkProtection/ExcludedDomains/ExcludedDomainsViewController.swift +++ b/DuckDuckGo/NetworkProtection/ExcludedDomains/ExcludedDomainsViewController.swift @@ -117,6 +117,8 @@ final class ExcludedDomainsViewController: NSViewController { if let newRowIndex = allDomains.firstIndex(of: domain) { tableView.scrollRowToVisible(newRowIndex) } + + model.askUserToReportIssues(withDomain: domain) } @IBAction func removeSelectedDomain(_ sender: NSButton) { diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 0b727a7e12..22fe6c61c0 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -84,11 +84,24 @@ let package = Package( ] ), + // MARK: - VPNPixels + + .target( + name: "VPNPixels", + dependencies: [ + .product(name: "PixelKit", package: "BrowserServicesKit"), + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ] + ), + // MARK: - NetworkProtectionUI .target( name: "NetworkProtectionUI", dependencies: [ + "VPNPixels", .product(name: "NetworkProtection", package: "BrowserServicesKit"), .product(name: "PixelKit", package: "BrowserServicesKit"), .product(name: "SwiftUIExtensions", package: "SwiftUIExtensions"), diff --git a/LocalPackages/NetworkProtectionMac/README.md b/LocalPackages/NetworkProtectionMac/README.md index 49354a0caa..4fb9bba395 100644 --- a/LocalPackages/NetworkProtectionMac/README.md +++ b/LocalPackages/NetworkProtectionMac/README.md @@ -4,3 +4,4 @@ This package contains several components of VPN that are specific to macOS. - NetworkProtectionUI: UI elements used in the VPN that are reusable across build targets. - NetworkProtectionIPC: code to support IPC communication between VPN targets. +- VPNPixels: module for NetworkProtectionUI to have pixel support for shared UI components. diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteIssuesReporter/SiteIssuesReporter.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteIssuesReporter/SiteIssuesReporter.swift new file mode 100644 index 0000000000..51fead7b56 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteIssuesReporter/SiteIssuesReporter.swift @@ -0,0 +1,60 @@ +// +// SiteIssuesReporter.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKit +import Foundation +import PixelKit +import VPNPixels + +public struct SiteIssuesReporter { + + private let pixelKit: PixelFiring? + + public init(pixelKit: PixelFiring? = PixelKit.shared) { + self.pixelKit = pixelKit + } + + private func makeAlert(title: String, message: String? = nil, buttonNames: [String] = ["Ok"]) -> NSAlert{ + + let alert = NSAlert() + alert.messageText = title + + if let message = message { + alert.informativeText = message + } + + for buttonName in buttonNames { + alert.addButton(withTitle: buttonName) + } + alert.accessoryView = NSView(frame: NSRect(x: 0, y: 0, width: 200, height: 0)) + return alert + } + + public func askUserToReportIssues(withDomain domain: String) { + let alert = makeAlert(title: "Report Site Issues?", + message: "Help us improve by anonymously reporting that \(domain) doesn't work correctly through the VPN.", + buttonNames: ["Report", "Don't Report"]) + + let response = alert.runModal() + + if response == .alertFirstButtonReturn { + let pixel: SiteTroubleshootingPixel = .reportIssues(domain: domain) + pixelKit?.fire(pixel) + } + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingViewModel.swift b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingViewModel.swift index b026feed85..2ccc1d8a88 100644 --- a/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingViewModel.swift +++ b/LocalPackages/NetworkProtectionMac/Sources/NetworkProtectionUI/Views/SiteTroubleshootingView/SiteTroubleshootingViewModel.swift @@ -16,9 +16,12 @@ // limitations under the License. // +import AppKit import Combine import Foundation import NetworkProtection +import PixelKit +import VPNPixels extension SiteTroubleshootingView { @@ -42,14 +45,17 @@ extension SiteTroubleshootingView { } private let uiActionHandler: VPNUIActionHandling + private let pixelKit: PixelFiring? private var cancellables = Set() public init(featureFlagPublisher: AnyPublisher, connectionStatusPublisher: AnyPublisher, siteTroubleshootingInfoPublisher: AnyPublisher, - uiActionHandler: VPNUIActionHandling) { + uiActionHandler: VPNUIActionHandling, + pixelKit: PixelFiring? = PixelKit.shared) { self.uiActionHandler = uiActionHandler + self.pixelKit = pixelKit subscribeToConnectionStatusChanges(connectionStatusPublisher) subscribeToSiteTroubleshootingInfoChanges(siteTroubleshootingInfoPublisher) @@ -81,7 +87,20 @@ extension SiteTroubleshootingView { func setExclusion(_ exclude: Bool, forDomain domain: String) { Task { @MainActor in + guard let siteInfo, + siteInfo.excluded != exclude else { + return + } + + let engagementPixel = DomainExclusionsEngagementPixel(added: exclude) + pixelKit?.fire(engagementPixel) + await uiActionHandler.setExclusion(exclude, forDomain: domain) + + if exclude && true { + let siteIssuesReporter = SiteIssuesReporter(pixelKit: pixelKit) + siteIssuesReporter.askUserToReportIssues(withDomain: domain) + } } } } diff --git a/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/DomainExclusionsEngagementPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/DomainExclusionsEngagementPixel.swift new file mode 100644 index 0000000000..acc2ad9197 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/DomainExclusionsEngagementPixel.swift @@ -0,0 +1,64 @@ +// +// DomainExclusionsEngagementPixel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +/// Pixels to understand domain exclusion engagement +/// +public enum DomainExclusionsEngagementPixel: VPNPixel { + + /// One or more exclusions were added. + /// + case exclusionAdded + + /// One or more exclusions were removed. + /// + case exclusionRemoved + + public init(added: Bool) { + self = added ? .exclusionAdded : .exclusionRemoved + } + + // The name is provided by the convenience implementation in VPNPixel, + // so we don't need to implement it here. + // + // var name: String + + public var unscopedPixelName: String { + switch self { + case .exclusionAdded: + return "domain_exclusion_enabled" + case .exclusionRemoved: + return "domain_exclusion_disabled" + } + } + + public var parameters: [String: String]? { + switch self { + case .exclusionAdded: + return [:] + case .exclusionRemoved: + return [:] + } + } + + public var error: Error? { + return nil + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/SiteTroubleshootingPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/SiteTroubleshootingPixel.swift new file mode 100644 index 0000000000..7eeac03830 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/SiteTroubleshootingPixel.swift @@ -0,0 +1,52 @@ +// +// SiteTroubleshootingPixel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +/// Site troubleshooting pixels +/// +public enum SiteTroubleshootingPixel: VPNPixel { + + /// The user decided to report site issues. + /// + case reportIssues(domain: String) + + // The name is provided by the convenience implementation in VPNPixel, + // so we don't need to implement it here. + // + // var name: String + + public var unscopedPixelName: String { + switch self { + case .reportIssues: + return "report_site_issues" + } + } + + public var parameters: [String: String]? { + switch self { + case .reportIssues(let domain): + return [PixelKit.Parameters.domain: domain] + } + } + + public var error: Error? { + return nil + } +} diff --git a/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/VPNPixel.swift b/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/VPNPixel.swift new file mode 100644 index 0000000000..cdb906b760 --- /dev/null +++ b/LocalPackages/NetworkProtectionMac/Sources/VPNPixels/VPNPixel.swift @@ -0,0 +1,44 @@ +// +// VPNPixel.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import PixelKit + +/// The prefix that should be used for all pixels in this module +/// +let vpnPixelModulePrefix = "vpn" + +public protocol VPNPixel: PixelKitEventV2 { + + /// The name of the pixel without the module prefix. + /// + var unscopedPixelName: String { get } +} + +extension VPNPixel { + + /// Convenience method to provide the scoped pixel name. + /// + public var name: String { + scopedPixelName(forUnscopedPixelName: unscopedPixelName) + } + + func scopedPixelName(forUnscopedPixelName unscopedName: String) -> String { + "\(vpnPixelModulePrefix)_\(unscopedName)" + } +} From 32d0cc64ca46285070581ad33cc8729c8e5ad850 Mon Sep 17 00:00:00 2001 From: Dax the Duck Date: Thu, 29 Aug 2024 05:32:06 +0000 Subject: [PATCH 09/14] Bump version to 1.104.0 (251) --- Configuration/BuildNumber.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig index b71e79c75e..527880d7b3 100644 --- a/Configuration/BuildNumber.xcconfig +++ b/Configuration/BuildNumber.xcconfig @@ -1 +1 @@ -CURRENT_PROJECT_VERSION = 250 +CURRENT_PROJECT_VERSION = 251 From ec47366fb6f2d590640f3fcda8e6958cef35ff2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 13:47:56 +0200 Subject: [PATCH 10/14] Bump Submodules/privacy-reference-tests from `afb4f61` to `6133e7d` (#3166) --- Submodules/privacy-reference-tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Submodules/privacy-reference-tests b/Submodules/privacy-reference-tests index afb4f6128a..6133e7d9d9 160000 --- a/Submodules/privacy-reference-tests +++ b/Submodules/privacy-reference-tests @@ -1 +1 @@ -Subproject commit afb4f6128a3b50d53ddcb1897ea1fb4df6858aa1 +Subproject commit 6133e7d9d9cd5f1b925cab1971b4d785dc639df7 From 9a07b1b63839d29b863290b6e9a9467ac908b7c7 Mon Sep 17 00:00:00 2001 From: Anh Do <18567+quanganhdo@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:34:11 -0400 Subject: [PATCH 11/14] VPN location fallback (#3131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1205591970852438/1206773442486707/f Tech Design URL: CC: **Description:** The VPN geoswitching logic currently doesn’t handle the case that a location becomes unavailable. We should fall back to the country, if a city isn’t available, or nearest if the country isn’t available. **Steps to test this PR:** 1. Edit the VPNLocationViewModel to hardcode an extra a location that doesn’t exist and present it on the UI. 2. Select the location and confirm 3. Check that the fallback logic follow that mentioned in the description. Sample: ``` let data = "{\"country\":\"vn\",\"cities\":[{\"name\":\"Hanoi\"}]}".utf8data let fakeLocation = try! JSONDecoder().decode(NetworkProtectionLocation.self, from: data) locations.append(fakeLocation) ``` **Definition of Done**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? --- ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- .../NetworkExtensionTargets/MacPacketTunnelProvider.swift | 2 +- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 4218b95de4..a6ce24d624 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13598,7 +13598,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 188.1.0; + version = 189.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 396e8be9f2..e2326fabcc 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "ce1b7228a38d2b18525590256051a012109cfee6", - "version" : "188.1.0" + "revision" : "1ee09794449969fe1f6bc73a43199ef711fb7c44", + "version" : "189.0.0" } }, { diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift index e8c83fb1ce..bec44225be 100644 --- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift +++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift @@ -54,7 +54,7 @@ final class MacPacketTunnelProvider: PacketTunnelProvider { // MARK: - Error Reporting - private static func networkProtectionDebugEvents(controllerErrorStore: NetworkProtectionTunnelErrorStore) -> EventMapping? { + private static func networkProtectionDebugEvents(controllerErrorStore: NetworkProtectionTunnelErrorStore) -> EventMapping { return EventMapping { event, _, _, _ in let domainEvent: NetworkProtectionPixelEvent #if DEBUG diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index b13d7fd5d7..78d25d24fd 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: "188.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "189.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 22fe6c61c0..6f033259c2 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "188.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "189.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index bee1dc2536..5b0cd8161f 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "188.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "189.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From 0394324b7b37253b98fd2e5d1bad9eb45c443fae Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Thu, 29 Aug 2024 07:45:49 -0700 Subject: [PATCH 12/14] Update CI test report to avoid reporting successful retries as failures (#3165) Task/Issue URL: https://app.asana.com/0/1203301625297703/1208174218141374/f Tech Design URL: CC: Description: This PR updates the test report action to set check_retries to true, which will avoid reporting a failure when a test fails and then succeeds upon a retry. --- .github/workflows/pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index e606637993..f23069962d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -297,6 +297,7 @@ jobs: with: check_name: "Test Report: ${{ matrix.flavor }}" report_paths: '${{ matrix.flavor }}*.xml' + check_retries: true - name: Update Asana with failed unit tests if: always() # always run even if the previous step fails From b8bf0f189703d501a7f952a6675b2ac28f258056 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Thu, 29 Aug 2024 16:43:52 +0100 Subject: [PATCH 13/14] Update C-S-S for DuckPlayer onboarding (#3169) Task/Issue URL: https://app.asana.com/0/0/1208179338963311/f **Description**: Add support for DuckPlayer onboarding on C-S-S --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 10 +++++----- LocalPackages/DataBrokerProtection/Package.swift | 2 +- LocalPackages/NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a6ce24d624..760b110967 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -13598,7 +13598,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 189.0.0; + version = 189.1.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e2326fabcc..5db77bbfc3 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "1ee09794449969fe1f6bc73a43199ef711fb7c44", - "version" : "189.0.0" + "revision" : "65bfdadf4150e89f8bac3a53fe949fad711649cb", + "version" : "189.1.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "f97053d24c21ea301d4067adbbe0899ff940526a", - "version" : "6.7.0" + "revision" : "799aae6d7ee4031a377ad891a549b66abf29e83c", + "version" : "6.11.0" } }, { @@ -75,7 +75,7 @@ { "identity" : "lottie-spm", "kind" : "remoteSourceControl", - "location" : "https://github.com/airbnb/lottie-spm.git", + "location" : "https://github.com/airbnb/lottie-spm", "state" : { "revision" : "1d29eccc24cc8b75bff9f6804155112c0ffc9605", "version" : "4.4.3" diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 78d25d24fd..e34d9922c4 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: "189.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "189.1.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 6f033259c2..9f49b52da9 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "189.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "189.1.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index 5b0cd8161f..d5137c0d14 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "189.0.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "189.1.0"), .package(path: "../SwiftUIExtensions") ], targets: [ From e54dd42751a81e1a91dc9f5a2f3c09eadc2c89f3 Mon Sep 17 00:00:00 2001 From: Sabrina Tardio <44158575+SabrinaTardio@users.noreply.github.com> Date: Thu, 29 Aug 2024 21:34:21 +0200 Subject: [PATCH 14/14] Update SLL Error (#3089) Task/Issue URL: https://app.asana.com/0/1204186595873227/1208016223626978/f Updates to use the new C-S-S special error page for SSL errors --- DuckDuckGo.xcodeproj/project.pbxproj | 66 +- .../xcshareddata/swiftpm/Package.resolved | 8 +- DuckDuckGo/Common/Localizables/UserText.swift | 33 - .../ErrorPage/ErrorPageHTMLTemplate.swift | 127 -- .../ErrorPage/SSLErrorPageUserScript.swift | 77 -- DuckDuckGo/Localizable.xcstrings | 1176 ----------------- .../SpecialPagesUserScriptExtension.swift | 7 +- ...ift => SpecialErrorPageTabExtension.swift} | 58 +- .../SpecialErrorPageUserScriptExtension.swift | 34 + .../Tab/TabExtensions/TabExtensions.swift | 2 +- DuckDuckGo/Tab/UserScripts/UserScripts.swift | 11 +- .../YoutubePlayer/DuckURLSchemeHandler.swift | 3 +- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- .../DuckSchemeHandlerTests.swift | 21 +- .../ErrorPageTabExtensionTest.swift | 64 +- .../SpecialErrorPageUserScriptTests.swift | 49 + .../SSLErrorPageUserScriptTests.swift | 122 -- 19 files changed, 223 insertions(+), 1641 deletions(-) delete mode 100644 DuckDuckGo/ErrorPage/SSLErrorPageUserScript.swift rename DuckDuckGo/Tab/TabExtensions/{SSLErrorPageTabExtension.swift => SpecialErrorPageTabExtension.swift} (73%) create mode 100644 DuckDuckGo/Tab/TabExtensions/SpecialErrorPageUserScriptExtension.swift create mode 100644 UnitTests/TabExtensionsTests/SpecialErrorPageUserScriptTests.swift delete mode 100644 UnitTests/UserScripts/SSLErrorPageUserScriptTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 760b110967..1c924956d6 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1463,6 +1463,13 @@ 566B736A2BECC02D00FF1959 /* SyncAlertsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B73682BECBF8400FF1959 /* SyncAlertsPresenter.swift */; }; 566B736C2BECC3C600FF1959 /* SyncPausedStateManaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B736B2BECC3C600FF1959 /* SyncPausedStateManaging.swift */; }; 566B736D2BECC3C600FF1959 /* SyncPausedStateManaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566B736B2BECC3C600FF1959 /* SyncPausedStateManaging.swift */; }; + 567A23BE2C7F539C0010F66C /* SpecialErrorPageUserScriptExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A23BD2C7F539C0010F66C /* SpecialErrorPageUserScriptExtension.swift */; }; + 567A23BF2C7F539C0010F66C /* SpecialErrorPageUserScriptExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A23BD2C7F539C0010F66C /* SpecialErrorPageUserScriptExtension.swift */; }; + 567A23C12C7F71570010F66C /* SpecialErrorPages in Frameworks */ = {isa = PBXBuildFile; productRef = 567A23C02C7F71570010F66C /* SpecialErrorPages */; }; + 567A23C52C7F75BB0010F66C /* SpecialErrorPages in Frameworks */ = {isa = PBXBuildFile; productRef = 567A23C42C7F75BB0010F66C /* SpecialErrorPages */; }; + 567A23CD2C80CE060010F66C /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A23CC2C80CE060010F66C /* SpecialErrorPageUserScriptTests.swift */; }; + 567A23CE2C80CF3D0010F66C /* SpecialErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567A23CC2C80CE060010F66C /* SpecialErrorPageUserScriptTests.swift */; }; + 567A23CF2C80CF4B0010F66C /* ErrorPageTabExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */; }; 567DA93F29E8045D008AC5EE /* MockEmailStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567DA93E29E8045D008AC5EE /* MockEmailStorage.swift */; }; 567DA94029E8045D008AC5EE /* MockEmailStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567DA93E29E8045D008AC5EE /* MockEmailStorage.swift */; }; 567DA94529E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567DA94429E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift */; }; @@ -1515,14 +1522,9 @@ 56AC09C82C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC09C62C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift */; }; 56B234BF2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */; }; 56B234C02A84EFD800F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */; }; - 56BA1E752BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */; }; - 56BA1E762BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */; }; - 56BA1E7F2BAB2D29001CF69F /* ErrorPageTabExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */; }; + 56BA1E752BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E742BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift */; }; + 56BA1E762BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E742BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift */; }; 56BA1E802BAB2E43001CF69F /* ErrorPageTabExtensionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */; }; - 56BA1E822BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */; }; - 56BA1E832BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */; }; - 56BA1E872BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */; }; - 56BA1E882BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */; }; 56BA1E8A2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; 56BA1E8B2BB1CB5B001CF69F /* CertificateTrustEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */; }; 56CE77612C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */; }; @@ -3518,6 +3520,8 @@ 566B196029CDB7C9007E38F4 /* CapturingOptionsButtonMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturingOptionsButtonMenuDelegate.swift; sourceTree = ""; }; 566B73682BECBF8400FF1959 /* SyncAlertsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncAlertsPresenter.swift; sourceTree = ""; }; 566B736B2BECC3C600FF1959 /* SyncPausedStateManaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPausedStateManaging.swift; sourceTree = ""; }; + 567A23BD2C7F539C0010F66C /* SpecialErrorPageUserScriptExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptExtension.swift; sourceTree = ""; }; + 567A23CC2C80CE060010F66C /* SpecialErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageUserScriptTests.swift; sourceTree = ""; }; 567DA93E29E8045D008AC5EE /* MockEmailStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockEmailStorage.swift; sourceTree = ""; }; 567DA94429E95C3F008AC5EE /* YoutubeOverlayUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YoutubeOverlayUserScriptTests.swift; sourceTree = ""; }; 5681ED3F2BDB955100F59729 /* SyncCredentialsAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncCredentialsAdapterTests.swift; sourceTree = ""; }; @@ -3544,10 +3548,8 @@ 56A054522C2592CE007D8FAB /* OnboardingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingUITests.swift; sourceTree = ""; }; 56AC09C62C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksBarViewControllerTests.swift; sourceTree = ""; }; 56B234BE2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarUrlExtensionsTests.swift; sourceTree = ""; }; - 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageTabExtension.swift; sourceTree = ""; }; + 56BA1E742BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialErrorPageTabExtension.swift; sourceTree = ""; }; 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPageTabExtensionTest.swift; sourceTree = ""; }; - 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageUserScript.swift; sourceTree = ""; }; - 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLErrorPageUserScriptTests.swift; sourceTree = ""; }; 56BA1E892BB1CB5B001CF69F /* CertificateTrustEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateTrustEvaluator.swift; sourceTree = ""; }; 56CE77602C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestedSearchesProviderTests.swift; sourceTree = ""; }; 56CEE9092B7A66C500CF10AA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -4409,6 +4411,7 @@ 371209252C232E6C003ADF3D /* RemoteMessaging in Frameworks */, 4BCBE45A2BA7E17800FC75A1 /* Subscription in Frameworks */, B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */, + 567A23C52C7F75BB0010F66C /* SpecialErrorPages in Frameworks */, 372217822B33380700B8E9C2 /* TestUtils in Frameworks */, 3706FCAA293F65D500E42796 /* UserScript in Frameworks */, 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */, @@ -4603,6 +4606,7 @@ 560EB9352C7897370080DBC8 /* Onboarding in Frameworks */, 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */, 1EA7B8D32B7E078C000330A4 /* SubscriptionUI in Frameworks */, + 567A23C12C7F71570010F66C /* SpecialErrorPages in Frameworks */, B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */, EE7295E32A545B9A008C0991 /* NetworkProtection in Frameworks */, 9807F645278CA16F00E1547B /* BrowserServicesKit in Frameworks */, @@ -6181,14 +6185,6 @@ path = DuckSchemeHandler; sourceTree = ""; }; - 56BA1E852BAC820D001CF69F /* UserScripts */ = { - isa = PBXGroup; - children = ( - 56BA1E862BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift */, - ); - path = UserScripts; - sourceTree = ""; - }; 7B1E819A27C8874900FF0E60 /* Autofill */ = { isa = PBXGroup; children = ( @@ -7029,7 +7025,6 @@ isa = PBXGroup; children = ( 56A054392C20876F007D8FAB /* DuckSchemeHandler */, - 56BA1E852BAC820D001CF69F /* UserScripts */, C13909F22B85FD60001626ED /* Autofill */, 5629846D2AC460DF00AC20EB /* Sync */, B6A5A28C25B962CB00AA7ADA /* App */, @@ -8148,10 +8143,11 @@ B66260DC29AC5D4300E9E3EE /* NavigationProtectionTabExtension.swift */, B6F1B0292BCE675C005E863C /* NetworkProtectionControllerTabExtension.swift */, B6BF5D842946FFDA006742B1 /* PrivacyDashboardTabExtension.swift */, - 56BA1E742BAAF70F001CF69F /* SSLErrorPageTabExtension.swift */, + 56BA1E742BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift */, B6BDDA002942389000F68088 /* TabExtensions.swift */, 1D9A4E592B43213B00F449E2 /* TabSnapshotExtension.swift */, B626A76C29928B1600053070 /* TestsClosureNavigationResponder.swift */, + 567A23BD2C7F539C0010F66C /* SpecialErrorPageUserScriptExtension.swift */, ); path = TabExtensions; sourceTree = ""; @@ -8229,7 +8225,6 @@ isa = PBXGroup; children = ( B684121B2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift */, - 56BA1E812BAC506F001CF69F /* SSLErrorPageUserScript.swift */, ); path = ErrorPage; sourceTree = ""; @@ -8412,6 +8407,7 @@ 1D1C36E529FB019C001FA40C /* HistoryTabExtensionTests.swift */, 1D8C2FE42B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift */, 56BA1E7C2BAB290E001CF69F /* ErrorPageTabExtensionTest.swift */, + 567A23CC2C80CE060010F66C /* SpecialErrorPageUserScriptTests.swift */, ); path = TabExtensionsTests; sourceTree = ""; @@ -8773,6 +8769,7 @@ 9D9DE5742C63AA0C00D20B15 /* AppKitExtensions */, 560EB9362C78974C0080DBC8 /* Onboarding */, C18BF9CD2C73678C00ED6B8A /* Freemium */, + 567A23C42C7F75BB0010F66C /* SpecialErrorPages */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -9191,6 +9188,7 @@ 9D9DE5722C63AA0700D20B15 /* AppKitExtensions */, 560EB9342C7897370080DBC8 /* Onboarding */, C18BF9CB2C73678500ED6B8A /* Freemium */, + 567A23C02C7F71570010F66C /* SpecialErrorPages */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -10248,8 +10246,8 @@ 3706FB35293F65D500E42796 /* FlatButton.swift in Sources */, 3706FB36293F65D500E42796 /* PinnedTabView.swift in Sources */, 3706FB37293F65D500E42796 /* DataEncryption.swift in Sources */, + 56BA1E762BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift in Sources */, BD7090D32C52ECFE009EED82 /* UnifiedMetadataCollector.swift in Sources */, - 56BA1E762BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */, 4B9DB0362A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */, 37197EA82942443D00394917 /* BrowserTabViewController.swift in Sources */, 3706FB39293F65D500E42796 /* PrivacyDashboardPopover.swift in Sources */, @@ -10513,6 +10511,7 @@ 3706FBDE293F65D500E42796 /* EncryptionKeyStoring.swift in Sources */, EEE50C2A2C38249C003DD7FF /* OptionalExtension.swift in Sources */, 4B4D60E32A0C883A00BCD287 /* AppMain.swift in Sources */, + 567A23BF2C7F539C0010F66C /* SpecialErrorPageUserScriptExtension.swift in Sources */, 37197EA12942441700394917 /* Tab+UIDelegate.swift in Sources */, 56D6A3D729DB2BAB0055215A /* ContinueSetUpView.swift in Sources */, 4BF97ADD2B43C5FC00EB4240 /* KeychainType+ClientDefault.swift in Sources */, @@ -10644,7 +10643,6 @@ 3706FC30293F65D500E42796 /* FireInfoViewController.swift in Sources */, B6F1B02F2BCE6B47005E863C /* TunnelControllerProvider.swift in Sources */, 31A83FB62BE28D7D00F74E67 /* UserText+DBP.swift in Sources */, - 56BA1E832BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */, 3706FC31293F65D500E42796 /* PermissionButton.swift in Sources */, 9F6434622BEC82B700D2D8A0 /* AttributionPixelHandler.swift in Sources */, 3706FC32293F65D500E42796 /* MoreOptionsMenu.swift in Sources */, @@ -10897,7 +10895,6 @@ 56CE77622C7DFCF800AC1ED2 /* OnboardingSuggestedSearchesProviderTests.swift in Sources */, 3706FE05293F661700E42796 /* DownloadsWebViewMock.m in Sources */, 3706FE06293F661700E42796 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */, - 56BA1E7F2BAB2D29001CF69F /* ErrorPageTabExtensionTest.swift in Sources */, 3706FE07293F661700E42796 /* PasswordManagementItemListModelTests.swift in Sources */, 3706FE08293F661700E42796 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, 3706FE09293F661700E42796 /* VariantManagerTests.swift in Sources */, @@ -10936,6 +10933,7 @@ 857E44642A9F70F200ED77A7 /* CampaignVariantTests.swift in Sources */, 561D29C72BDA74F4007B91D0 /* MockDDGSyncing.swift in Sources */, 3706FE1E293F661700E42796 /* GeolocationProviderTests.swift in Sources */, + 567A23CE2C80CF3D0010F66C /* SpecialErrorPageUserScriptTests.swift in Sources */, 9F39106A2B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */, 9FAD623B2BCFDB32007F3A65 /* WebsiteInfoHelpers.swift in Sources */, 56A054312C2043C8007D8FAB /* OnboardingTabExtensionTests.swift in Sources */, @@ -10966,6 +10964,7 @@ 562532A12BC069190034D316 /* ZoomPopoverViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, + 567A23CF2C80CF4B0010F66C /* ErrorPageTabExtensionTest.swift in Sources */, 37DB56F02C3B31CD0093D4DC /* MockRemoteMessagingAvailabilityProvider.swift in Sources */, 1D8C2FEB2B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift in Sources */, 56A0540E2C1C375E007D8FAB /* MockWindow.swift in Sources */, @@ -11108,7 +11107,6 @@ 3706FE79293F661700E42796 /* AppearancePreferencesTests.swift in Sources */, 3706FE7A293F661700E42796 /* FirePopoverViewModelTests.swift in Sources */, 7B09CBAA2BA4BE8200CF245B /* NetworkProtectionPixelEventTests.swift in Sources */, - 56BA1E882BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */, 3706FE7B293F661700E42796 /* HistoryStoringMock.swift in Sources */, 562984702AC4610100AC20EB /* SyncPreferencesTests.swift in Sources */, 3706FE7C293F661700E42796 /* LocalBookmarkStoreTests.swift in Sources */, @@ -11538,6 +11536,7 @@ 31F28C4F28C8EEC500119F70 /* YoutubePlayerUserScript.swift in Sources */, 4B41EDAE2B168AFF001EEDF4 /* UnifiedFeedbackFormViewController.swift in Sources */, 7BFF35732C10D75000F89673 /* IPCServiceLauncher.swift in Sources */, + 567A23BE2C7F539C0010F66C /* SpecialErrorPageUserScriptExtension.swift in Sources */, AA5FA697275F90C400DCE9C9 /* FaviconImageCache.swift in Sources */, 1430DFF524D0580F00B8978C /* TabBarViewController.swift in Sources */, B62B483E2ADE48DE000DECE5 /* ArrayBuilder.swift in Sources */, @@ -11906,7 +11905,7 @@ 4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */, 7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */, B6085D092743AAB600A9C456 /* FireproofDomains.xcdatamodeld in Sources */, - 56BA1E752BAAF70F001CF69F /* SSLErrorPageTabExtension.swift in Sources */, + 56BA1E752BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift in Sources */, 85589E8227BBB8630038AD11 /* HomePageView.swift in Sources */, B6BF5D932947199A006742B1 /* SerpHeadersNavigationResponder.swift in Sources */, 569277C129DDCBB500B633EF /* HomePageContinueSetUpModel.swift in Sources */, @@ -11989,7 +11988,6 @@ B66260E629ACAE4B00E9E3EE /* NavigationHotkeyHandler.swift in Sources */, EA0BA3A9272217E6002A0B6C /* ClickToLoadUserScript.swift in Sources */, AAA892EA250A4CEF005B37B2 /* WindowControllersManager.swift in Sources */, - 56BA1E822BAC506F001CF69F /* SSLErrorPageUserScript.swift in Sources */, 85C5991B27D10CF000E605B2 /* FireAnimationView.swift in Sources */, B6B4D1CA2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */, AA6197C4276B314D008396F0 /* FaviconUrlReference.swift in Sources */, @@ -12298,7 +12296,6 @@ B67C6C472654C643006C872E /* FileManagerExtensionTests.swift in Sources */, B69B50482726C5C200758A2B /* StatisticsLoaderTests.swift in Sources */, 9F6434702BECBA2800D2D8A0 /* SubscriptionRedirectManagerTests.swift in Sources */, - 56BA1E872BAC8239001CF69F /* SSLErrorPageUserScriptTests.swift in Sources */, 142879DA24CE1179005419BB /* SuggestionViewModelTests.swift in Sources */, 4B9292BC2667103100AD2C21 /* BookmarkSidebarTreeControllerTests.swift in Sources */, 4B9DB05A2A983B55000927DB /* MockWaitlistRequest.swift in Sources */, @@ -12525,6 +12522,7 @@ AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */, 1DFAB5222A8983DE00A0F7F6 /* SetExtensionTests.swift in Sources */, 4BF6962028BEEE8B00D402D4 /* LocalPinningManagerTests.swift in Sources */, + 567A23CD2C80CE060010F66C /* SpecialErrorPageUserScriptTests.swift in Sources */, 9F0FFFB82BCCAE9C007C87DD /* AddEditBookmarkDialogViewModelMock.swift in Sources */, AAEC74B82642E43800C2EFBC /* HistoryStoreTests.swift in Sources */, 9FAD623D2BD09DE5007F3A65 /* WebsiteInfoTests.swift in Sources */, @@ -13598,7 +13596,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 189.1.0; + version = 190.0.0; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { @@ -13939,6 +13937,16 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Onboarding; }; + 567A23C02C7F71570010F66C /* SpecialErrorPages */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = SpecialErrorPages; + }; + 567A23C42C7F75BB0010F66C /* SpecialErrorPages */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = SpecialErrorPages; + }; 7B00997C2B6508B700FE7C31 /* NetworkProtectionProxy */ = { isa = XCSwiftPackageProductDependency; productName = NetworkProtectionProxy; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5db77bbfc3..6c396add94 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "65bfdadf4150e89f8bac3a53fe949fad711649cb", - "version" : "189.1.0" + "revision" : "ac53011582abcca4aefd66f15308332273eecb49", + "version" : "190.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "799aae6d7ee4031a377ad891a549b66abf29e83c", - "version" : "6.11.0" + "revision" : "5876a5d2e2e7f5a2e11f6419c6c3fafb7cafdfca", + "version" : "6.12.0" } }, { diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 0378703b02..21443b6a1a 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -229,40 +229,7 @@ struct UserText { static let errorPageHeader = NSLocalizedString("page.error.header", value: "DuckDuckGo can’t load this page.", comment: "Error page heading text") static let webProcessCrashPageHeader = NSLocalizedString("page.crash.header", value: "This webpage has crashed.", comment: "Error page heading text shown when a Web Page process had crashed") static let webProcessCrashPageMessage = NSLocalizedString("page.crash.message", value: "Try reloading the page or come back later.", comment: "Error page message text shown when a Web Page process had crashed") - static let sslErrorPageHeader = NSLocalizedString("ssl.error.page.header", value: "Warning: This site may be insecure", comment: "Title shown in an error page that warn users of security risks on a website due to SSL issues") static let sslErrorPageTabTitle = NSLocalizedString("ssl.error.page.tab.title", value: "Warning: Site May Be Insecure", comment: "Title shown in an error page tab that warn users of security risks on a website due to SSL issues") - static func sslErrorPageBody(_ domain: String) -> String { - let localized = NSLocalizedString("ssl.error.page.body", - value: "The certificate for this site is invalid. You might be connecting to a server that is pretending to be %1$@ which could put your confidential information at risk.", - comment: "Error description shown in an error page that warns users of security risks on a website due to SSL issues. %1$@ represent the site domain.") - return String(format: localized, domain) - } - static let sslErrorPageAdvancedButton = NSLocalizedString("ssl.error.page.advanced.button", value: "Advanced…", comment: "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to see advanced options on click.") - static let sslErrorPageLeaveSiteButton = NSLocalizedString("ssl.error.page.leave.site.button", value: "Leave This Site", comment: "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to leave the website and navigate to previous page.") - static let sslErrorPageVisitSiteButton = NSLocalizedString("ssl.error.page.visit.site.button", value: "Accept Risk and Visit Site", comment: "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to visit the website anyway despite the risks.") - static let sslErrorAdvancedInfoTitle = NSLocalizedString("ssl.error.page.advanced.info.title", value: "DuckDuckGo warns you when a website has an invalid certificate.", comment: "Title of the Advanced info section shown in an error page that warns users of security risks on a website due to SSL issues.") - static let sslErrorAdvancedInfoBodyWrongHost = NSLocalizedString("ssl.error.page.advanced.info.body.wrong.host", value: "It’s possible that the website is misconfigured or that an attacker has compromised your connection.", comment: "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.") - static let sslErrorAdvancedInfoBodyExpired = NSLocalizedString("ssl.error.page.advanced.info.body.expired", value: "It’s possible that the website is misconfigured, that an attacker has compromised your connection, or that your system clock is incorrect.", comment: "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.") - static func sslErrorCertificateExpiredMessage(_ domain: String) -> String { - let localized = NSLocalizedString("ssl.error.certificate.expired.message", - value: "The security certificate for %1$@ is expired.", - comment: "Describes an SSL error where a website's security certificate is expired. '%1$@' is a placeholder for the website's domain.") - return String(format: localized, domain) - } - static func sslErrorCertificateWrongHostMessage(_ domain: String, eTldPlus1: String) -> String { - let localized = NSLocalizedString("ssl.error.wrong.host.message", - value: "The security certificate for %1$@ does not match *.%2$@.", - comment: "Explains an SSL error when a site's certificate doesn't match its domain. '%1$@' is the site's domain.") - return String(format: localized, domain, eTldPlus1) - } - static func sslErrorCertificateSelfSignedMessage(_ domain: String) -> String { - let localized = NSLocalizedString("ssl.error.self.signed.message", - value: "The security certificate for %1$@ is not trusted by your device's operating system.", - comment: "Warns the user that the site's security certificate is self-signed and not trusted. '%1$@' is the site's domain.") - return String(format: localized, domain) - } - - static let openSystemPreferences = NSLocalizedString("open.preferences", value: "Open System Preferences", comment: "Open System Preferences (to re-enable permission for the App) (up to and including macOS 12") static let openSystemSettings = NSLocalizedString("open.settings", value: "Open System Settings…", comment: "This string represents a prompt or button label prompting the user to open system settings") diff --git a/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift b/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift index a288139546..3a51ebb3b3 100644 --- a/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift +++ b/DuckDuckGo/ErrorPage/ErrorPageHTMLTemplate.swift @@ -44,130 +44,3 @@ struct ErrorPageHTMLTemplate { } } - -struct SSLErrorPageHTMLTemplate { - let domain: String - let errorCode: Int - let tld = TLD() - - static var htmlTemplatePath: String { - guard let file = ContentScopeScripts.Bundle.path(forResource: "index", ofType: "html", inDirectory: "pages/sslerrorpage") else { - assertionFailure("HTML template not found") - return "" - } - return file - } - - func makeHTMLFromTemplate() -> String { - let sslError = SSLErrorType.forErrorCode(errorCode) - guard let html = try? String(contentsOfFile: Self.htmlTemplatePath) else { - assertionFailure("Should be able to load template") - return "" - } - let eTldPlus1 = tld.eTLDplus1(domain) ?? domain - let loadTimeData = createJSONString(header: sslError.header, body: sslError.body(for: domain), advancedButton: sslError.advancedButton, leaveSiteButton: sslError.leaveSiteButton, advancedInfoHeader: sslError.advancedInfoTitle, specificMessage: sslError.specificMessage(for: domain, eTldPlus1: eTldPlus1), advancedInfoBody: sslError.advancedInfoBody, visitSiteButton: sslError.visitSiteButton) - return html.replacingOccurrences(of: "$LOAD_TIME_DATA$", with: loadTimeData, options: .literal) - } - - private func createJSONString(header: String, body: String, advancedButton: String, leaveSiteButton: String, advancedInfoHeader: String, specificMessage: String, advancedInfoBody: String, visitSiteButton: String) -> String { - let innerDictionary: [String: Any] = [ - "header": header.escapedUnicodeHtmlString(), - "body": body.escapedUnicodeHtmlString(), - "advancedButton": advancedButton.escapedUnicodeHtmlString(), - "leaveSiteButton": leaveSiteButton.escapedUnicodeHtmlString(), - "advancedInfoHeader": advancedInfoHeader.escapedUnicodeHtmlString(), - "specificMessage": specificMessage.escapedUnicodeHtmlString(), - "advancedInfoBody": advancedInfoBody.escapedUnicodeHtmlString(), - "visitSiteButton": visitSiteButton.escapedUnicodeHtmlString() - ] - - let outerDictionary: [String: Any] = [ - "strings": innerDictionary - ] - - do { - let jsonData = try JSONSerialization.data(withJSONObject: outerDictionary, options: .prettyPrinted) - if let jsonString = String(data: jsonData, encoding: .utf8) { - return jsonString - } else { - return "Error: Could not encode jsonData to String." - } - } catch { - return "Error: \(error.localizedDescription)" - } - } - -} - -public enum SSLErrorType { - case expired - case wrongHost - case selfSigned - case invalid - - var header: String { - return UserText.sslErrorPageHeader - } - - func body(for domain: String) -> String { - let boldDomain = "\(domain)" - return UserText.sslErrorPageBody(boldDomain) - } - - var advancedButton: String { - return UserText.sslErrorPageAdvancedButton - } - - var leaveSiteButton: String { - return UserText.sslErrorPageLeaveSiteButton - } - - var visitSiteButton: String { - return UserText.sslErrorPageVisitSiteButton - } - - var advancedInfoTitle: String { - return UserText.sslErrorAdvancedInfoTitle - } - - var advancedInfoBody: String { - switch self { - case .expired: - return UserText.sslErrorAdvancedInfoBodyExpired - case .wrongHost: - return UserText.sslErrorAdvancedInfoBodyWrongHost - case .selfSigned: - return UserText.sslErrorAdvancedInfoBodyWrongHost - case .invalid: - return UserText.sslErrorAdvancedInfoBodyWrongHost - } - } - - func specificMessage(for domain: String, eTldPlus1: String) -> String { - let boldDomain = "\(domain)" - let boldETldPlus1 = "\(eTldPlus1)" - switch self { - case .expired: - return UserText.sslErrorCertificateExpiredMessage(boldDomain) - case .wrongHost: - return UserText.sslErrorCertificateWrongHostMessage(boldDomain, eTldPlus1: boldETldPlus1) - case .selfSigned: - return UserText.sslErrorCertificateSelfSignedMessage(boldDomain) - case .invalid: - return UserText.sslErrorCertificateSelfSignedMessage(boldDomain) - } - } - - static func forErrorCode(_ errorCode: Int) -> Self { - switch Int32(errorCode) { - case errSSLCertExpired: - return .expired - case errSSLHostNameMismatch: - return .wrongHost - case errSSLXCertChainInvalid: - return .selfSigned - default: - return .invalid - } - } -} diff --git a/DuckDuckGo/ErrorPage/SSLErrorPageUserScript.swift b/DuckDuckGo/ErrorPage/SSLErrorPageUserScript.swift deleted file mode 100644 index 1f3487834e..0000000000 --- a/DuckDuckGo/ErrorPage/SSLErrorPageUserScript.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// SSLErrorPageUserScript.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import UserScript - -final class SSLErrorPageUserScript: NSObject, Subfeature { - enum MessageName: String, CaseIterable { - case leaveSite - case visitSite - } - - public let messageOriginPolicy: MessageOriginPolicy = .all - public let featureName: String = "sslErrorPage" - - var isEnabled: Bool = false - var failingURL: URL? - - weak var broker: UserScriptMessageBroker? - weak var delegate: SSLErrorPageUserScriptDelegate? - - func with(broker: UserScriptMessageBroker) { - self.broker = broker - } - - @MainActor - func handler(forMethodNamed methodName: String) -> Subfeature.Handler? { - guard isEnabled else { return nil } - switch MessageName(rawValue: methodName) { - case .leaveSite: - return handleLeaveSiteAction - case .visitSite: - return handleVisitSiteAction - default: - assertionFailure("SSLErrorPageUserScript: Failed to parse User Script message: \(methodName)") - return nil - } - } - - @MainActor - func handleLeaveSiteAction(params: Any, message: UserScriptMessage) -> Encodable? { - delegate?.leaveSite() - return nil - } - - @MainActor - func handleVisitSiteAction(params: Any, message: UserScriptMessage) -> Encodable? { - delegate?.visitSite() - return nil - } - - // MARK: - UserValuesNotification - - struct UserValuesNotification: Encodable { - let userValuesNotification: UserValues - } -} - -protocol SSLErrorPageUserScriptDelegate: AnyObject { - func leaveSite() - func visitSite() -} diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 374d3ae6f2..8cd2dcad72 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -6255,126 +6255,6 @@ } } }, - "autofill.usernames-and-passwords" : { - "comment" : "Autofill autosaved data type", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Benutzernamen und Passwörter" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Usernames and passwords" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nombres de usuario y contraseñas" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Noms d'utilisateur et mots de passe" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nome utente e password" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gebruikersnamen en wachtwoorden" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nazwy użytkowników i hasła" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nomes de utilizador e palavras-passe" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Имена пользователей и пароли" - } - } - } - }, - "autofill.view-autofill-content" : { - "comment" : "View Autofill Content Button name in the autofill settings", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Autovervollständigen-Inhalte anzeigen" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "View Autofill Content…" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver contenido de autocompletar..." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Afficher le contenu de saisie automatique…" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Visualizza compilazione automatica del contenuto..." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inhoud voor automatisch invullen bekijken ..." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wyświetl zawartość autouzupełniania…" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ver conteúdo de preenchimento automático…" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Просмотреть данные автозаполнения..." - } - } - } - }, "autofill.view-autofill-content.identities" : { "comment" : "View Identities Content Button title in the autofill Settings", "extractionState" : "extracted_with_value", @@ -18521,66 +18401,6 @@ } } }, - "duck-player.autoplay-title" : { - "comment" : "Autoplay title in settings", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisches Abspielen" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Autoplay" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reproducción automática" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lecture automatique" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Riproduzione automatica" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisch afspelen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatyczne odtwarzanie" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reprodução automática" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автовоспроизведение" - } - } - } - }, "duck-player.contingency-title" : { "comment" : "Title for message explaining to the user that Duck Player is not available", "extractionState" : "extracted_with_value", @@ -18761,66 +18581,6 @@ } } }, - "duck-player.newtab-title" : { - "comment" : "New Tab title in settings", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Neuer Tab" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "New Tab" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nueva pestaña" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nouvel onglet" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nuova scheda" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nieuw tabblad" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nowa karta" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Novo separador" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Новая вкладка" - } - } - } - }, "duck-player.newtab.info-preference" : { "comment" : "New tab preference extra info in settings", "extractionState" : "extracted_with_value", @@ -19466,60 +19226,6 @@ } } }, - "Duplicate Bookmarks Skipped:" : { - "comment" : "Data import summary format of how many duplicate bookmarks (%lld) were skipped during import.", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Doppelte Lesezeichen übersprungen:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Marcadores duplicados omitidos:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Signets en double ignorés :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Segnalibri duplicati ignorati:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Overgeslagen dubbele bladwijzers:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pominięte zduplikowane zakładki:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Marcadores duplicados ignorados:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Пропущены дубликаты:" - } - } - } - }, "duplicate.tab" : { "comment" : "Menu item. Duplicate as a verb", "extractionState" : "extracted_with_value", @@ -25155,114 +24861,6 @@ } } }, - "If your computer prompts you to enter a password prior to import, DuckDuckGo will not see that password.\n\nImported passwords are stored securely using encryption." : { - "comment" : "Warning that Chromium data import would require entering system passwords.", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wenn dein Computer dich vor dem Import auffordert, ein Passwort einzugeben, wird DuckDuckGo das Passwort nicht sehen.\n\nImportierte Passwörter werden durch Verschlüsselung sicher gespeichert." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si tu ordenador te pide que introduzcas una contraseña antes de importar, DuckDuckGo no la verá.\n\nLas contraseñas importadas se almacenan de forma segura mediante encriptación." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Si votre ordinateur vous invite à saisir un mot de passe avant l'importation, DuckDuckGo ne verra pas ce dernier.\n\nLes mots de passe importés sont stockés en toute sécurité grâce à un cryptage." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se il computer richiede di immettere una password prima dell'importazione, DuckDuckGo non visualizza la password.\n\nLe password importate vengono archiviate in modo sicuro tramite crittografia." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Als je computer je vraagt om vóór het importeren een wachtwoord in te voeren, zal DuckDuckGo dat wachtwoord niet zien.\n\nGeïmporteerde wachtwoorden worden veilig opgeslagen door middel van versleuteling." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jeśli komputer poprosi Cię o wprowadzenie hasła przed rozpoczęciem importowania, DuckDuckGo nie zobaczy tego hasła.\n\nZaimportowane hasła są bezpiecznie przechowywane przy użyciu szyfrowania." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se for apresentada uma solicitação no teu computador para introduzires uma palavra-passe antes da importação, o DuckDuckGo não a irá ver.\n\nAs palavras-passe importadas são armazenadas com segurança por meio de encriptação." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Если перед импортированием компьютер попросит вас ввести пароль, DuckDuckGo его не увидит.\n\nИмпортированные пароли надежно хранятся в зашифрованном виде." - } - } - } - }, - "Import" : { - "comment" : "Menu item", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Importieren" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Importar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Importer" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Importa" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Importeren" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Import" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Importar" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Импорт" - } - } - } - }, "Import Bookmarks" : { "comment" : "Title of dialog with instruction for the user to import bookmarks from another browser", "localizations" : { @@ -37652,66 +37250,6 @@ } } }, - "passsword.management" : { - "comment" : "Used as title for password management user interface", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Autovervollständigen" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Autofill" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Autocompletar" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Saisie automatique" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compilazione automatica" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisch invullen" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Autouzupełnianie" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Preenchimento automático" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Автозаполнение" - } - } - } - }, "passsword.management.all-items" : { "comment" : "Used as title for the Autofill All Items option", "extractionState" : "extracted_with_value", @@ -52400,60 +51938,6 @@ } } }, - "Select data to import:" : { - "comment" : "Data Import section title for checkboxes of data type to import: Passwords or Bookmarks.", - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zu importierende Daten auswählen:" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Selecciona los datos a importar:" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sélectionner les données à importer :" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seleziona i dati da importare:" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Selecteer gegevens om te importeren:" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wybierz dane do zaimportowania:" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Selecionar dados para importar:" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Выберите данные для импорта:" - } - } - } - }, "Select Data to Import:" : { "comment" : "Data Import section title for checkboxes of data type to import: Passwords or Bookmarks.", "localizations" : { @@ -54754,486 +54238,6 @@ } } }, - "ssl.error.certificate.expired.message" : { - "comment" : "Describes an SSL error where a website's security certificate is expired. '%1$@' is a placeholder for the website's domain.", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Sicherheitszertifikat für %1$@ ist abgelaufen." - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "The security certificate for %1$@ is expired." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El certificado de seguridad de %1$@ ha caducado." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Le certificat de sécurité de %1$@ a expiré." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il certificato di sicurezza per %1$@ è scaduto." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Het beveiligingscertificaat voor %1$@ is verlopen." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Certyfikat zabezpieczeń dla %1$@ wygasł." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "O certificado de segurança de %1$@ expirou." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Срок действия сертификата безопасности сайта %1$@ истек." - } - } - } - }, - "ssl.error.page.advanced.button" : { - "comment" : "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to see advanced options on click.", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erweitert …" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Advanced…" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avanzadas..." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Options avancées" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avanzate…" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geavanceerd ..." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zaawansowane..." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avançado…" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Дополнительно..." - } - } - } - }, - "ssl.error.page.advanced.info.body.expired" : { - "comment" : "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Es ist möglich, dass die Website falsch konfiguriert ist, ein Angreifer deine Verbindung kompromittiert hat oder deine Systemuhr falsch ist." - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "It’s possible that the website is misconfigured, that an attacker has compromised your connection, or that your system clock is incorrect." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Es posible que el sitio web esté mal configurado, que un atacante haya comprometido tu conexión o que el reloj del sistema no sea correcto." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il est possible que le site soit mal configuré, qu'un pirate ait compromis votre connexion ou que l'horloge de votre système soit incorrecte." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "È possibile che il sito web non sia configurato correttamente, che un aggressore abbia compromesso la tua connessione o che l'orologio del tuo sistema non sia corretto." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mogelijk is de website verkeerd geconfigureerd, heeft een aanvaller je verbinding gecompromitteerd of staat de klok van je systeem niet goed." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Możliwe, że witryna jest błędnie skonfigurowana, osoba atakująca naruszyła połączenie lub ustawienie zegara systemowego jest nieprawidłowe." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "É possível que o site esteja mal configurado, que um invasor tenha comprometido a tua ligação ou que o teu relógio do sistema esteja incorreto." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Возможно, неверно задана конфигурация сайта, соединение перехвачено злоумышленником либо неправильно настроены системные часы." - } - } - } - }, - "ssl.error.page.advanced.info.body.wrong.host" : { - "comment" : "Body of the text of the Advanced info shown in an error page that warns users of security risks on a website due to SSL issues.", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Möglicherweise ist die Website falsch konfiguriert oder ein Angreifer hat deine Verbindung kompromittiert." - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "It’s possible that the website is misconfigured or that an attacker has compromised your connection." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Es posible que el sitio web esté mal configurado o que un atacante haya comprometido tu conexión." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il est possible que le site soit mal configuré ou qu'un pirate ait compromis votre connexion." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "È possibile che il sito web non sia configurato correttamente o che un malintenzionato abbia compromesso la tua connessione." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mogelijk is de website verkeerd geconfigureerd of heeft een aanvaller je verbinding gecompromitteerd." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Możliwe, że witryna jest błędnie skonfigurowana lub osoba atakująca naruszyła połączenie." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "É possível que o site esteja mal configurado ou que um invasor tenha comprometido a tua ligação." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Возможно, неверно задана конфигурация сайта либо ваше соединение перехвачено злоумышленником." - } - } - } - }, - "ssl.error.page.advanced.info.title" : { - "comment" : "Title of the Advanced info section shown in an error page that warns users of security risks on a website due to SSL issues.", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo warnt dich, wenn eine Website ein ungültiges Zertifikat hat." - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "DuckDuckGo warns you when a website has an invalid certificate." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo te avisa cuando un sitio web tiene un certificado no válido." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo vous avertit lorsque le certificat d'un site Web n'est pas valide." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo ti avverte quando un sito web ha un certificato non valido." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo waarschuwt je wanneer een website een ongeldig certificaat heeft." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo ostrzega gdy witryna internetowa ma nieprawidłowy certyfikat." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "O DuckDuckGo avisa-te quando um site tem um certificado inválido." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "DuckDuckGo предупредит вас, если у сайта окажется недействительный сертификат." - } - } - } - }, - "ssl.error.page.body" : { - "comment" : "Error description shown in an error page that warns users of security risks on a website due to SSL issues. %1$@ represent the site domain.", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Zertifikat für diese Website ist ungültig. Möglicherweise stellst du eine Verbindung zu einem Server her, der vorgibt, %1$@ zu sein, wodurch deine vertraulichen Daten in Gefahr sein könnten." - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "The certificate for this site is invalid. You might be connecting to a server that is pretending to be %1$@ which could put your confidential information at risk." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El certificado de este sitio no es válido. Puede que te estés conectando a un servidor que se hace pasar por %1$@, lo que podría poner en riesgo tu información confidencial." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Le certificat de ce site n'est pas valide. Vous vous connectez peut-être à un serveur qui se fait passer pour %1$@, ce qui pourrait mettre vos informations confidentielles en danger." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il certificato di questo sito non è valido. Potresti collegarti a un server che finge di essere %1$@ e che potrebbe mettere a rischio le tue informazioni riservate." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Het certificaat voor deze website is ongeldig. Mogelijk maak je verbinding met een server die zich voordoet als %1$@, waardoor je vertrouwelijke informatie in gevaar kan komen." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Certyfikat tej witryny jest nieprawidłowy. Być może łączysz się z serwerem podszywającym się pod %1$@, co może narazić poufne informacje na niebezpieczeństwo." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "O certificado deste site é inválido. Podes estar a estabelecer ligação a um servidor que finge ser %1$@, o que pode colocar as tuas informações confidenciais em risco." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сертификат этого сайта недействителен. Возможно, вы пытаетесь подключиться к серверу, который выдает себя за %1$@, что ставит под угрозу вашу конфиденциальную информацию." - } - } - } - }, - "ssl.error.page.header" : { - "comment" : "Title shown in an error page that warn users of security risks on a website due to SSL issues", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Warnung: Diese Website ist möglicherweise unsicher" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Warning: This site may be insecure" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Advertencia: este sitio puede ser inseguro" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Avertissement : ce site n'est peut-être pas sécurisé" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Attenzione: questo sito potrebbe non essere sicuro" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Waarschuwing: Deze website is mogelijk onveilig" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ostrzeżenie: ta witryna może być niebezpieczna" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aviso: este site pode ser inseguro" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Внимание! Возможно, сайт небезопасен" - } - } - } - }, - "ssl.error.page.leave.site.button" : { - "comment" : "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to leave the website and navigate to previous page.", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Diese Website verlassen" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Leave This Site" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Salir de este sitio" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quitter ce site" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Esci da questo sito" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deze website verlaten" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Opuść tę stronę" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deixar este site" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Покинуть сайт" - } - } - } - }, "ssl.error.page.tab.title" : { "comment" : "Title shown in an error page tab that warn users of security risks on a website due to SSL issues", "extractionState" : "extracted_with_value", @@ -55294,186 +54298,6 @@ } } }, - "ssl.error.page.visit.site.button" : { - "comment" : "Button shown in an error page that warns users of security risks on a website due to SSL issues. The buttons allows the user to visit the website anyway despite the risks.", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Risiko akzeptieren und Website besuchen" - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Accept Risk and Visit Site" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aceptar el riesgo y visitar el sitio" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Accepter le risque et visiter le site" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Accetta il rischio e visita il sito" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Risico accepteren en site bezoeken" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zaakceptuj ryzyko i odwiedź witrynę" - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aceitar o risco e visitar o site" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Принять риск и перейти на сайт" - } - } - } - }, - "ssl.error.self.signed.message" : { - "comment" : "Warns the user that the site's security certificate is self-signed and not trusted. '%1$@' is the site's domain.", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Sicherheitszertifikat für %1$@ wird vom Betriebssystem deines Geräts als nicht vertrauenswürdig eingestuft." - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "The security certificate for %1$@ is not trusted by your device's operating system." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El sistema operativo del dispositivo no confía en el certificado de seguridad de %1$@." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Le certificat de sécurité de %1$@ n'est pas approuvé par le système d'exploitation de votre appareil." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il certificato di sicurezza di %1$@ non è considerato attendibile dal sistema operativo del tuo dispositivo." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Het besturingssysteem van je apparaat vertrouwt het beveiligingscertificaat voor %1$@ niet." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Certyfikat bezpieczeństwa %1$@ nie jest zaufany przez system operacyjny urządzenia." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "O sistema operativo do teu dispositivo não confia no certificado de segurança de %1$@." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Операционная система вашего устройства не доверяет сертификату безопасности сайта %1$@." - } - } - } - }, - "ssl.error.wrong.host.message" : { - "comment" : "Explains an SSL error when a site's certificate doesn't match its domain. '%1$@' is the site's domain.", - "extractionState" : "extracted_with_value", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Sicherheitszertifikat für %1$@ stimmt nicht mit *.%2$@ überein." - } - }, - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "The security certificate for %1$@ does not match *.%2$@." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "El certificado de seguridad de %1$@ no coincide con *.%2$@." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Le certificat de sécurité de %1$@ ne correspond pas à *.%2$@." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Il certificato di protezione per %1$@ non corrisponde a *.%2$@." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Het beveiligingscertificaat voor %1$@ komt niet overeen met *.%2$@. " - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Certyfikat zabezpieczeń %1$@ nie jest zgodny z *.%2$@." - } - }, - "pt" : { - "stringUnit" : { - "state" : "translated", - "value" : "O certificado de segurança de %1$@ não corresponde a *.%2$@." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сертификат безопасности сайта %1$@ не подходит для домена *.%2$@." - } - } - } - }, "Start Speaking" : { "comment" : "Main Menu Edit-Speech item", "localizations" : { diff --git a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift index 15dc8d38a1..f5cff41746 100644 --- a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift +++ b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift @@ -18,6 +18,7 @@ import Foundation import BrowserServicesKit +import SpecialErrorPages extension SpecialPagesUserScript { @MainActor @@ -38,8 +39,10 @@ extension SpecialPagesUserScript { } func withErrorPages() { - let sslErrorPageUserScript = SSLErrorPageUserScript() - self.registerSubfeature(delegate: sslErrorPageUserScript) + let lenguageCode = Locale.current.languageCode ?? "en" + let specialErrorPageUserScript = SpecialErrorPageUserScript(localeStrings: SpecialErrorPageUserScript.localeStrings(for: lenguageCode), + languageCode: lenguageCode) + self.registerSubfeature(delegate: specialErrorPageUserScript) } @MainActor diff --git a/DuckDuckGo/Tab/TabExtensions/SSLErrorPageTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/SpecialErrorPageTabExtension.swift similarity index 73% rename from DuckDuckGo/Tab/TabExtensions/SSLErrorPageTabExtension.swift rename to DuckDuckGo/Tab/TabExtensions/SpecialErrorPageTabExtension.swift index aac116210a..753ed648da 100644 --- a/DuckDuckGo/Tab/TabExtensions/SSLErrorPageTabExtension.swift +++ b/DuckDuckGo/Tab/TabExtensions/SpecialErrorPageTabExtension.swift @@ -1,5 +1,5 @@ // -// SSLErrorPageTabExtension.swift +// SpecialErrorPageTabExtension.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -22,25 +22,31 @@ import WebKit import Combine import ContentScopeScripts import BrowserServicesKit +import SpecialErrorPages +import Common -protocol SSLErrorPageScriptProvider { - var sslErrorPageUserScript: SSLErrorPageUserScript? { get } +protocol SpecialErrorPageScriptProvider { + var specialErrorPageUserScript: SpecialErrorPageUserScript? { get } } -extension UserScripts: SSLErrorPageScriptProvider {} +extension UserScripts: SpecialErrorPageScriptProvider {} -final class SSLErrorPageTabExtension { +final class SpecialErrorPageTabExtension { weak var webView: ErrorPageTabExtensionNavigationDelegate? - private weak var sslErrorPageUserScript: SSLErrorPageUserScript? + private weak var specialErrorPageUserScript: SpecialErrorPageUserScript? private var shouldBypassSSLError = false private var urlCredentialCreator: URLCredentialCreating private var featureFlagger: FeatureFlagger + private let tld = TLD() private var cancellables = Set() + var errorData: SpecialErrorData? + var failingURL: URL? + init( webViewPublisher: some Publisher, - scriptsPublisher: some Publisher, + scriptsPublisher: some Publisher, urlCredentialCreator: URLCredentialCreating = URLCredentialCreator(), featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger) { self.featureFlagger = featureFlagger @@ -49,22 +55,13 @@ final class SSLErrorPageTabExtension { self?.webView = webView }.store(in: &cancellables) scriptsPublisher.sink { [weak self] scripts in - self?.sslErrorPageUserScript = scripts.sslErrorPageUserScript - self?.sslErrorPageUserScript?.delegate = self + self?.specialErrorPageUserScript = scripts.specialErrorPageUserScript + self?.specialErrorPageUserScript?.delegate = self }.store(in: &cancellables) } - @MainActor - private func loadSSLErrorHTML(url: URL, alternate: Bool, errorCode: Int) { - let domain: String = url.host ?? url.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true) - let html = SSLErrorPageHTMLTemplate(domain: domain, errorCode: errorCode).makeHTMLFromTemplate() - webView?.loadAlternateHTML(html, baseURL: .error, forUnreachableURL: url) - loadHTML(html: html, url: url, alternate: alternate) - } - - @MainActor - private func loadErrorHTML(_ error: WKError, header: String, forUnreachableURL url: URL, alternate: Bool) { - let html = ErrorPageHTMLTemplate(error: error, header: header).makeHTMLFromTemplate() + @MainActor private func loadSSLErrorHTML(url: URL, alternate: Bool) { + let html = SpecialErrorPageHTMLTemplate.htmlFromTemplate loadHTML(html: html, url: url, alternate: alternate) } @@ -76,10 +73,9 @@ final class SSLErrorPageTabExtension { webView?.setDocumentHtml(html) } } - } -extension SSLErrorPageTabExtension: NavigationResponder { +extension SpecialErrorPageTabExtension: NavigationResponder { @MainActor func navigation(_ navigation: Navigation, didFailWith error: WKError) { let url = error.failingUrl ?? navigation.url @@ -87,21 +83,22 @@ extension SSLErrorPageTabExtension: NavigationResponder { guard error.errorCode != NSURLErrorCannotFindHost else { return } if !error.isFrameLoadInterrupted, !error.isNavigationCancelled { - // when already displaying the error page and reload navigation fails again: don‘t navigate, just update page HTML guard let webView else { return } let shouldPerformAlternateNavigation = navigation.url != webView.url || navigation.navigationAction.targetFrame?.url != .error if featureFlagger.isFeatureOn(.sslCertificatesBypass), error.errorCode == NSURLErrorServerCertificateUntrusted, let errorCode = error.userInfo["_kCFStreamErrorCodeKey"] as? Int { - sslErrorPageUserScript?.failingURL = url - loadSSLErrorHTML(url: url, alternate: shouldPerformAlternateNavigation, errorCode: errorCode) + failingURL = url + let domain: String = url.host ?? url.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true) + errorData = SpecialErrorData(kind: .ssl, errorType: SSLErrorType.forErrorCode(errorCode).rawValue, domain: domain, eTldPlus1: tld.eTLDplus1(failingURL?.host)) + loadSSLErrorHTML(url: url, alternate: shouldPerformAlternateNavigation) } } } @MainActor func navigationDidFinish(_ navigation: Navigation) { - sslErrorPageUserScript?.isEnabled = navigation.url == sslErrorPageUserScript?.failingURL + specialErrorPageUserScript?.isEnabled = navigation.url == failingURL } @MainActor @@ -116,7 +113,8 @@ extension SSLErrorPageTabExtension: NavigationResponder { } } -extension SSLErrorPageTabExtension: SSLErrorPageUserScriptDelegate { +extension SpecialErrorPageTabExtension: SpecialErrorPageUserScriptDelegate { + func leaveSite() { guard webView?.canGoBack == true else { webView?.close() @@ -129,18 +127,20 @@ extension SSLErrorPageTabExtension: SSLErrorPageUserScriptDelegate { shouldBypassSSLError = true _ = webView?.reloadPage() } + + func advancedInfoPresented() {} } protocol ErrorPageTabExtensionProtocol: AnyObject, NavigationResponder {} -extension SSLErrorPageTabExtension: TabExtension, ErrorPageTabExtensionProtocol { +extension SpecialErrorPageTabExtension: TabExtension, ErrorPageTabExtensionProtocol { typealias PublicProtocol = ErrorPageTabExtensionProtocol func getPublicProtocol() -> PublicProtocol { self } } extension TabExtensions { var errorPage: ErrorPageTabExtensionProtocol? { - resolve(SSLErrorPageTabExtension.self) + resolve(SpecialErrorPageTabExtension.self) } } diff --git a/DuckDuckGo/Tab/TabExtensions/SpecialErrorPageUserScriptExtension.swift b/DuckDuckGo/Tab/TabExtensions/SpecialErrorPageUserScriptExtension.swift new file mode 100644 index 0000000000..900c2de99e --- /dev/null +++ b/DuckDuckGo/Tab/TabExtensions/SpecialErrorPageUserScriptExtension.swift @@ -0,0 +1,34 @@ +// +// SpecialErrorPageUserScriptExtension.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SpecialErrorPages +import ContentScopeScripts + +extension SpecialErrorPageUserScript { + + static func localeStrings(for languageCode: String = Locale.current.languageCode ?? "en") -> String? { + if let localizedFile = ContentScopeScripts.Bundle.path(forResource: "special-error", + ofType: "json", + inDirectory: "pages/special-error/locales/\(languageCode)") { + return try? String(contentsOfFile: localizedFile) + } + return nil + } + +} diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index c4a5f773d2..e998c25902 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -196,7 +196,7 @@ extension TabExtensionsBuilder { } add { - SSLErrorPageTabExtension(webViewPublisher: args.webViewFuture, + SpecialErrorPageTabExtension(webViewPublisher: args.webViewFuture, scriptsPublisher: userScripts.compactMap { $0 }) } #if SPARKLE diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index b82117b5ad..0804b63aad 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -21,6 +21,7 @@ import BrowserServicesKit import UserScript import WebKit import Subscription +import SpecialErrorPages @MainActor final class UserScripts: UserScriptsProvider { @@ -44,7 +45,7 @@ final class UserScripts: UserScriptsProvider { let autoconsentUserScript: UserScriptWithAutoconsent let youtubeOverlayScript: YoutubeOverlayUserScript? let youtubePlayerUserScript: YoutubePlayerUserScript? - let sslErrorPageUserScript: SSLErrorPageUserScript? + let specialErrorPageUserScript: SpecialErrorPageUserScript? let onboardingUserScript: OnboardingUserScript? #if SPARKLE let releaseNotesUserScript: ReleaseNotesUserScript? @@ -68,7 +69,9 @@ final class UserScripts: UserScriptsProvider { autoconsentUserScript = AutoconsentUserScript(scriptSource: sourceProvider, config: sourceProvider.privacyConfigurationManager.privacyConfig) - sslErrorPageUserScript = SSLErrorPageUserScript() + let lenguageCode = Locale.current.languageCode ?? "en" + specialErrorPageUserScript = SpecialErrorPageUserScript(localeStrings: SpecialErrorPageUserScript.localeStrings(for: lenguageCode), + languageCode: lenguageCode) onboardingUserScript = OnboardingUserScript(onboardingActionsManager: sourceProvider.onboardingActionsManager!) @@ -95,8 +98,8 @@ final class UserScripts: UserScriptsProvider { } if let specialPages = specialPages { - if let sslErrorPageUserScript { - specialPages.registerSubfeature(delegate: sslErrorPageUserScript) + if let specialErrorPageUserScript { + specialPages.registerSubfeature(delegate: specialErrorPageUserScript) } if let youtubePlayerUserScript { specialPages.registerSubfeature(delegate: youtubePlayerUserScript) diff --git a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift index 3be41aeab5..513aa59d02 100644 --- a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift +++ b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift @@ -67,7 +67,6 @@ extension DuckURLSchemeHandler { private func handleNativeUIPages(requestURL: URL, urlSchemeTask: WKURLSchemeTask) { // return empty page for native UI pages navigations (like the Home page or Settings) if the request is not for the Duck Player let data = Self.emptyHtml.utf8data - let response = URLResponse(url: requestURL, mimeType: "text/html", expectedContentLength: data.count, @@ -108,7 +107,7 @@ private extension DuckURLSchemeHandler { private extension DuckURLSchemeHandler { func handleSpecialPages(urlSchemeTask: WKURLSchemeTask) { guard let requestURL = urlSchemeTask.request.url else { - assertionFailure("No URL for Onboarding scheme handler") + assertionFailure("No URL for Special Pages scheme handler") return } guard let (response, data) = response(for: requestURL) else { return } diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index e34d9922c4..a79685ddb4 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: "189.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "190.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../XPCHelper"), ], diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index 9f49b52da9..b1da627366 100644 --- a/LocalPackages/NetworkProtectionMac/Package.swift +++ b/LocalPackages/NetworkProtectionMac/Package.swift @@ -32,7 +32,7 @@ let package = Package( .library(name: "VPNAppLauncher", targets: ["VPNAppLauncher"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "189.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "190.0.0"), .package(url: "https://github.com/airbnb/lottie-spm", exact: "4.4.3"), .package(path: "../AppLauncher"), .package(path: "../UDSHelper"), diff --git a/LocalPackages/SubscriptionUI/Package.swift b/LocalPackages/SubscriptionUI/Package.swift index d5137c0d14..dc054cbeb6 100644 --- a/LocalPackages/SubscriptionUI/Package.swift +++ b/LocalPackages/SubscriptionUI/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: ["SubscriptionUI"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "189.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "190.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [ diff --git a/UnitTests/DuckSchemeHandler/DuckSchemeHandlerTests.swift b/UnitTests/DuckSchemeHandler/DuckSchemeHandlerTests.swift index df01397e81..cec7c92df1 100644 --- a/UnitTests/DuckSchemeHandler/DuckSchemeHandlerTests.swift +++ b/UnitTests/DuckSchemeHandler/DuckSchemeHandlerTests.swift @@ -26,7 +26,7 @@ final class DuckSchemeHandlerTests: XCTestCase { func testWebViewFromOnboardingHandlerReturnsResponseAndData() throws { // Given - let onboardingURL = URL(string: "duck://onboarding?platform=integration")! + let onboardingURL = URL(string: "duck://onboarding")! let handler = DuckURLSchemeHandler() let webView = WKWebView() let schemeTask = MockSchemeTask(request: URLRequest(url: onboardingURL)) @@ -43,6 +43,25 @@ final class DuckSchemeHandlerTests: XCTestCase { XCTAssertNil(schemeTask.error) } + func testWebViewFromReleaseNoteHandlerReturnsResponseAndData() throws { + // Given + let releaseNotesURL = URL(string: "duck://release-notes")! + let handler = DuckURLSchemeHandler() + let webView = WKWebView() + let schemeTask = MockSchemeTask(request: URLRequest(url: releaseNotesURL)) + + // When + handler.webView(webView, start: schemeTask) + + // Then + XCTAssertEqual(schemeTask.response?.url, releaseNotesURL) + XCTAssertEqual(schemeTask.response?.mimeType, "text/html") + XCTAssertNotNil(schemeTask.data) + XCTAssertTrue(schemeTask.data?.utf8String()?.contains("Browser Release Notes") ?? false) + XCTAssertTrue(schemeTask.didFinishCalled) + XCTAssertNil(schemeTask.error) + } + @MainActor func testWebViewFromDuckPlayerHandlerReturnsResponseAndData() throws { // Given diff --git a/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift index 51cfbfff44..9426baccfe 100644 --- a/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift +++ b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift @@ -22,23 +22,24 @@ import Navigation import Common import WebKit import XCTest +import SpecialErrorPages @testable import DuckDuckGo_Privacy_Browser final class ErrorPageTabExtensionTest: XCTestCase { var mockWebViewPublisher: PassthroughSubject! - var scriptPublisher: PassthroughSubject! - var errorPageExtention: SSLErrorPageTabExtension! + var scriptPublisher: PassthroughSubject! + var errorPageExtention: SpecialErrorPageTabExtension! var credentialCreator: MockCredentialCreator! let errorURLString = "com.example.error" override func setUpWithError() throws { mockWebViewPublisher = PassthroughSubject() - scriptPublisher = PassthroughSubject() + scriptPublisher = PassthroughSubject() credentialCreator = MockCredentialCreator() let featureFlagger = MockFeatureFlagger() - errorPageExtention = SSLErrorPageTabExtension(webViewPublisher: mockWebViewPublisher, scriptsPublisher: scriptPublisher, urlCredentialCreator: credentialCreator, featureFlagger: featureFlagger) + errorPageExtention = SpecialErrorPageTabExtension(webViewPublisher: mockWebViewPublisher, scriptsPublisher: scriptPublisher, urlCredentialCreator: credentialCreator, featureFlagger: featureFlagger) } override func tearDownWithError() throws { @@ -59,7 +60,7 @@ final class ErrorPageTabExtensionTest: XCTestCase { XCTAssertTrue(errorPageExtention.webView === aWebView) } - @MainActor func testWhenCertificateExpired_ThenExpectedErrorPageIsShown() { + @MainActor func testWhenCertificateExpired_ThenTabExtenstionErrorIsExpectedError() { // GIVEN let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) errorPageExtention.webView = mockWebView @@ -71,8 +72,7 @@ final class ErrorPageTabExtensionTest: XCTestCase { errorPageExtention.navigation(navigation, didFailWith: error) // THEN - let expectedSpecificMessage = SSLErrorType.expired.specificMessage(for: errorURLString, eTldPlus1: eTldPlus1).replacingOccurrences(of: "", with: "<\\/b>").escapedUnicodeHtmlString() - XCTAssertTrue(mockWebView.capturedHTML.contains(expectedSpecificMessage)) + XCTAssertEqual(errorPageExtention.errorData, SpecialErrorData(kind: .ssl, errorType: "expired", domain: eTldPlus1)) } @MainActor func testWhenCertificateSelfSigned_ThenExpectedErrorPageIsShown() { @@ -87,8 +87,7 @@ final class ErrorPageTabExtensionTest: XCTestCase { errorPageExtention.navigation(navigation, didFailWith: error) // THEN - let expectedSpecificMessage = SSLErrorType.selfSigned.specificMessage(for: errorURLString, eTldPlus1: eTldPlus1).replacingOccurrences(of: "", with: "<\\/b>").escapedUnicodeHtmlString() - XCTAssertTrue(mockWebView.capturedHTML.contains(expectedSpecificMessage)) + XCTAssertEqual(errorPageExtention.errorData, SpecialErrorData(kind: .ssl, errorType: "selfSigned", domain: eTldPlus1)) } @MainActor func testWhenCertificateWrongHost_ThenExpectedErrorPageIsShown() { @@ -103,27 +102,27 @@ final class ErrorPageTabExtensionTest: XCTestCase { errorPageExtention.navigation(navigation, didFailWith: error) // THEN - let expectedSpecificMessage = SSLErrorType.wrongHost.specificMessage(for: errorURLString, eTldPlus1: eTldPlus1).replacingOccurrences(of: "", with: "<\\/b>").escapedUnicodeHtmlString() - XCTAssertTrue(mockWebView.capturedHTML.contains(expectedSpecificMessage)) - + XCTAssertEqual(errorPageExtention.errorData, SpecialErrorData(kind: .ssl, errorType: "wrongHost", domain: eTldPlus1)) } @MainActor func test_WhenUserScriptsPublisherPublishSSLErrorPageScript_ThenErrorPageExtensionIsSetAsUserScriptDelegate() { // GIVEN - let aSSLErrorUserScript = SSLErrorPageUserScript() - let mockScriptProvider = MockSSLErrorPageScriptProvider(script: aSSLErrorUserScript) + let userScript = SpecialErrorPageUserScript(localeStrings: SpecialErrorPageUserScript.localeStrings(), + languageCode: Locale.current.languageCode ?? "en") + let mockScriptProvider = MockSpecialErrorPageScriptProvider(script: userScript) // WHEN scriptPublisher.send(mockScriptProvider) // THEN - XCTAssertNotNil(aSSLErrorUserScript.delegate) + XCTAssertNotNil(userScript.delegate) } @MainActor func testWhenNavigationEnded_IfNoFailure_SSLUserScriptIsNotEnabled() { // GIVEN - let userScript = SSLErrorPageUserScript() - let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let userScript = SpecialErrorPageUserScript(localeStrings: SpecialErrorPageUserScript.localeStrings(), + languageCode: Locale.current.languageCode ?? "en") + let mockScriptProvider = MockSpecialErrorPageScriptProvider(script: userScript) let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) @@ -135,13 +134,14 @@ final class ErrorPageTabExtensionTest: XCTestCase { // THEN XCTAssertFalse(userScript.isEnabled) - XCTAssertNil(userScript.failingURL) + XCTAssertNil(errorPageExtention.failingURL) } @MainActor func testWhenNavigationEnded_IfNonSSLFailure_SSLUserScriptIsNotEnabled() { // GIVEN - let userScript = SSLErrorPageUserScript() - let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let userScript = SpecialErrorPageUserScript(localeStrings: SpecialErrorPageUserScript.localeStrings(), + languageCode: Locale.current.languageCode ?? "en") + let mockScriptProvider = MockSpecialErrorPageScriptProvider(script: userScript) let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) @@ -156,13 +156,14 @@ final class ErrorPageTabExtensionTest: XCTestCase { // THEN XCTAssertFalse(userScript.isEnabled) - XCTAssertNil(userScript.failingURL) + XCTAssertNil(errorPageExtention.failingURL) } @MainActor func testWhenNavigationEnded_IfSSLFailure_AndErrorURLIsDifferentFromNavigationURL_SSLUserScriptIsNotEnabled() { // GIVEN - let userScript = SSLErrorPageUserScript() - let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let userScript = SpecialErrorPageUserScript(localeStrings: SpecialErrorPageUserScript.localeStrings(), + languageCode: Locale.current.languageCode ?? "en") + let mockScriptProvider = MockSpecialErrorPageScriptProvider(script: userScript) let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) let action = NavigationAction(request: URLRequest(url: URL(string: "com.different.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) @@ -176,13 +177,14 @@ final class ErrorPageTabExtensionTest: XCTestCase { // THEN XCTAssertFalse(userScript.isEnabled) - XCTAssertEqual(userScript.failingURL?.absoluteString, errorURLString) + XCTAssertEqual(errorPageExtention.failingURL?.absoluteString, errorURLString) } @MainActor func testWhenNavigationEnded_IfSSLFailure_AndErrorURLIsTheSameAsNavigationURL_SSLUserScriptIsEnabled() { // GIVEN - let userScript = SSLErrorPageUserScript() - let mockScriptProvider = MockSSLErrorPageScriptProvider(script: userScript) + let userScript = SpecialErrorPageUserScript(localeStrings: SpecialErrorPageUserScript.localeStrings(), + languageCode: Locale.current.languageCode ?? "en") + let mockScriptProvider = MockSpecialErrorPageScriptProvider(script: userScript) let mockWebView = MockWKWebView(url: URL(string: errorURLString)!) let action = NavigationAction(request: URLRequest(url: URL(string: "com.example.error")!), navigationType: .custom(.userEnteredUrl), currentHistoryItemIdentity: nil, redirectHistory: nil, isUserInitiated: true, sourceFrame: FrameInfo(frame: WKFrameInfo()), targetFrame: nil, shouldDownload: false, mainFrameNavigation: nil) let navigation = Navigation(identity: .init(nil), responders: .init(), state: .started, redirectHistory: [action], isCurrent: true, isCommitted: true) @@ -196,7 +198,7 @@ final class ErrorPageTabExtensionTest: XCTestCase { // THEN XCTAssertTrue(userScript.isEnabled) - XCTAssertEqual(userScript.failingURL?.absoluteString, errorURLString) + XCTAssertEqual(errorPageExtention.failingURL?.absoluteString, errorURLString) } func testWhenLeaveSiteCalled_AndCanGoBackTrue_ThenWebViewGoesBack() { @@ -351,11 +353,11 @@ class MockWKWebView: NSObject, ErrorPageTabExtensionNavigationDelegate { } } -class MockSSLErrorPageScriptProvider: SSLErrorPageScriptProvider { - var sslErrorPageUserScript: SSLErrorPageUserScript? +class MockSpecialErrorPageScriptProvider: SpecialErrorPageScriptProvider { + var specialErrorPageUserScript: SpecialErrorPageUserScript? - init(script: SSLErrorPageUserScript?) { - self.sslErrorPageUserScript = script + init(script: SpecialErrorPageUserScript?) { + self.specialErrorPageUserScript = script } } diff --git a/UnitTests/TabExtensionsTests/SpecialErrorPageUserScriptTests.swift b/UnitTests/TabExtensionsTests/SpecialErrorPageUserScriptTests.swift new file mode 100644 index 0000000000..37080466c1 --- /dev/null +++ b/UnitTests/TabExtensionsTests/SpecialErrorPageUserScriptTests.swift @@ -0,0 +1,49 @@ +// +// SpecialErrorPageUserScriptTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import SpecialErrorPages +@testable import DuckDuckGo_Privacy_Browser + + final class SpecialErrorPageUserScriptTests: XCTestCase { + + func testLocaleStringsForNotSupportedLanguage() { + // Given + let languageCode = "ko" + + // When + let result = SpecialErrorPageUserScript.localeStrings(for: languageCode) + + // Then + XCTAssertNil(result, "The result should be nil for Korean language") + } + + func testLocaleStringsForPolishLanguage() { + // Given + let languageCode = "pl" + + // When + let result = SpecialErrorPageUserScript.localeStrings(for: languageCode) + + // Then + XCTAssertNotNil(result, "The result should not be nil for the Polish language code.") + let expectedSubstring = "Ostrzeżenie: ta witryna może być niebezpieczna" + XCTAssertTrue(result!.contains(expectedSubstring), "The result should contain the expected Polish string.") + } + + } diff --git a/UnitTests/UserScripts/SSLErrorPageUserScriptTests.swift b/UnitTests/UserScripts/SSLErrorPageUserScriptTests.swift deleted file mode 100644 index f94b23f889..0000000000 --- a/UnitTests/UserScripts/SSLErrorPageUserScriptTests.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// SSLErrorPageUserScriptTests.swift -// -// Copyright © 2024 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest -import UserScript - -@testable import DuckDuckGo_Privacy_Browser - -final class SSLErrorPageUserScriptTests: XCTestCase { - - var delegate: CapturingSSLErrorPageUserScriptDelegate! - var userScript: SSLErrorPageUserScript! - - override func setUpWithError() throws { - delegate = CapturingSSLErrorPageUserScriptDelegate() - userScript = SSLErrorPageUserScript() - userScript.delegate = delegate - } - - override func tearDownWithError() throws { - delegate = nil - userScript = nil - } - - func test_FeatureHasCorrectName() throws { - XCTAssertEqual(userScript.featureName, "sslErrorPage") - } - - func test_BrokerIsCorrectlyAdded() throws { - // WHEN - let broker = UserScriptMessageBroker(context: "some contect") - userScript.with(broker: broker) - - // THEN - XCTAssertEqual(userScript.broker, broker) - } - - @MainActor - func test_WhenHandlerForLeaveSiteCalled_AndIsEnabledFalse_ThenNoHandlerIsReturned() { - // WHEN - let handler = userScript.handler(forMethodNamed: "leaveSite") - - // THEN - XCTAssertNil(handler) - } - - @MainActor - func test_WhenHandlerForVisitSiteCalled_AndIsEnabledFalse_ThenNoHandlerIsReturned() { - // WHEN - let handler = userScript.handler(forMethodNamed: "visitSite") - - // THEN - XCTAssertNil(handler) - } - - @MainActor - func test_WhenHandlerForLeaveSiteCalled_AndIsEnabledTrue_ThenLeaveSiteCalled() async { - // GIVEN - var encodable: Encodable? - userScript.isEnabled = true - - // WHEN - let handler = userScript.handler(forMethodNamed: "leaveSite") - if let handler { - encodable = try? await handler(Data(), WKScriptMessage()) - } - - // THEN - XCTAssertNotNil(handler) - XCTAssertNil(encodable) - XCTAssertTrue(delegate.leaveSiteCalled) - XCTAssertFalse(delegate.visitSiteCalled) - } - - @MainActor - func test_WhenHandlerForVisitSiteCalled_AndIsEnabledTrue_ThenVisitSiteCalled() async { - // GIVEN - var encodable: Encodable? - userScript.isEnabled = true - - // WHEN - let handler = userScript.handler(forMethodNamed: "visitSite") - if let handler { - encodable = try? await handler(Data(), WKScriptMessage()) - } - - // THEN - XCTAssertNotNil(handler) - XCTAssertNil(encodable) - XCTAssertTrue(delegate.visitSiteCalled) - XCTAssertFalse(delegate.leaveSiteCalled) - } - -} - -class CapturingSSLErrorPageUserScriptDelegate: SSLErrorPageUserScriptDelegate { - var leaveSiteCalled = false - var visitSiteCalled = false - - func leaveSite() { - leaveSiteCalled = true - } - - func visitSite() { - visitSiteCalled = true - } -}