From 1374a07e17d710058d417a1a9e49aa25f2aa3c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacek=20=C5=81yp?= Date: Sat, 9 Nov 2024 01:57:42 +0100 Subject: [PATCH] Introduce report broken site prompt on triple refresh page (#3523) Task/Issue URL: https://app.asana.com/0/72649045549333/1207889813347128/f **Description**: Add a prompt that appears when the user refreshes a specific page three times within a 20-second window. --- DuckDuckGo.xcodeproj/project.pbxproj | 74 ++++++++++- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../BrokenSitePromptLimiter.swift | 27 ++++ .../BrokenSitePromptLimiterStore.swift | 30 +++++ DuckDuckGo/Common/Localizables/UserText.swift | 5 + .../Utilities/UserDefaultsWrapper.swift | 10 ++ DuckDuckGo/Localizable.xcstrings | 122 +++++++++++++++++- .../MainWindow/MainViewController.swift | 7 +- .../PopoverMessageViewController.swift | 7 +- .../AddressBarButtonsViewController.swift | 9 +- .../View/NavigationBarPopovers.swift | 9 +- .../View/NavigationBarViewController.swift | 52 +++++++- .../PageRefreshMonitor.swift | 28 ++++ .../PageRefreshMonitor/PageRefreshStore.swift | 26 ++++ .../View/PrivacyDashboardPopover.swift | 9 +- DuckDuckGo/Statistics/GeneralPixel.swift | 11 ++ DuckDuckGo/Tab/Model/Tab.swift | 16 ++- .../DataBrokerProtection/Package.swift | 2 +- .../NetworkProtectionMac/Package.swift | 2 +- LocalPackages/SubscriptionUI/Package.swift | 2 +- 20 files changed, 424 insertions(+), 28 deletions(-) create mode 100644 DuckDuckGo/BrokenSitePrompt/BrokenSitePromptLimiter.swift create mode 100644 DuckDuckGo/BrokenSitePrompt/BrokenSitePromptLimiterStore.swift create mode 100644 DuckDuckGo/PageRefreshMonitor/PageRefreshMonitor.swift create mode 100644 DuckDuckGo/PageRefreshMonitor/PageRefreshStore.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 5b2f6f2f23..c6874ad10a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2942,10 +2942,22 @@ C1F142DC2CB93AED003DA518 /* FreemiumDBPPromotionViewCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F142DA2CB93AED003DA518 /* FreemiumDBPPromotionViewCoordinatorTests.swift */; }; CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */; }; CB24F70D29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */; }; + CB63DECB2CDC0BBE0097986A /* PageRefreshMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB63DECA2CDC0BB80097986A /* PageRefreshMonitor.swift */; }; + CB63DECC2CDC0BBE0097986A /* PageRefreshMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB63DECA2CDC0BB80097986A /* PageRefreshMonitor.swift */; }; CB6BCDF927C6BEFF00CC76DC /* PrivacyFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB6BCDF827C6BEFF00CC76DC /* PrivacyFeatures.swift */; }; CBC83E3629B63D380008E19C /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = CBC83E3529B63D380008E19C /* Configuration */; }; CBDD5DE329A67F2700832877 /* MockConfigurationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DE229A67F2700832877 /* MockConfigurationStore.swift */; }; CBDD5DE429A6800300832877 /* MockConfigurationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDD5DE229A67F2700832877 /* MockConfigurationStore.swift */; }; + CBECDB802CD9829A005B8B87 /* PageRefreshStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBECDB7F2CD98296005B8B87 /* PageRefreshStore.swift */; }; + CBECDB812CD9829A005B8B87 /* PageRefreshStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBECDB7F2CD98296005B8B87 /* PageRefreshStore.swift */; }; + CBECDB842CDA813C005B8B87 /* BrokenSitePromptLimiterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBECDB832CDA8137005B8B87 /* BrokenSitePromptLimiterStore.swift */; }; + CBECDB852CDA813C005B8B87 /* BrokenSitePromptLimiterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBECDB832CDA8137005B8B87 /* BrokenSitePromptLimiterStore.swift */; }; + CBECDB872CDACE29005B8B87 /* BrokenSitePromptLimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBECDB862CDACE29005B8B87 /* BrokenSitePromptLimiter.swift */; }; + CBECDB882CDACE29005B8B87 /* BrokenSitePromptLimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBECDB862CDACE29005B8B87 /* BrokenSitePromptLimiter.swift */; }; + CBECDB8A2CDBD616005B8B87 /* PageRefreshMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = CBECDB892CDBD616005B8B87 /* PageRefreshMonitor */; }; + CBECDB8C2CDBD61C005B8B87 /* BrokenSitePrompt in Frameworks */ = {isa = PBXBuildFile; productRef = CBECDB8B2CDBD61C005B8B87 /* BrokenSitePrompt */; }; + CBECDB8E2CDBD62C005B8B87 /* PageRefreshMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = CBECDB8D2CDBD62C005B8B87 /* PageRefreshMonitor */; }; + CBECDB902CDBD631005B8B87 /* BrokenSitePrompt in Frameworks */ = {isa = PBXBuildFile; productRef = CBECDB8F2CDBD631005B8B87 /* BrokenSitePrompt */; }; CD2AB5BF2C8222F20019EB49 /* PhishingDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE248A42C821FFE00F9399D /* PhishingDetection.swift */; }; CD2AB5C02C8222F20019EB49 /* PhishingDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE248A42C821FFE00F9399D /* PhishingDetection.swift */; }; CD2AB5C12C8222F40019EB49 /* PhishingDetectionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE248A52C821FFE00F9399D /* PhishingDetectionPreferences.swift */; }; @@ -4802,8 +4814,12 @@ C1E961EE2B87AA29001760E1 /* AutofillActionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionBuilder.swift; sourceTree = ""; }; C1F142DA2CB93AED003DA518 /* FreemiumDBPPromotionViewCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreemiumDBPPromotionViewCoordinatorTests.swift; sourceTree = ""; }; CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationURLProvider.swift; sourceTree = ""; }; + CB63DECA2CDC0BB80097986A /* PageRefreshMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageRefreshMonitor.swift; sourceTree = ""; }; CB6BCDF827C6BEFF00CC76DC /* PrivacyFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyFeatures.swift; sourceTree = ""; }; CBDD5DE229A67F2700832877 /* MockConfigurationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockConfigurationStore.swift; sourceTree = ""; }; + CBECDB7F2CD98296005B8B87 /* PageRefreshStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageRefreshStore.swift; sourceTree = ""; }; + CBECDB832CDA8137005B8B87 /* BrokenSitePromptLimiterStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSitePromptLimiterStore.swift; sourceTree = ""; }; + CBECDB862CDACE29005B8B87 /* BrokenSitePromptLimiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrokenSitePromptLimiter.swift; sourceTree = ""; }; CD2AB5C92C8225E70019EB49 /* URLTokenValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLTokenValidator.swift; sourceTree = ""; }; CD3301282C887B1C009AA127 /* URLTokenValidatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLTokenValidatorTests.swift; sourceTree = ""; }; CD33012B2C89B588009AA127 /* ErrorPageHTMLFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorPageHTMLFactory.swift; sourceTree = ""; }; @@ -4902,7 +4918,9 @@ 4BF97AD32B43C43F00EB4240 /* NetworkProtectionUI in Frameworks */, 7B1459572B7D43E500047F2C /* NetworkProtectionProxy in Frameworks */, B6F7128229F6820A00594A45 /* QuickLookUI.framework in Frameworks */, + CBECDB902CDBD631005B8B87 /* BrokenSitePrompt in Frameworks */, 9FF521482BAA909C00B9819B /* Lottie in Frameworks */, + CBECDB8E2CDBD62C005B8B87 /* PageRefreshMonitor in Frameworks */, 984FD3BF299ACF35007334DD /* Bookmarks in Frameworks */, 37A5E2F0298AA1B20047046B /* Persistence in Frameworks */, 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */, @@ -5189,8 +5207,10 @@ 9D9DE5732C63AA0700D20B15 /* AppKitExtensions in Frameworks */, F1DF95E72BD188B60045E591 /* LoginItems in Frameworks */, 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */, + CBECDB8A2CDBD616005B8B87 /* PageRefreshMonitor in Frameworks */, 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */, 4B4D60B12A0C83B900BCD287 /* NetworkProtectionUI in Frameworks */, + CBECDB8C2CDBD61C005B8B87 /* BrokenSitePrompt in Frameworks */, 98A50964294B691800D10880 /* Persistence in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -7723,6 +7743,7 @@ AA585D80248FD31100E9A3E2 /* DuckDuckGo */ = { isa = PBXGroup; children = ( + CBECDB822CDA812A005B8B87 /* BrokenSitePrompt */, 31521ABE2CC0139C00248E6F /* AIChat */, AA4D700525545EDE00C3411E /* Application */, AA585D85248FD31400E9A3E2 /* Assets.xcassets */, @@ -7771,6 +7792,7 @@ 4B2D06642A132F3A00DE1F49 /* NetworkProtectionAppExtension.entitlements */, 4B5F14C42A145D6A0060320F /* NetworkProtectionVPNController.entitlements */, 85B7184727677A7D00B4277F /* Onboarding */, + CBECDB7B2CD9824B005B8B87 /* PageRefreshMonitor */, 1D074B252909A371006E4AC3 /* PasswordManager */, B64C84DB2692D6E80048FEBE /* Permissions */, CDE248A22C821FD500F9399D /* PhishingDetection */, @@ -9512,6 +9534,24 @@ path = Resources; sourceTree = ""; }; + CBECDB7B2CD9824B005B8B87 /* PageRefreshMonitor */ = { + isa = PBXGroup; + children = ( + CB63DECA2CDC0BB80097986A /* PageRefreshMonitor.swift */, + CBECDB7F2CD98296005B8B87 /* PageRefreshStore.swift */, + ); + path = PageRefreshMonitor; + sourceTree = ""; + }; + CBECDB822CDA812A005B8B87 /* BrokenSitePrompt */ = { + isa = PBXGroup; + children = ( + CBECDB832CDA8137005B8B87 /* BrokenSitePromptLimiterStore.swift */, + CBECDB862CDACE29005B8B87 /* BrokenSitePromptLimiter.swift */, + ); + path = BrokenSitePrompt; + sourceTree = ""; + }; CD33012E2C89B5B3009AA127 /* ErrorPage */ = { isa = PBXGroup; children = ( @@ -9737,6 +9777,8 @@ C18BF9CD2C73678C00ED6B8A /* Freemium */, 567A23C42C7F75BB0010F66C /* SpecialErrorPages */, CD34F0C32C8869FF006826BE /* PhishingDetection */, + CBECDB8D2CDBD62C005B8B87 /* PageRefreshMonitor */, + CBECDB8F2CDBD631005B8B87 /* BrokenSitePrompt */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -10207,6 +10249,8 @@ C18BF9CB2C73678500ED6B8A /* Freemium */, 567A23C02C7F71570010F66C /* SpecialErrorPages */, CD34F0BB2C885D65006826BE /* PhishingDetection */, + CBECDB892CDBD616005B8B87 /* PageRefreshMonitor */, + CBECDB8B2CDBD61C005B8B87 /* BrokenSitePrompt */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -11625,6 +11669,7 @@ 3706FBF2293F65D500E42796 /* FindInPageModel.swift in Sources */, 1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, + CBECDB812CD9829A005B8B87 /* PageRefreshStore.swift in Sources */, 567A23D52C81E2180010F66C /* ContextualDaxDialogsFactory.swift in Sources */, 3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */, 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */, @@ -11688,6 +11733,7 @@ 1DA84D302C11989D0011C80F /* Update.swift in Sources */, B6E3E5592BBFD51400A41922 /* PreviewViewController.swift in Sources */, EEC8EB3E2982CA3B0065AA39 /* JSAlertViewModel.swift in Sources */, + CBECDB852CDA813C005B8B87 /* BrokenSitePromptLimiterStore.swift in Sources */, 3706FC16293F65D500E42796 /* PasswordManagementLoginModel.swift in Sources */, 3706FC17293F65D500E42796 /* TabViewModel.swift in Sources */, 3706FC18293F65D500E42796 /* TabDragAndDropManager.swift in Sources */, @@ -11783,6 +11829,7 @@ 3706FC50293F65D500E42796 /* FeedbackWindow.swift in Sources */, 3706FC51293F65D500E42796 /* RecentlyVisitedView.swift in Sources */, B6E3E5552BBFCEE300A41922 /* NoDownloadsCellView.swift in Sources */, + CBECDB882CDACE29005B8B87 /* BrokenSitePromptLimiter.swift in Sources */, B645D8F729FA95440024461F /* WKProcessPoolExtension.swift in Sources */, 9F9C49FE2BC7E9830099738D /* BookmarkAllTabsDialogViewModel.swift in Sources */, 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, @@ -11840,6 +11887,7 @@ B69A14FB2B4D705D00B9417D /* BookmarkFolderPicker.swift in Sources */, 3706FC6C293F65D500E42796 /* BookmarkViewModel.swift in Sources */, 3706FC6D293F65D500E42796 /* DaxSpeech.swift in Sources */, + CB63DECC2CDC0BBE0097986A /* PageRefreshMonitor.swift in Sources */, 3706FC6E293F65D500E42796 /* DuckURLSchemeHandler.swift in Sources */, 37445F9A2A1566420029F789 /* SyncDataProviders.swift in Sources */, 3706FC6F293F65D500E42796 /* FirePopoverViewModel.swift in Sources */, @@ -12886,6 +12934,7 @@ 4B0AACAC28BC63ED001038AC /* ChromiumFaviconsReader.swift in Sources */, AABEE6AB24ACA0F90043105B /* SuggestionTableRowView.swift in Sources */, 37CD54CB27F2FDD100F1F7B9 /* DownloadsPreferences.swift in Sources */, + CBECDB872CDACE29005B8B87 /* BrokenSitePromptLimiter.swift in Sources */, 4B1E6EF227AB5E5D00F51793 /* PasswordManagementItemList.swift in Sources */, AAC5E4D025D6A709007F5990 /* Bookmark.swift in Sources */, 4B0EF7262B578095009D6481 /* AppVersionExtension.swift in Sources */, @@ -13003,6 +13052,7 @@ CB6BCDF927C6BEFF00CC76DC /* PrivacyFeatures.swift in Sources */, 378F44EB29B4C73E00899924 /* ViewExtension.swift in Sources */, B6DB3CF926A00E2D00D459B7 /* AVCaptureDevice+SwizzledAuthState.swift in Sources */, + CBECDB842CDA813C005B8B87 /* BrokenSitePromptLimiterStore.swift in Sources */, 1E0C72062ABC63BD00802009 /* SubscriptionPagesUserScript.swift in Sources */, AAAB9116288EB46B00A057A9 /* VisitMenuItem.swift in Sources */, B6B71C582B23379600487131 /* NSLayoutConstraintExtension.swift in Sources */, @@ -13224,6 +13274,7 @@ C16127EE2BDFB46400966BB9 /* DataImportShortcutsView.swift in Sources */, 9FA173DF2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift in Sources */, F44C130225C2DA0400426E3E /* NSAppearanceExtension.swift in Sources */, + CBECDB802CD9829A005B8B87 /* PageRefreshStore.swift in Sources */, 4B3B8490297A0E1000A384BD /* EmailManagerExtension.swift in Sources */, B64C84F1269310120048FEBE /* PermissionManager.swift in Sources */, 37CD54D027F2FDD100F1F7B9 /* DefaultBrowserPreferences.swift in Sources */, @@ -13319,6 +13370,7 @@ 4B37EE5F2B4CFC3C00A89A61 /* HomePageRemoteMessagingStorage.swift in Sources */, 31F28C5128C8EEC500119F70 /* YoutubeOverlayUserScript.swift in Sources */, B6ABD0CA2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */, + CB63DECB2CDC0BBE0097986A /* PageRefreshMonitor.swift in Sources */, B6040856274B830F00680351 /* DictionaryExtension.swift in Sources */, 3199AF772C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift in Sources */, B684592725C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift in Sources */, @@ -14972,7 +15024,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 207.1.0; + version = 208.0.0; }; }; 9D84E4002CD4E66F0046CD8B /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { @@ -15813,6 +15865,26 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = Configuration; }; + CBECDB892CDBD616005B8B87 /* PageRefreshMonitor */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PageRefreshMonitor; + }; + CBECDB8B2CDBD61C005B8B87 /* BrokenSitePrompt */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = BrokenSitePrompt; + }; + CBECDB8D2CDBD62C005B8B87 /* PageRefreshMonitor */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = PageRefreshMonitor; + }; + CBECDB8F2CDBD631005B8B87 /* BrokenSitePrompt */ = { + isa = XCSwiftPackageProductDependency; + package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = BrokenSitePrompt; + }; CD34F0BB2C885D65006826BE /* PhishingDetection */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0b177bdc50..1bc6693de4 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" : "26cc3c597990db8a0f8aa4be743b25ce65076c95", - "version" : "207.1.0" + "revision" : "17154907fe86c75942331ed6d037694c666ddd95", + "version" : "208.0.0" } }, { diff --git a/DuckDuckGo/BrokenSitePrompt/BrokenSitePromptLimiter.swift b/DuckDuckGo/BrokenSitePrompt/BrokenSitePromptLimiter.swift new file mode 100644 index 0000000000..c193d0ca60 --- /dev/null +++ b/DuckDuckGo/BrokenSitePrompt/BrokenSitePromptLimiter.swift @@ -0,0 +1,27 @@ +// +// BrokenSitePromptLimiter.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 BrokenSitePrompt + +extension BrokenSitePromptLimiter { + + static let shared: BrokenSitePromptLimiter = BrokenSitePromptLimiter(privacyConfigManager: ContentBlocking.shared.privacyConfigurationManager, + store: BrokenSitePromptLimiterStore()) + +} diff --git a/DuckDuckGo/BrokenSitePrompt/BrokenSitePromptLimiterStore.swift b/DuckDuckGo/BrokenSitePrompt/BrokenSitePromptLimiterStore.swift new file mode 100644 index 0000000000..7eaaa4d699 --- /dev/null +++ b/DuckDuckGo/BrokenSitePrompt/BrokenSitePromptLimiterStore.swift @@ -0,0 +1,30 @@ +// +// BrokenSitePromptLimiterStore.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 BrokenSitePrompt + +final class BrokenSitePromptLimiterStore: BrokenSitePromptLimiterStoring { + + @UserDefaultsWrapper(key: .lastBrokenSiteToastShownDate, defaultValue: .distantPast) + var lastToastShownDate: Date + + @UserDefaultsWrapper(key: .toastDismissStreakCounter, defaultValue: 0) + var toastDismissStreakCounter: Int + +} diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index a296ce30cb..e2683d67c6 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1346,6 +1346,11 @@ struct UserText { static let daxDialogTapTheShield = NSLocalizedString("contextual.onboarding.browsing.trackers.tap.shield", value: "☝️ Tap the shield for more info.", comment: "Suggests to tap to a shield shaped icon that is above the copy") } + enum BrokenSitePrompt { + static let title = NSLocalizedString("site.not.working.title", value: "Site not working?", comment: "Title that appears on a dialog asking users about possible breakage of a site") + static let buttonTitle = NSLocalizedString("site.not.working.button.title", value: "Let Us Know", comment: "Button title that appears on a dialog asking users about possible breakage of a site") + } + // Key: "subscription.menu.item" // Comment: "Title for Subscription item in the options menu" static let subscriptionOptionsMenuItem = "Privacy Pro" diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index a18850b028..506f21637c 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -224,6 +224,16 @@ public struct UserDefaultsWrapper { // Subscription case subscriptionEnvironment = "subscription.environment" + + // PageRefreshMonitor + + case refreshTimestamps = "pageRefreshMonitor.refresh-timestamps" + + // BrokenSitePrompt + + case lastBrokenSiteToastShownDate = "brokenSitePrompt.last-broken-site-toast-shown-date" + case toastDismissStreakCounter = "brokenSitePrompt.toast-dismiss-streak-counter" + } enum RemovedKeys: String, CaseIterable { diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 036a9bc8fc..7b47106108 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -59289,6 +59289,126 @@ } } }, + "site.not.working.button.title" : { + "comment" : "Button title that appears on a dialog asking users about possible breakage of a site", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lass es uns wissen" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Let Us Know" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Háznoslo saber" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Faites-le nous savoir" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Faccelo sapere" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Laat het ons weten" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daj nam znać" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contacte-nos" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сообщить" + } + } + } + }, + "site.not.working.title" : { + "comment" : "Title that appears on a dialog asking users about possible breakage of a site", + "extractionState" : "extracted_with_value", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Webseite funktioniert nicht?" + } + }, + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Site not working?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿El sitio no funciona?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Le site ne fonctionne pas ?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Il sito non funziona?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Werkt de site niet?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Witryna nie działa?" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "O site não funciona?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сайт не работает?" + } + } + } + }, "Smart Copy/Paste" : { "comment" : "Main Menu Edit-Substitutions item", "localizations" : { @@ -64402,4 +64522,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index ef6c352471..7d8df888c9 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -23,6 +23,7 @@ import Common import NetworkProtection import NetworkProtectionIPC import os.log +import BrokenSitePrompt final class MainViewController: NSViewController { private lazy var mainView = MainView(frame: NSRect(x: 0, y: 0, width: 600, height: 660)) @@ -61,7 +62,8 @@ final class MainViewController: NSViewController { bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, autofillPopoverPresenter: AutofillPopoverPresenter, vpnXPCClient: VPNControllerXPCClient = .shared, - aiChatMenuConfig: AIChatMenuVisibilityConfigurable = AIChatMenuConfiguration()) { + aiChatMenuConfig: AIChatMenuVisibilityConfigurable = AIChatMenuConfiguration(), + brokenSitePromptLimiter: BrokenSitePromptLimiter = .shared) { self.aiChatMenuConfig = aiChatMenuConfig let tabCollectionViewModel = tabCollectionViewModel ?? TabCollectionViewModel() @@ -116,7 +118,8 @@ final class MainViewController: NSViewController { networkProtectionPopoverManager: networkProtectionPopoverManager, networkProtectionStatusReporter: networkProtectionStatusReporter, autofillPopoverPresenter: autofillPopoverPresenter, - aiChatMenuConfig: aiChatMenuConfig) + aiChatMenuConfig: aiChatMenuConfig, + brokenSitePromptLimiter: brokenSitePromptLimiter) browserTabViewController = BrowserTabViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager) findInPageViewController = FindInPageViewController.create() diff --git a/DuckDuckGo/MessageViews/PopoverMessageViewController.swift b/DuckDuckGo/MessageViews/PopoverMessageViewController.swift index 3d63c1cddf..6f4ebca0c2 100644 --- a/DuckDuckGo/MessageViews/PopoverMessageViewController.swift +++ b/DuckDuckGo/MessageViews/PopoverMessageViewController.swift @@ -91,7 +91,10 @@ final class PopoverMessageViewController: NSHostingController() private var cancellables = Set() private let aiChatMenuConfig: AIChatMenuVisibilityConfigurable + private let brokenSitePromptLimiter: BrokenSitePromptLimiter @UserDefaultsWrapper(key: .homeButtonPosition, defaultValue: .right) static private var homeButtonPosition: HomeButtonPosition @@ -120,21 +123,23 @@ final class NavigationBarViewController: NSViewController { private let networkProtectionButtonModel: NetworkProtectionNavBarButtonModel private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation - static func create(tabCollectionViewModel: TabCollectionViewModel, isBurner: Bool, + static func create(tabCollectionViewModel: TabCollectionViewModel, + isBurner: Bool, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), downloadListCoordinator: DownloadListCoordinator = .shared, dragDropManager: BookmarkDragDropManager = .shared, networkProtectionPopoverManager: NetPPopoverManager, networkProtectionStatusReporter: NetworkProtectionStatusReporter, autofillPopoverPresenter: AutofillPopoverPresenter, - aiChatMenuConfig: AIChatMenuVisibilityConfigurable) -> NavigationBarViewController { + aiChatMenuConfig: AIChatMenuVisibilityConfigurable, + brokenSitePromptLimiter: BrokenSitePromptLimiter) -> NavigationBarViewController { NSStoryboard(name: "NavigationBar", bundle: nil).instantiateInitialController { coder in - self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel, isBurner: isBurner, networkProtectionFeatureActivation: networkProtectionFeatureActivation, downloadListCoordinator: downloadListCoordinator, dragDropManager: dragDropManager, networkProtectionPopoverManager: networkProtectionPopoverManager, networkProtectionStatusReporter: networkProtectionStatusReporter, autofillPopoverPresenter: autofillPopoverPresenter, aiChatMenuConfig: aiChatMenuConfig) + self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel, isBurner: isBurner, networkProtectionFeatureActivation: networkProtectionFeatureActivation, downloadListCoordinator: downloadListCoordinator, dragDropManager: dragDropManager, networkProtectionPopoverManager: networkProtectionPopoverManager, networkProtectionStatusReporter: networkProtectionStatusReporter, autofillPopoverPresenter: autofillPopoverPresenter, aiChatMenuConfig: aiChatMenuConfig, brokenSitePromptLimiter: brokenSitePromptLimiter) }! } init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, isBurner: Bool, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation, downloadListCoordinator: DownloadListCoordinator, dragDropManager: BookmarkDragDropManager, networkProtectionPopoverManager: NetPPopoverManager, networkProtectionStatusReporter: NetworkProtectionStatusReporter, autofillPopoverPresenter: AutofillPopoverPresenter, - aiChatMenuConfig: AIChatMenuVisibilityConfigurable) { + aiChatMenuConfig: AIChatMenuVisibilityConfigurable, brokenSitePromptLimiter: BrokenSitePromptLimiter) { self.popovers = NavigationBarPopovers(networkProtectionPopoverManager: networkProtectionPopoverManager, autofillPopoverPresenter: autofillPopoverPresenter, isBurner: isBurner) self.tabCollectionViewModel = tabCollectionViewModel @@ -144,6 +149,7 @@ final class NavigationBarViewController: NSViewController { self.downloadListCoordinator = downloadListCoordinator self.dragDropManager = dragDropManager self.aiChatMenuConfig = aiChatMenuConfig + self.brokenSitePromptLimiter = brokenSitePromptLimiter goBackButtonMenuDelegate = NavigationButtonMenuDelegate(buttonType: .back, tabCollectionViewModel: tabCollectionViewModel) goForwardButtonMenuDelegate = NavigationButtonMenuDelegate(buttonType: .forward, tabCollectionViewModel: tabCollectionViewModel) super.init(coder: coder) @@ -423,6 +429,11 @@ final class NavigationBarViewController: NSViewController { name: AutoconsentUserScript.newSitePopupHiddenNotification, object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(attemptToShowBrokenSitePrompt(_:)), + name: .pageRefreshMonitorDidDetectRefreshPattern, + object: nil) + UserDefaults.netP .publisher(for: \.networkProtectionShouldShowVPNUninstalledMessage) .receive(on: DispatchQueue.main) @@ -538,6 +549,39 @@ final class NavigationBarViewController: NSViewController { } } + @objc private func attemptToShowBrokenSitePrompt(_ sender: Notification) { + guard brokenSitePromptLimiter.shouldShowToast(), + let url = tabCollectionViewModel.selectedTabViewModel?.tab.url, !url.isDuckDuckGo, + isOnboardingFinished + else { return } + showBrokenSitePrompt() + } + + private var isOnboardingFinished: Bool { + OnboardingActionsManager.isOnboardingFinished && Application.appDelegate.onboardingStateMachine.state == .onboardingCompleted + } + + private func showBrokenSitePrompt() { + guard view.window?.isKeyWindow == true, + let privacyButton = addressBarViewController?.addressBarButtonsViewController?.privacyEntryPointButton else { return } + brokenSitePromptLimiter.didShowToast() + PixelKit.fire(GeneralPixel.siteNotWorkingShown) + let popoverMessage = PopoverMessageViewController(message: UserText.BrokenSitePrompt.title, + buttonText: UserText.BrokenSitePrompt.buttonTitle, + buttonAction: { + self.brokenSitePromptLimiter.didOpenReport() + self.addressBarViewController?.addressBarButtonsViewController?.openPrivacyDashboardPopover(entryPoint: .prompt) + PixelKit.fire(GeneralPixel.siteNotWorkingWebsiteIsBroken) + }, + shouldShowCloseButton: true, + autoDismissDuration: nil, + onDismiss: { + self.brokenSitePromptLimiter.didDismissToast() + } + ) + popoverMessage.show(onParent: self, relativeTo: privacyButton, behavior: .semitransient) + } + func toggleDownloadsPopover(keepButtonVisible: Bool) { downloadsButton.isHidden = false diff --git a/DuckDuckGo/PageRefreshMonitor/PageRefreshMonitor.swift b/DuckDuckGo/PageRefreshMonitor/PageRefreshMonitor.swift new file mode 100644 index 0000000000..fdaa91e9b2 --- /dev/null +++ b/DuckDuckGo/PageRefreshMonitor/PageRefreshMonitor.swift @@ -0,0 +1,28 @@ +// +// PageRefreshMonitor.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 PageRefreshMonitor +import PixelKit + +extension PageRefreshMonitor { + + static let onDidDetectRefreshPattern: () -> Void = { + PixelKit.fire(GeneralPixel.pageRefreshThreeTimesWithin20Seconds) + } + +} diff --git a/DuckDuckGo/PageRefreshMonitor/PageRefreshStore.swift b/DuckDuckGo/PageRefreshMonitor/PageRefreshStore.swift new file mode 100644 index 0000000000..bc648f5605 --- /dev/null +++ b/DuckDuckGo/PageRefreshMonitor/PageRefreshStore.swift @@ -0,0 +1,26 @@ +// +// PageRefreshStore.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 PageRefreshMonitor + +final class PageRefreshStore: PageRefreshStoring { + + @UserDefaultsWrapper(key: .refreshTimestamps, defaultValue: []) + var refreshTimestamps: [Date] + +} diff --git a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardPopover.swift b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardPopover.swift index 346d23e39f..5c2c88f253 100644 --- a/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardPopover.swift +++ b/DuckDuckGo/PrivacyDashboard/View/PrivacyDashboardPopover.swift @@ -17,6 +17,7 @@ // import Cocoa +import PrivacyDashboard final class PrivacyDashboardPopover: NSPopover { @@ -35,22 +36,22 @@ final class PrivacyDashboardPopover: NSPopover { (contentViewController as? PrivacyDashboardViewController)! } - override init() { + init(entryPoint: PrivacyDashboardEntryPoint = .dashboard) { super.init() #if DEBUG self.behavior = .semitransient #else self.behavior = .transient #endif - setupContentController() + setupContentController(entryPoint: entryPoint) } required init?(coder: NSCoder) { fatalError("\(Self.self): Bad initializer") } - private func setupContentController() { - let controller = PrivacyDashboardViewController() + private func setupContentController(entryPoint: PrivacyDashboardEntryPoint) { + let controller = PrivacyDashboardViewController(entryPoint: entryPoint) controller.sizeDelegate = self contentViewController = controller } diff --git a/DuckDuckGo/Statistics/GeneralPixel.swift b/DuckDuckGo/Statistics/GeneralPixel.swift index 21bf3f4687..d7861dd042 100644 --- a/DuckDuckGo/Statistics/GeneralPixel.swift +++ b/DuckDuckGo/Statistics/GeneralPixel.swift @@ -442,6 +442,12 @@ enum GeneralPixel: PixelKitEventV2 { case errorPageShownOther case errorPageShownWebkitTermination + // Broken site prompt + + case pageRefreshThreeTimesWithin20Seconds + case siteNotWorkingShown + case siteNotWorkingWebsiteIsBroken + var name: String { switch self { @@ -1082,6 +1088,11 @@ enum GeneralPixel: PixelKitEventV2 { case .errorPageShownOther: return "m_mac_errorpageshown_other" case .errorPageShownWebkitTermination: return "m_mac_errorpageshown_webkittermination" + + // Broken site prompt + case .pageRefreshThreeTimesWithin20Seconds: return "m_mac_reload-three-times-within-20-seconds" + case .siteNotWorkingShown: return "m_mac_site-not-working_shown" + case .siteNotWorkingWebsiteIsBroken: return "m_mac_site-not-working_website-is-broken" } } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index e4449ac5bb..0a8ac5bdf6 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -29,6 +29,7 @@ import PhishingDetection import SpecialErrorPages import os.log import Onboarding +import PageRefreshMonitor protocol TabDelegate: ContentOverlayUserScriptDelegate { func tabWillStartNavigation(_ tab: Tab, isUserInitiated: Bool) @@ -68,6 +69,7 @@ protocol NewWindowPolicyDecisionMaker { private let statisticsLoader: StatisticsLoader? private let onboardingPixelReporter: OnboardingAddressBarReporting private let internalUserDecider: InternalUserDecider? + private let pageRefreshMonitor: PageRefreshMonitoring let pinnedTabsManager: PinnedTabsManager private let webViewConfiguration: WKWebViewConfiguration @@ -119,7 +121,9 @@ protocol NewWindowPolicyDecisionMaker { phishingDetector: PhishingSiteDetecting = PhishingDetection.shared, phishingState: PhishingTabStateManaging = PhishingTabStateManager(), tabsPreferences: TabsPreferences = TabsPreferences.shared, - onboardingPixelReporter: OnboardingAddressBarReporting = OnboardingPixelReporter() + onboardingPixelReporter: OnboardingAddressBarReporting = OnboardingPixelReporter(), + pageRefreshMonitor: PageRefreshMonitoring = PageRefreshMonitor(onDidDetectRefreshPattern: PageRefreshMonitor.onDidDetectRefreshPattern, + store: PageRefreshStore()) ) { let duckPlayer = duckPlayer @@ -165,7 +169,8 @@ protocol NewWindowPolicyDecisionMaker { phishingDetector: phishingDetector, phishingState: phishingState, tabsPreferences: tabsPreferences, - onboardingPixelReporter: onboardingPixelReporter) + onboardingPixelReporter: onboardingPixelReporter, + pageRefreshMonitor: pageRefreshMonitor) } @MainActor @@ -201,7 +206,8 @@ protocol NewWindowPolicyDecisionMaker { phishingDetector: PhishingSiteDetecting, phishingState: PhishingTabStateManaging, tabsPreferences: TabsPreferences, - onboardingPixelReporter: OnboardingAddressBarReporting + onboardingPixelReporter: OnboardingAddressBarReporting, + pageRefreshMonitor: PageRefreshMonitoring ) { self.content = content @@ -234,6 +240,7 @@ protocol NewWindowPolicyDecisionMaker { assert(userContentController != nil) self.userContentController = userContentController self.onboardingPixelReporter = onboardingPixelReporter + self.pageRefreshMonitor = pageRefreshMonitor webView = WebView(frame: CGRect(origin: .zero, size: webViewSize), configuration: configuration) webView.allowsLinkPreview = false @@ -801,6 +808,9 @@ protocol NewWindowPolicyDecisionMaker { userInteractionDialog = nil self.brokenSiteInfo?.tabReloadRequested() + if let url = webView.url { + pageRefreshMonitor.register(for: url) + } // In the case of an error only reload web URLs to prevent uxss attacks via redirecting to javascript:// if let error = error, diff --git a/LocalPackages/DataBrokerProtection/Package.swift b/LocalPackages/DataBrokerProtection/Package.swift index 75a8f5964f..1ca148fcbd 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: "207.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "208.0.0"), .package(path: "../SwiftUIExtensions"), .package(path: "../AppKitExtensions"), .package(path: "../XPCHelper"), diff --git a/LocalPackages/NetworkProtectionMac/Package.swift b/LocalPackages/NetworkProtectionMac/Package.swift index a51ac5b27b..10a3daafbf 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: "207.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "208.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 1a06e84748..5674c884bc 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: "207.1.0"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "208.0.0"), .package(path: "../SwiftUIExtensions") ], targets: [